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

时间:2017-01-11 11:43:00

标签: java generics 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转换 - &gt;双 - &gt; Double,但Java 8因尝试立即从int而失败 - &gt;双

我觉得特别有趣的是,当我将代码从getData()更改为data时,Java 8 接受代码,即访问GenericData& #39;通过变量本身而不是getter-method的值:

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

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

  1. 为什么Java 8没有推断出类似Java 7的类型,并且在自动装箱double到Double之前将我的int转换为double?
  2. 为什么这个问题只发生在泛型方法而不是泛型变量?
  3. 完整的源代码:

    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位)。

4 个答案:

答案 0 :(得分:14)

方法调用表达式的特殊之处在于它们可能是 Poly Expressions ,受目标类型的限制。

请考虑以下示例:

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 Expression ,编译器必须推断T := Double。这再现了同样的错误。

这是第一个不一致的地方。虽然您的方法getData()引用了T的类型参数GenericData,但是一种通用方法(确定/不应该涉及类型推断)这里TDouble

JLS §8.4.4. Generic Methods
  

如果方法声明了一个或多个类型变量

,则该方法是泛型

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

JLS §15.12. Method Invocation Expressions
  

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

     
      
  • ...
  •   
  • 由以下小节确定的要调用的方法是通用的(第8.4.4节),并且具有一个返回类型,该类型提及至少一个方法的类型参数。
  •   

由于此方法调用不是多面体表达式,因此它的行为应与aDouble()调用的示例相同,而不是any()

但请注意§15.25.3

  

请注意,引用条件表达式不必包含多重表达式作为操作数,以便成为多重表达式。它只是凭借其出现的上下文而是一个多义表达式。例如,在以下代码中,条件表达式是poly表达式,并且每个操作数都被视为位于Class<? super Integer>的赋值上下文中:

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

那么,它是引用条件表达式还是数值条件表达式?

§15.25. Conditional Operator ? :说:

  

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

     
      
  • 如果第二个和第三个操作数表达式都是布尔表达式,则条件表达式是布尔条件表达式   ...
  •   
  • 如果第二个和第三个操作数表达式都是数值表达式,则条件表达式是数字条件表达式   为了对条件进行分类,以下表达式是数字表达式:      
        
    • 独立形式(第15.2节)的表达式,其类型可转换为数字类型(§4.2,§5.1.8)。
    •   
    • 带括号的数字表达式(第15.8.5节)。
    •   
    • 可转换为数字类型的类的类实例创建表达式(第15.9节)。
    •   
    • 方法调用表达式(第15.12节),其中所选择的最具体方法(第15.12.2.5节)具有可转换为数字类型的返回类型。
    •   
    • 数字条件表达式。
    •   
  •   
  • 否则,条件表达式是引用条件表达式。
  •   

因此,根据这些规则,不排除泛型方法调用,所有显示的条件表达式都是数值条件表达式,应该可以工作,因为只有“否则”它们被认为是引用条件表达。我测试过的Eclipse版本编译了所有这些版本而没有报告任何错误。

这导致奇怪的情况,对于any()情况,我们需要目标类型以找出它具有数字返回类型并推断条件是数字条件表达式,即一个独立表达式。请注意,对于布尔条件表达式,有一条注释:

  

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

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

但如上所述,这仅适用于any()示例,因为getData()方法不是泛型。

答案 1 :(得分:5)

这似乎是Oracle编译器的一个已知问题:Bug 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中没有问题

答案 2 :(得分:1)

不得不声称这不是一个答案,只是一个推理。 凭借我在编译器方面的简短经验(而不是特定于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
编辑:霍尔格引用的文件似乎是一个很好的澄清。

答案 3 :(得分:0)

  

CompileMe.java:4:错误:不兼容的类型:

中的错误类型      

条件表达式

      Double d = g == null ? 0 : g.getData(); // type error!!!
int cannot be converted to Double

这里0是整数,你把它放在Double里面。

试试这个

public class CompileMe {
    public static void main(String[] args) {
        GenericData<Double> g = new GenericData(1d);
        Double d = g == null ? 0d : g.getData(); // type error!!!
        Double d2 = g == null ? 0d : g.data; // now why does this work...
        System.out.println(d);
        System.out.println(d2);
    }
}

class GenericData<T> {
    public T data;
    public GenericData(T data) {
        this.data = data;
    }
    public T getData() {
       return data;
    }
}

或者使用double literal而不是Double wrapper class

    public class CompileMe {
    public static void main(String[] args) {
        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...
        System.out.println(d);
        System.out.println(d2);
    }
}

class GenericData<T> {
    public T data;
    public GenericData(T data) {
        this.data = data;
    }
    public T getData() {
        return data;
    }
}

因为加宽和装箱不会同时发生。