Java泛型代码可以通过javac编译,但在Eclipse Helios上失败。

9
我有以下测试类,使用泛型来重载方法。使用javac编译时可以运行,但在Eclipse Helios中无法编译通过。我的java版本是1.6.0_21。
我读到的所有文章都表明Eclipse是正确的,这段代码不应该工作。然而,使用javac编译和运行时,选择了正确的方法。
这怎么可能?
谢谢!
import java.util.ArrayList;

public class Test {
    public static void main (String [] args) {
        Test t = new Test();
        ArrayList<String> ss = new ArrayList<String>();
        ss.add("hello");
        ss.add("world");
        ArrayList<Integer> is = new ArrayList<Integer>();
        is.add(1);
        is.add(2);
        System.out.println(t.getFirst(ss));
        System.out.println(t.getFirst(is));
    }   
    public String getFirst (ArrayList<String> ss) {
        return ss.get(0);
    }
    public Integer getFirst (ArrayList<Integer> ss) {
        return ss.get(0);
    }
}

4
如果这是一个实际的用例,我建议只使用这一种方法 public <A> A getFirst (ArrayList<A> ss) { return ss.get(0); }... 如果这只是为了举例而编造的,那就继续。 - Tom Crockett
在 Eclipse 中的编译错误是什么? - meriton
在Galileo中运行良好,正如它应该的那样。 - erickson
@meriton:错误是:方法getFirst(ArrayList<String>)的擦除与类型Test中的另一个方法具有相同的擦除getFirst(ArrayList<E>)。 - Deech
8个回答

6

Java语言规范, 第8.4.2节中写到:

在一个类中声明两个具有覆盖等效签名(下面定义)的方法是一种编译时错误。

如果m1是m2的子签名或者m2是m1的子签名,则两个方法签名m1和m2是覆盖等效的。

如果m1的签名与m2的签名相同,或者m1的签名与m2的擦除签名相同,则m1的签名是m2的子签名。

  • m2具有与m1相同的签名,或

  • m1的签名与m2的擦除签名相同。

很明显,这两个方法不具有覆盖等效签名,因为ArrayList<String>不是ArrayListArrayList<Integer>的擦除签名)。

声明方法是合法的。另外,方法调用表达式也是有效的,因为有一个最具体的方法,由于只有一个匹配参数类型的方法。编辑:Yishai正确指出,在这种情况下还有另一个严格的限制。Java语言规范,第8.4.8.3节写道:如果类型声明T具有成员方法m1,并且存在在T或T的超类型中声明的方法m2,使得满足以下所有条件,则编译时错误:m1和m2具有相同的名称;m2可以从T访问;m1的签名不是m2的子签名(§8.4.2);m1或某个方法m1直接或间接地覆盖具有与m2相同的擦除或某个方法m2覆盖(直接或间接)。附录:关于擦除和缺乏擦除
与普遍观念相反,方法签名中的泛型并未被擦除。泛型在字节码(Java虚拟机的指令集)中被擦除。方法签名不是指令集的一部分,它们按照源代码规定写入类文件。(顺带一提,这些信息也可以通过反射在运行时查询。)
请思考:如果类型参数完全从类文件中被擦除,你如何在你选择的IDE中使用代码自动完成功能显示ArrayList.add(E)需要一个类型为E的参数,而不是Object(即E的擦除形式),如果你没有附加JDK源代码?编译器如何知道当静态类型的方法参数不是E的子类型时抛出编译错误?

3
我认为这个情况比你想象的更近。考虑到:如果你将两个方法的返回类型都改为Object,那么代码将无法编译。JLS 没有说返回类型可以作为重载等效性判定方法签名的区分特征。我最终同意Sun编译器是正确的,但这非常微妙。 - Yishai
好的,我已经编辑过了,包括相关部分。这个规则不是关于覆盖等价性(其中指定返回类型不重要),而是更一般性质的。 - meriton

4

这段代码是正确的,正如JLS 15.12.2.5 选择最具体的方法中所描述的。

此外,考虑编写接口:

List<String> ss = new ArrayList<String>();
List<Integer> is = new ArrayList<Integer>();
// etc.

正如 @McDowell 所指出的那样,修改后的方法签名出现在类文件中:
$ javap build/classes/Test
Compiled from "Test.java"
public class Test extends java.lang.Object{
    public Test();
    public static void main(java.lang.String[]);
    public java.lang.String getFirst(java.util.ArrayList);
    public java.lang.Integer getFirst(java.util.ArrayList);
}

请注意,这并不与@meriton有关于类文件的观察相矛盾。例如,此片段的输出为:
Method[] methods = Test.class.getDeclaredMethods();
for (Method m : methods) {
    System.out.println(Arrays.toString(m.getGenericParameterTypes()));
}

显示main()的形式参数,以及两个通用类型参数:

[class [Ljava.lang.String;]
[java.util.ArrayList<java.lang.String>]
[java.util.ArrayList<java.lang.Integer>]

1
类型擦除后,方法签名变为public String getFirst(ArrayList)public Integer getFirst(ArrayList) - McDowell
1
不是 McDowell,方法签名没有被擦除。 - meriton
@meriton:@McDowell:如果我理解正确的话,你们两个都是正确的:签名被擦除了,但泛型类型参数被保留了,正如上面所建议的那样。 - trashgod

3

在Eclipse Helios中,这对我来说可行。方法选择发生在编译时,编译器有足够的信息来完成此操作。


2
经过一些研究,我找到了答案:
如上所述的代码应该无法编译。在运行时,ArrayList<String>ArrayList<Integer>仍然是ArrayList。但是你的代码不起作用是因为返回类型不同。如果你为两个方法设置相同的返回类型,javac就不会编译它...
我读到Java 1.6中有一个错误(在Java 1.7中已经修复)关于这个错误。这都是关于返回类型的问题...所以,你需要改变你的方法签名。
这里有Oracle Bug Database中的bug 6182950

2
如果javac存在缺陷,那么这是可能的。Javac仅仅是软件,与其他任何软件一样容易出现漏洞。此外,Java语言规范非常复杂,在某些地方有点模糊,因此这也可能是Eclipse团队和javac团队之间解释差异的原因。
我建议首先在Eclipse支持渠道上咨询此问题。他们通常非常善于发现这些问题并解释他们认为自己正确的理由或者承认他们的错误。
就记录而言,我认为Eclipse在这里是正确的。

我只是根据历史平均值猜测的。考虑到其他人的分析,我认为你是正确的。无论如何,与Eclipse团队讨论这个问题是前进的方向。 - dty

2

你确定Eclipse也设置为使用Java 1.6吗?


2
出于兴趣,你认为这个问题有什么关联吗?Eclipse至少设置为1.5,否则根本无法编译。你认为1.5和1.6之间的语言语义存在显著差异吗? - dty
我只是想确保它没有设置为1.4。我不使用eclipse,但我知道IDE通常具有独立于您系统的Java版本设置。至于1.5-1.6的区别,我不知道有任何重大的区别。 - Jeffrey
如果将其设置为1.4,由于泛型,它根本无法编译。 - dty

0
Eclipse和javac使用不同的编译器。Eclipse使用第三方编译器将您的代码转换为Java VM的字节码。Javac使用Sun发布的Java编译器。因此,相同的代码可能会产生略微不同的结果。我相信Netbeans使用Sun的编译器,所以也可以在其中进行检查。

2
这里的语言有点宽松。明确一下,javac是Sun(Oracle)发布的编译器。而Eclipse不使用第三方编译器——它使用自己的编译器实现。 - dty

0
需要记住的一点是,(所有?当然有些,例如NetBeans编辑器)静态代码分析工具不将返回类型或修饰符(private/public等)视为方法签名的一部分。
如果是这种情况,那么借助类型擦除,两个“getFirst”方法都会得到签名“getFirst(java.util.ArrayList)”,从而触发名称冲突...

泛型在字节码中被擦除(Java虚拟机的指令集)。方法签名不是指令集的一部分;它们按照源代码中的规定写入类文件。有关更多信息,请参见我的答案。 - meriton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接