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

Quo*_*ota 17 java generics autoboxing java-8

我找到了一段代码,从Java 7切换到Java 8后停止编译.它没有任何新的Java 8内容,如lambda或流.

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

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

您可能猜测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
Run Code Online (Sandbox Code Playgroud)

似乎Java 7能够从int - > double - > Double进行转换,但Java 8因尝试立即从int - > Double而失败.

我觉得特别有趣的是Java 8 确实接受代码时,我改变它getData()data,即访问GenericData通过变量本身而不是消气方法的价值:

Double d2 = g == null ? 0 : g.data; // now why does this work...
Run Code Online (Sandbox Code Playgroud)

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

  1. 为什么Java 8不会推断像Java 7这样的类型,并且在自动装箱double到Double之前将我的int转换为double?
  2. 为什么这个问题只发生在泛型方法而不是泛型变量?

完整源代码:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

最后如果重要:我运行Windows 8和Java 1.8.0_112(64位).

Hol*_*ger 14

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

请考虑以下示例:

static Double aDouble() {
    return 0D;
}
…
Double d = g == null ? 0 : aDouble();
Run Code Online (Sandbox Code Playgroud)

这编译没有任何问题

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

这里,调用any()是一个Poly Expression,编译器必须推断T := Double.这再现了相同的错误.

这是第一个不一致.虽然你的方法getData()是指类型参数TGenericData,它不是一个通用的方法(有/应参与确定没有类型推断TDouble在这里.

JLS§8.4.4.通用方法

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

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

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

如果满足以下所有条件,则方法调用表达式是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;
}
Run Code Online (Sandbox Code Playgroud)

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

§15.25.条件运算符?:说:

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

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

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

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

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

但是对于数字条件表达式,没有这样的音符,无论是有意还是无意.

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

  • 请注意,但我已经两次阅读了你的答案,仍然没有明显的线索,为什么这仍然失败了jdk-8编译器.底线这是一个jdk-8编译器bug吗? (2认同)
  • @Holger发现[错误ID:JDK-8162708](http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8162708) - 似乎与您的观察相符 (2认同)

Hul*_*ulk 5

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

引用:

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

class Foo<T> {
  public T getValue() {
    // returns a value ...
  }
}
Run Code Online (Sandbox Code Playgroud)

你可以在三元运算符中调用上面的方法,如下所示

Foo<Integer> foo = new Foo<>();
Float f = new Random().nextBoolean() ? foo.getValue() : 0f;
Run Code Online (Sandbox Code Playgroud)

你从javac 1.8编译器得到一个语法错误.

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

解决方案:尚未解决

受影响的版本:8

来自评论:

此问题仅适用于8u,7和9中没有问题


Kev*_*ang 1

必须声明这不是一个答案,而只是一个推理。根据我在编译器方面的简短经验(不是 Javac 特有的),它可能与代码的解析方式有关。

在下面的反编译代码中,您会看到调用方法GenericData.getData:()Ljava/lang/Object或引用 field 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
Run Code Online (Sandbox Code Playgroud)

如果将三元运算符表达式与等效的 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
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

尽管如此,令人困惑的是为什么通用字段有效但通用方法无效......

val = val == null ? 0 : g.data; // OK
val = val == null ? 0 : g.getData(); // Compilation error
Run Code Online (Sandbox Code Playgroud)

编辑:霍尔格引用的文件似乎是一个很好的澄清。