Java 8 autoboxing +泛型:变量与方法的不同行为

我发现从Java 7切换到Java 8后停止编译的一段代码。 它没有任何新的Java 8的东西,如lambda或流。

我将有问题的代码缩小到以下情况:

GenericData<Double> g = new GenericData<>(1d);
Double d = g == null ? 0 : g.getData(); // type error!!!

您大概可以猜测GenericData的构造函数具有该泛型类型的一个参数,而getData()方法只返回该泛型类型。 (完整的源代码见下文。)

现在困扰我的是,在Java 7中,代码编译得很好,而在Java 8中,我得到以下错误:

CompileMe.java:20: error: incompatible types: bad type in conditional expression
Double d = g == null ? 0 : g.getData();
                       ^
int cannot be converted to Double

看起来Java 7能够从int - > double - > Double进行转换,但是Java 8试图立即从int - > Double进行失败。

我特别感到有趣的是,当我将它从getData()更改为data时,Java 8确实接受了代码,即通过变量本身而不是getter-method访问GenericData的值:

Double d2 = g == null ? 0 : g.data; // now why does this work...

所以我在这里提出的两个问题是:

  • 为什么Java 8不推荐像Java 7那样的类型,并且在自动装箱之前将我的int投入double倍增到Double?
  • 为什么这个问题只发生在泛型方法中,而不是泛型变量?
  • 完整的源代码:

    public class CompileMe {
        public void foo() {
            GenericData<Double> g = new GenericData(1d);
            Double d = g == null ? 0 : g.getData(); // type error!!!
            Double d2 = g == null ? 0 : g.data; // now why does this work...
        }
    }
    
    class GenericData<T> {
        public T data;
        public GenericData(T data) {
            this.data = data;
        }
        public T getData() {
            return data;
        }
    }
    

    要测试它,运行编译器,如下所示:

    javac -source 1.7 -target 1.7 CompileMe.java   # ok (just warnings)
    javac -source 1.8 -target 1.8 CompileMe.java   # error (as described above)
    

    最后以防万一:我运行Windows 8和Java 1.8.0_112(64位)。


    方法调用表达式的特殊之处在于它们可以是聚合表达式,受到目标输入的限制。

    考虑下面的例子:

    static Double aDouble() {
        return 0D;
    }
    …
    Double d = g == null ? 0 : aDouble();
    

    这编译没有任何问题

    static <T> T any() {
        return null;
    }
    …
    Double d = g == null ? 0 : any();
    

    在这里, any()的调用是一个Poly表达式,编译器必须推断T := Double 。 这再现了同样的错误。

    这是第一个不一致。 虽然你的方法getData()引用了GenericData的类型参数T ,但它不是一个通用的方法(在这里应该没有类型推断来确定TDouble

    JLS§8.4.4。 通用方法

    如果一个方法声明了一个或多个类型变量,则该方法是通用的

    getData()不声明类型变量,它只使用一个。

    JLS§15.12。 方法调用表达式:

    如果满足以下所有条件,则方法调用表达式是一个poly表达式:

  • ...
  • 由以下小节确定的要调用的方法是通用的(第8.4.4节),并且返回类型至少提到了方法的一个类型参数。
  • 由于此方法调用不是poly表达式,因此它应该像使用aDouble()调用的示例而不是any()

    但是注意§15.25.3:

    请注意,引用条件表达式不必包含多表达式作为操作数以便成为多表达式。 这是一种多元表达,仅凭其出现的上下文而已。 例如,在下面的代码中,条件表达式是一个多表达式,并且每个操作数都被认为是在一个赋值上下文中,目标Class<? super Integer> Class<? super Integer>

    Class<? super Integer> choose(boolean b,
                                  Class<Integer> c1,
                                  Class<Number> c2) {
        return b ? c1 : c2;
    }
    

    那么,它是一个引用条件表达式还是一个数字条件表达式?

    §15.25。 有条件的运营商? :说:

    有三种条件表达式,根据第二个和第三个操作数表达式分类:布尔条件表达式,数值条件表达式和引用条件表达式。 分类规则如下:

  • 如果第二个和第三个操作数表达式都是布尔表达式,那么条件表达式就是一个布尔条件表达式。
    ...
  • 如果第二个和第三个操作数表达式都是数字表达式,则条件表达式是一个数字条件表达式。
    为了对条件进行分类,以下表达式是数字表达式:
  • 独立形式(第15.2节)的表达式,其类型可转换为数字类型(§4.2,§5.1.8)。
  • 带括号的数字表达式(第15.8.5节)。
  • 可转换为数值类型的类的实例创建表达式(第15.9节)。
  • 方法调用表达式(第15.12节),其中所选的最具体方法(第15.12.2.5节)具有可转换为数字类型的返回类型。
  • 数字条件表达式。
  • 否则,条件表达式就是一个引用条件表达式。
  • 因此,根据这些规则,不排除泛型方法调用,所有显示的条件表达式都是数值条件表达式,并且应该起作用,因为只有“否则”它们才被认为是引用条件表达式。 我测试过的Eclipse版本在不报告任何错误的情况下编译了所有这些版本。

    这导致了奇怪的情况,对于any()情况,我们需要进行目标打字,以发现它具有数字返回类型,并推断条件是数字条件表达式,即独立表达式。 请注意,对于布尔条件表达式,有这样的评论:

    请注意,对于泛型方法,这是在实例化方法的类型参数之前的类型。

    但对于数字条件表达式,不存在这样的注释,无论是否有意。

    但正如所说的,无论如何,这只适用于any()示例,因为getData()方法不是通用的。


    这似乎是Oracle编译器的一个已知问题:错误ID:JDK-8162708

    引用:

    问题描述:
    如果您在泛型类中有一个方法声明如下:

    class Foo<T> {
      public T getValue() {
        // returns a value ...
      }
    }
    

    你可以在三元运算符里面调用上面的方法

    Foo<Integer> foo = new Foo<>();
    Float f = new Random().nextBoolean() ? foo.getValue() : 0f;
    

    你会从javac 1.8编译器中得到一个语法错误。

    但是上面的代码编译时没有javac 1.7和1.9的错误和警告。

    解决方法:未解决

    受影响的版本:8

    来自评论:

    这个问题只适用于8u,7和9没有问题


    不得不声称这不是一个答案,只是一个推理。 凭借我在编译器方面的短暂经验(不是Javac专用),它可能与解析代码的方式有关。

    在下面的反编译代码中,您会看到调用方法GenericData.getData:()Ljava/lang/Object或引用字段GenericData.data:Ljava/lang/Object ,它们都会首先通过Object返回值获取值/方法,随后由一个cast

      stack=4, locals=4, args_size=1
         0: new           #2                  // class general/GenericData
         3: dup
         4: dconst_1
         5: invokestatic  #3                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
         8: invokespecial #4                  // Method general/GenericData."<init>":(Ljava/lang/Object;)V
        11: astore_1
        12: aload_1
        13: ifnonnull     23
        16: dconst_0
        17: invokestatic  #3                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
        20: goto          30
        23: aload_1
        24: invokevirtual #5                  // Method general/GenericData.getData:()Ljava/lang/Object;
        27: checkcast     #6                  // class java/lang/Double
        30: astore_2
        31: aload_1
        32: ifnonnull     39
        35: dconst_0
        36: goto          49
        39: aload_1
        40: getfield      #7                  // Field general/GenericData.data:Ljava/lang/Object;
        43: checkcast     #6                  // class java/lang/Double
        46: invokevirtual #8                  // Method java/lang/Double.doubleValue:()D
        49: invokestatic  #3                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
        52: astore_3
        53: return
    

    如果将三元运算符表达式与等价的if-else进行比较:

    Integer v = 10;
    v = v != null ? 1 : 0;
    
         0: bipush        10
         2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: astore_1
         6: aload_1
         7: ifnull        14
        10: iconst_1
        11: goto          15
        14: iconst_0
        15: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        18: astore_1
        19: return
    
    Integer v = 10;
    if (v != null)
        v = 1;
    else
        v = 0;
    
         0: bipush        10
         2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: astore_1
         6: aload_1
         7: ifnull        18
        10: iconst_1
        11: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        14: astore_1
        15: goto          23
        18: iconst_0
        19: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        22: astore_1
        23: return
    

    两个版本没有显着差异。 所以我不认为所有黑魔法都有隐秘的秘密。 这是编译器如何解析整个表达式的结果,并且基于上下文来找出使所有组件同样满意的类型。 例如,

    Double val = 0; // compilation error: context is clear, 0 is an integer, so Integer.valueOf(i), but don't match expected type - Double
    val = 0 + g.getData(); // OK, enough context to figure out the type should be Double
    

    尽管如此,混淆的原因在于为什么泛型字段有效,而不是泛型方法......

    val = val == null ? 0 : g.data; // OK
    val = val == null ? 0 : g.getData(); // Compilation error
    

    编辑:Holger引用的文档似乎是一个很好的说明。

    链接地址: http://www.djcxy.com/p/73659.html

    上一篇: Java 8 autoboxing + generics: different behaviour with variable vs. method

    下一篇: Different Data Types Operations double