59

这是有效的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));
    }

}
  • Eclipse 3.5 说
  • Eclipse 3.6说不
  • Intellij 9 说是的
  • Sun javac 1.6.0_20 说
  • GCJ 4.4.3 说
  • GWT 编译器说
  • 我之前的 Stackoverflow 问题人群说不

我对java理论的理解说不

了解JLS对此的看法会很有趣。

4

10 回答 10

29

这取决于您希望如何调用这些方法。如果您希望从其他 Java 源代码调用这些方法,那么由于Edwin 的回答中说明的原因,它被认为是无效的。这是 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
字符串
数字

更新

Simon 发布了一个调用这些方法的示例程序:

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
编译自“RealyCompilesAndRunsFine.java”
类 RealyCompilesAndRunsFine 扩展 java.lang.Object{
RealyCompilesAndRunsFine();
  代码:
   0:aload_0
   1:调用特殊#1;//方法 java/lang/Object."":()V
   4:返回

公共静态 java.lang.String f(java.util.List);
  代码:
   0:aload_0
   1:iconst_0
   2:调用接口#2、2;//接口方法 java/util/List.get:(I)Ljava/lang/Object;
   7:检查广播#3;//类java/lang/String
   10:返回

公共静态 java.lang.Integer f(java.util.List);
  代码:
   0:aload_0
   1:iconst_0
   2:调用接口#2、2;//接口方法 java/util/List.get:(I)Ljava/lang/Object;
   7:检查广播#4;//类 java/lang/Integer
   10:返回

公共静态无效主(java.lang.String[]);
  代码:
   0:iconst_1
   1:新数组#3;//类java/lang/String
   4:重复
   5:iconst_0
   6:最不发达国家#5;//字符串asdf
   8:商店
   9:调用静态#6;//方法 java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
   12:调用静态#7;//方法f:(Ljava/util/List;)Ljava/lang/String;
   15:astore_1
   16:图标st_1
   17:新阵列#4;//类 java/lang/Integer
   20:重复
   21:iconst_0
   22:双推 123
   24:调用静态#8;//方法 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   27:商店
   28:调用静态#6;//方法 java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
   31:调用静态#9;//方法f:(Ljava/util/List;)Ljava/lang/Integer;
   34:astore_2
   35:获取静态#10;//字段 java/lang/System.out:Ljava/io/PrintStream;
   38:加载_1
   39:调用虚拟#11;//方法java/io/PrintStream.println:(Ljava/lang/String;)V
   42:获取静态#10;//字段 java/lang/System.out:Ljava/io/PrintStream;
   45:加载_2
   46:调用虚拟#12;//方法java/io/PrintStream.println:(Ljava/lang/Object;)V
   49:返回

事实证明,Sun 编译器正在生成消除方法歧义所需的字节码(参见最后一个方法中的说明 12 和 31)。

更新#2

Java 语言规范表明这实际上可能是有效的 Java 源代码。在第 449 页(§15.12 方法调用表达式)我们看到:

可能没有一种方法是最具体的,因为有两种或多种方法是最具体的。在这种情况下:

  • 如果所有最具体的方法都具有覆盖等效(第 8.4.2 节)签名,则:
    • 如果没有将最具体的方法之一声明为抽象的,则它是最具体的方法。
    • 否则,如果所有最大特定方法都被声明为抽象方法,并且所有最大特定方法的签名具有相同的擦除(第 4.6 节),则在具有最具体的返回类型。但是,当且仅当在每个最大特定方法的 throws 子句中声明了该异常或其擦除时,才认为最特定的方法会引发已检查异常。
  • 否则,我们说方法调用有歧义,出现编译时错误。

除非我弄错了,否则这种行为应该只适用于声明为抽象的方法,虽然......

更新#3

感谢 ILMTitan 的评论:

@Adam Paynter:您的粗体文本无关紧要,因为这只是两种方法等效覆盖的情况,而丹表明情况并非如此。因此,决定因素必须是 JLS 在确定最具体的方法时是否考虑泛型类型。– ILM钛

于 2010-06-24T13:11:56.387 回答
13

---根据以下评论进行了编辑---

好的,所以它是有效的 Java,但它不应该是。关键是它并不真正依赖于返回类型,而是依赖于擦除的泛型参数。

这不适用于非静态方法,并且在非静态方法上被明确禁止。由于额外的问题,在类中尝试这样做会失败,首先是典型的类不像类Class那样是final的。

这是在其他方面相当一致的语言中的不一致。TI 会四处走动,说这应该是非法的,即使在技术上是允许的。它并没有真正增加语言的可读性,也几乎没有增加解决有意义问题的能力。它似乎要解决的唯一有意义的问题是,您是否对该语言足够熟悉,知道何时该语言在解决类型擦除、泛型和生成的方法签名方面的内部不一致似乎违反了它的核心原则。

绝对要避免代码,因为以任何数量的更有意义的方式解决相同的问题是微不足道的,唯一的好处是查看审阅者/扩展者是否知道语言规范中尘土飞扬的肮脏角落。

--- 原帖如下---

虽然编译器可能允许这样做,但答案仍然是否定的。

Erasure 会将 List<String> 和 List<Integer> 都变成一个朴素的 List。这意味着您的两个“f”方法将具有相同的签名但不同的返回类型。返回类型不能用于区分方法,因为当你返回一个普通的超类型时这样做会失败;像:

Object o = f(Arrays.asList("asdf"));

您是否尝试过将返回的值捕获到变量中?也许编译器已经优化了一些东西,以至于它没有踩到正确的错误代码。

于 2010-06-24T12:58:03.050 回答
11

一个没有得到解答的疑问是:为什么它只会在 Eclipse 3.6 中触发编译错误?

原因如下:这是一个功能

在 javac 7 中,无论返回类型如何,两个方法都被视为重复(或名称冲突错误)。

此行为现​​在与 javac 1.5 更加一致,后者报告了方法上的名称冲突错误并忽略了它们的返回类型。仅在 1.6 中进行了更改,包括检测重复方法时的返回类型。

我们决定在 3.6 版本中对所有合规级别(1.5、1.6、1.7)进行此更改,因此如果用户使用 javac 7 编译代码,他们不会对更改感到惊讶。

于 2010-06-25T11:01:24.947 回答
5

它在规范中有效。

方法的签名是方法m1签名的子签名,m2如果有的话

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

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

所以这些不是彼此的子签名,因为擦除List<String>不是List<Integer>,反之亦然。

两个方法签名m1并且m2是覆盖等效的当且仅当m1是 的子签名m2m2的子签名m1

所以这两个不是覆盖等效的(注意iff)。重载的规则是:

如果一个类的两个方法(无论是在同一个类中声明,还是都由一个类继承,或者一个声明一个继承)具有相同的名称但签名不是覆盖等效的,则该方法名称被称为超载。

因此,这两个方法被重载,一切都应该工作。

于 2010-06-24T13:27:33.733 回答
5

好吧,如果我在规范第 8.4.2 节的第一个列表中正确理解了项目符号三,它说您的 f() 方法具有相同的签名:

http://java.sun.com/docs/books/jls/third_edition/html/classes.html#38649

真正回答这个问题的是规范,而不是编译器 X 或 IDE X 的观察行为。通过查看工具,我们只能说工具的作者如何解释规范。

如果我们应用第三条,我们得到:

...
    公共静态字符串 f(List<String> 列表) {
        System.out.println("字符串");
        返回空值;
    }

    公共静态整数 f(List<String> 列表) {
        System.out.println("数字");
        返回空值;
    }
...

并且签名匹配,因此存在冲突并且代码不应编译。

于 2010-06-24T13:31:38.793 回答
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);
    }

}
于 2010-06-24T13:10:51.500 回答
1

据我所知,.class 文件可以包含这两种方法,因为方法描述符包含参数以及返回类型。如果返回类型相同,则描述符将相同,并且在类型擦除后方法将无法区分(因此它也不适用于 void)。http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html#7035

现在,用invoke_virtual调用方法需要方法描述符,所以实际上你可以说你想调用哪个方法,所以看起来所有那些仍然有通用信息的编译器只是简单地把方法描述符放在匹配参数的泛型类型,因此它在字节码中硬编码要调用的方法(由它们的描述符或更确切地说由这些描述符中的返回类型区分),即使参数现在是一个列表,没有泛型信息。

虽然我觉得这种做法有点问题,但我必须说我觉得你可以这样做很酷,并且认为泛型应该首先被设计成能够像这样工作(是的,我知道那会产生向后兼容性的问题)。

于 2010-06-24T13:29:31.143 回答
1

Java 类型推断(当您调用像 Array.asList 这样的静态通用方法时会发生什么)很复杂,并且在 JLS 中没有很好地指定。2008 年的这篇论文对一些问题以及如何修复它进行了非常有趣的描述:

Java 类型推断被破坏:我们如何修复它?

于 2010-06-24T14:16:48.513 回答
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

于 2010-06-24T13:27:04.343 回答
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
于 2010-06-25T13:53:13.130 回答