【问题标题】:Is this valid Java?这是有效的Java吗?
【发布时间】:2011-03-07 19:17: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 编译器说
  • 我的previous Stackoverflow question 的人群说

我的java理论理解说

知道the JLS 对此有何评论会很有趣。

【问题讨论】:

  • @krock 他已经在问题中提到了这一点。
  • 鼻恶魔?我的意思是,到目前为止,你已经得到了人们给出的所有答案,但都没有定论。
  • 代码编译运行良好,没有人解释原因。
  • 实际上它可以工作并且可以运行并且输出:字符串数字

标签: java generics javac gcj


【解决方案1】:

--- 针对下面的 cmets 进行了编辑---

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

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

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

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

--- 原帖如下---

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

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

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

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

【讨论】:

  • 同意最后一部分 - 尝试做类似List&lt;String&gt; s = f(Arrays.asList("asdf")); List&lt;Integer&gt; i = f(Arrays.asList(123));
  • 我认为您只能删除两个签名之一,看看这两个签名是否等效。看我的回答。
  • 是的,我们都讨厌极端案例,我们只是想描述它们。但我不排除使用这种极端情况,并要求某人在所有这些“f”方法中置之不理,只是为了看看他将得到的所有错误的面貌。我希望说“f”方法不会在stackoverflow引擎中触发粗俗事件。
  • "Erasure 会将 List 和 List 都变成一个简单的 List。" => 不,不会!
【解决方案2】:

也可以工作(这次使用 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);
    }

}

【讨论】:

  • 我尝试编译该类并收到相同的结果。查看编译器生成的字节码后,似乎(在字节码级别),它正在做我的回答中演示的事情。
【解决方案3】:

这取决于您希望如何调用这些方法。如果您希望从其他 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 字符串 数字

更新

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 编译自“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 Language Specification 表明这实际上可能是有效的 Java 源代码。在第 449 页(第 15.12 节方法调用表达式)我们看到:

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

  • 如果所有最具体的方法都有覆盖等效(第 8.4.2 节)签名, 然后:
    • 如果没有将最具体的方法之一声明为抽象的, 这是最具体的方法。
    • 否则,如果所有最具体的方法都被声明为抽象的, 并且所有最大特定方法的签名具有相同的 擦除(§4.6),然后在其中任意选择最具体的方法 具有最具体的最具体方法的子集 返回类型。然而,最具体的方法被认为是抛出一个 当且仅当该异常或其擦除在 每个最具体的方法的 throws 子句。
  • 否则,我们说方法调用有歧义,编译时 发生错误。

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

更新 #3

感谢 ILMTitan 的评论:

@Adam Paynter:你的粗体字是 没关系,因为这只是一个案例 当有两种方法时 覆盖等效,丹显示 情况并非如此。就这样 决定因素必须是如果 JLS 考虑泛型类型时 确定最具体的方法。 – ILM钛

【讨论】:

  • +1 用于字节码调查。我个人认为编译器使用 List 泛型 argument type 来确定在源代码中调用了哪个方法,然后将其编译成基于 return type 区分的字节码有点不合常理我>,但你去。
  • @Tim:是的,这是字节码语义与语言语义不同的地方之一。我相信还有其他例子,但我想不起来了。
  • 仍然,不是生成的字节码回答了“这是有效的 java 吗?”这个问题。字节码只能回答“编译器的作者认为它是有效的 java 吗?”最终,我们必须依靠规范来回答这个问题,然后查看编译器看它是否兼容。
  • @Don:我同意,生成的字节码没有声明它是有效的 Java 源代码。也许我不清楚。我相信它不是有效的Java源代码。然而,一些编译器作者忽略了这一点并允许源代码编译,因为它在字节码级别不是问题(一些程序员可能试图针对)。
  • 这个问题的正确答案究竟如何?关于其他语言生成字节码的观点与该代码是否是有效的 Java 无关。 Java 编译器验证 Java 源代码,这是实际的问题。不是其他基于 JVM 的语言是否可以根据字节码的生成方式来区分这些方法。
【解决方案4】:

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

【讨论】:

  • 据我所知只有 eclipse 3.6 有这个问题。
【解决方案5】:

specification上有效。

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

  • m2m1 具有相同的签名,或者

  • m1 的签名与删除 m2

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

m1m2 的两个方法签名是 覆盖等效当且仅当 m1m2m2 的子签名是 m1 的子签名。

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

如果一个类的两个方法(无论是 都在同一个类中声明,或 都由一个类继承,或者一个 声明和一个继承)有 同名但签名不同 覆盖等效,然后是方法 据说 name 被重载了。

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

【讨论】:

  • 您似乎错过了您第一次引用的部分上方的项目符号,即 Don Branson 指出的部分。
【解决方案6】:

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

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

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

【讨论】:

    【解决方案7】:

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

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

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

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

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

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

    【讨论】:

      【解决方案8】:

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

      Java Type Inference is Broken: How Can We Fix it?

      【讨论】:

        【解决方案9】:

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

        原因如下:it's a feature

        在 javac 7 中,考虑了两种方法 重复(或名称冲突错误) 无论它们的返回类型如何。

        这种行为现在更加一致 使用 javac 1.5,报告名称 方法上的冲突错误并被忽略 他们的返回类型。只有在 1.6 中是 包含返回类型的更改 检测重复方法时。

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

        【讨论】:

        • 感谢拼图中缺失的部分。
        【解决方案10】:

        看起来编译器根据泛型选择了最具体的方法。

        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
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-04-15
          • 2020-03-12
          • 1970-01-01
          • 1970-01-01
          • 2012-08-23
          • 1970-01-01
          相关资源
          最近更新 更多