这是有效的Java吗?
import java.util.Arrays;
import java.util.List;
class TestWillThatCompile {
public static String f(List<String> list) {
System.out.println("strings");
return null;
}
public static Integer f(List<Integer> list) {
System.out.println("numbers");
return null;
}
public static void main(String[] args) {
f(Arrays.asList("asdf"));
f(Arrays.asList(123));
}
}
我的Java理论理解是没有!
了解the JLS对此有何看法会很有趣。
答案 0 :(得分:29)
这取决于您希望如何调用这些方法。如果您希望从其他Java源代码中调用这些方法 ,那么由于Edwin's answer中说明的原因,它被视为无效。这是Java语言的限制。
但是,并非所有类都需要从Java源代码生成(考虑使用JVM作为运行时的所有语言:JRuby,Jython等...)。 在字节码级别,JVM可以消除两种方法的歧义,因为字节码指令指定了他们期望的返回类型。例如,这是一个用Jasmin编写的类,可以调用这些方法之一:
.class public CallAmbiguousMethod
.super java/lang/Object
.method public static main([Ljava/lang/String;)V
.limit stack 3
.limit locals 1
; Call the method that returns String
aconst_null
invokestatic TestWillThatCompile/f(Ljava/util/List;)Ljava/lang/String;
; Call the method that returns Integer
aconst_null
invokestatic TestWillThatCompile/f(Ljava/util/List;)Ljava/lang/Integer;
return
.end method
我使用以下命令将其编译为类文件:
java -jar jasmin.jar CallAmbiguousMethod.j
并使用以下方式调用它:
java CallAmbiguousMethod
看哪,输出是:
> java CallAmbiguousMethod strings numbers
<强>更新强>
Simon发布了调用这些方法的an example program:
import java.util.Arrays;
import java.util.List;
class RealyCompilesAndRunsFine {
public static String f(List<String> list) {
return list.get(0);
}
public static Integer f(List<Integer> list) {
return list.get(0);
}
public static void main(String[] args) {
final String string = f(Arrays.asList("asdf"));
final Integer integer = f(Arrays.asList(123));
System.out.println(string);
System.out.println(integer);
}
}
这是生成的Java字节码:
>javap -c RealyCompilesAndRunsFine Compiled from "RealyCompilesAndRunsFine.java" class RealyCompilesAndRunsFine extends java.lang.Object{ RealyCompilesAndRunsFine(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."":()V 4: return public static java.lang.String f(java.util.List); Code: 0: aload_0 1: iconst_0 2: invokeinterface #2, 2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 7: checkcast #3; //class java/lang/String 10: areturn public static java.lang.Integer f(java.util.List); Code: 0: aload_0 1: iconst_0 2: invokeinterface #2, 2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 7: checkcast #4; //class java/lang/Integer 10: areturn public static void main(java.lang.String[]); Code: 0: iconst_1 1: anewarray #3; //class java/lang/String 4: dup 5: iconst_0 6: ldc #5; //String asdf 8: aastore 9: invokestatic #6; //Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List; 12: invokestatic #7; //Method f:(Ljava/util/List;)Ljava/lang/String; 15: astore_1 16: iconst_1 17: anewarray #4; //class java/lang/Integer 20: dup 21: iconst_0 22: bipush 123 24: invokestatic #8; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 27: aastore 28: invokestatic #6; //Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List; 31: invokestatic #9; //Method f:(Ljava/util/List;)Ljava/lang/Integer; 34: astore_2 35: getstatic #10; //Field java/lang/System.out:Ljava/io/PrintStream; 38: aload_1 39: invokevirtual #11; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 42: getstatic #10; //Field java/lang/System.out:Ljava/io/PrintStream; 45: aload_2 46: invokevirtual #12; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V 49: return
事实证明,Sun编译器正在生成消除方法歧义所需的字节码(参见上一种方法中的指令12和31)。
更新#2
Java Language Specification表明这实际上可能是有效的Java源代码。在页449(§15.12方法调用表达式),我们看到:
可能没有方法是最具体的,因为有两个或 更多最具体的方法。在这种情况下:
- 如果所有最大特定方法都具有覆盖等效(§8.4.2)签名, 然后:
- 如果没有将最大特定方法中的一个声明为抽象, 这是最具体的方法。
- 否则,如果所有最大特定方法都被声明为abstract, 并且所有最大特定方法的签名都具有相同的特征 擦除(§4.6),然后在其中任意选择最具体的方法 具有最具体的最大特定方法的子集 返回类型。但是,最具体的方法被认为是抛出一个 当且仅当声明该异常或其擦除时检查异常 每种最具体方法的抛出条款。
- 否则,我们说方法调用是不明确的,而且是编译时 发生错误。
除非我弄错了,否则此行为应仅适用于声明为abstract的方法,但是......
更新#3
感谢ILMTitan的评论:
@Adam Paynter:您的粗体文字确实如此 没关系,因为它只是一个案例 当两种方法是 覆盖等价,丹表示 事实并非如此。就这样 决定因素必须是JLS 将通用类型考虑在内 确定最具体的方法。 - ILMTitan
答案 1 :(得分:13)
---编辑以回应以下评论---
好的,所以它是有效的Java,但它不应该。关键是它不是真的依赖于返回类型,而是依赖于擦除的泛型参数。
这不适用于非静态方法,并且在非静态方法上明确禁止。尝试在课堂上这样做会因为额外的问题而失败,首先是典型的课程不是 final ,因为课程 Class 。
在其他相当一致的语言中,这是不一致的。即使在技术上允许,TI也会说它应该是非法的。它并没有真正为语言的可读性添加任何东西,并且它对解决有意义的问题的能力几乎没有增加。它似乎解决的唯一有意义的问题是你是否熟悉该语言,以便知道何时它的核心 tenets 似乎被语言在解析类型擦除,泛型和结果方法中的内部不一致所违反签名。
绝对要避免的代码,因为以任何更有意义的方式解决同样的问题是微不足道的,唯一的好处是看看评论者/扩展者是否知道语言规范中一个尘土飞扬的脏角。
---原帖后面---
虽然编译器可能允许它,但答案仍然是否定的。
Erasure将同时转换List&lt; String&gt;和列表&lt;整数&gt;成为一个朴素的名单。这意味着您的两个“f”方法将具有相同的签名但返回类型不同。返回类型不能用于区分方法,因为当您返回到公共超类型时,这样做会失败;像:
Object o = f(Arrays.asList("asdf"));
您是否尝试将返回的值捕获到变量中?也许编译器已经对这些事情进行了优化,以至于它没有踩到正确的错误代码。
答案 2 :(得分:11)
有人质疑尚未回答的问题是:为什么它只会在Eclipse 3.6中触发编译错误?
原因如下:it's a feature。
在javac 7中,考虑了两种方法 重复(或名称冲突错误) 无论他们的回报类型如何。
此行为现在更加一致 与javac 1.5,报告了名称 方法上的冲突错误并被忽略 他们的回报类型。只有1.6是一个 改变包括返回类型 检测重复方法时。
我们已决定进行此更改 所有合规级别(1.5, 1.6, 1.7)在3.6版本中,如果用户不会对变更感到惊讶 使用javac 7编译代码。
答案 3 :(得分:5)
它在specification上有效。
方法
m1
的签名是 签名的子签名 方法m2
,如果
m2
与m1
具有相同的签名,或
m1
的签名与签名的擦除相同m2
。
因此,这些不是彼此的子签名,因为List<String>
的删除不是List<Integer>
,反之亦然。
两个方法签名
m1
和m2
是 覆盖等效iffm1
是am2
或m2
的子签名是am1
的子签名。
所以这两个不是覆盖等价的(注意 iff )。超载的规则是:
如果一个类的两个方法(是否 两者都在同一类中声明,或者 都由一个类继承,或者一个 声明和一个继承)有 相同的名称,但签名不是 override - 等效,然后是方法 据说名称超载。
因此,这两种方法都被重载了,一切都应该有效。
答案 4 :(得分:5)
好吧,如果我在规范8.4.2节的第一个列表中正确理解了第三点,它说你的f()方法具有相同的签名:
http://java.sun.com/docs/books/jls/third_edition/html/classes.html#38649
这是真正回答这个问题的规范,而不是编译器X或IDE X的观察行为。我们可以通过查看工具来说明该工具的作者是如何解释规范的。
如果我们应用子弹三,我们得到:
... public static String f(List<String> list) { System.out.println("strings"); return null; } public static Integer f(List<String> list) { System.out.println("numbers"); return null; } ...
并且签名匹配,因此存在冲突,代码不应该编译。
答案 5 :(得分:1)
同样工作(这次是sun java 1.6.0_16)
import java.util.Arrays;
import java.util.List;
class RealyCompilesAndRunsFine {
public static String f(List<String> list) {
return list.get(0);
}
public static Integer f(List<Integer> list) {
return list.get(0);
}
public static void main(String[] args) {
final String string = f(Arrays.asList("asdf"));
final Integer integer = f(Arrays.asList(123));
System.out.println(string);
System.out.println(integer);
}
}
答案 6 :(得分:1)
据我所知,.class文件可以包含两种方法,因为方法描述符包含参数,以及返回类型。如果返回类型相同,那么描述符将是相同的,并且在类型擦除之后方法将是无法区分的(因此它也不适用于void)。 http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html#7035
现在,使用invoke_virtual调用方法需要方法描述符,所以你实际上可以说你要调用哪一个方法,所以看起来所有那些仍然具有通用信息的编译器只是放置描述符对于匹配参数的泛型类型的方法,那么它在字节码中被硬编码调用哪个方法(由它们的描述符区分,或者更准确地说是那些描述符中的返回类型),即使参数现在是List ,没有通用信息。
虽然我发现这种做法有点可疑,但我必须说我觉得你可以做到这一点很酷,并认为仿制药本来应该设计成能够像这样工作(是的,我知道会产生向后兼容性的问题。)
答案 7 :(得分:1)
Java类型推断(当您调用静态,泛型方法(如Array.asList)时发生的事情)很复杂,并且在JLS中没有明确指定。本文从2008年开始对一些问题进行了非常有趣的描述,以及如何解决这些问题:
答案 8 :(得分:0)
Eclipse可以生成字节代码:
public class Bla {
private static BigDecimal abc(List<BigDecimal> l) {
return l.iterator().next().multiply(new BigDecimal(123));
}
private static String abc(List<String> l) {
return l.iterator().next().length() + "";
}
public static void main(String[] args) {
System.out.println(abc(Arrays.asList("asdf")));
System.out.println(abc(Arrays.<BigDecimal>asList(new BigDecimal(123))));
}
}
输出:
4
15129
答案 9 :(得分:0)
看起来编译器根据泛型选择最具体的方法。
import java.util.Arrays;
import java.util.List;
class TestWillThatCompile {
public static Object f(List<?> list) {
System.out.println("strings");
return null;
}
public static Integer f(List<Integer> list) {
System.out.println("numbers");
return null;
}
public static void main(String[] args) {
f(Arrays.asList("asdf"));
f(Arrays.asList(123));
}
}
输出:
strings
numbers