Java 8 自动装箱 + 泛型:变量与方法的不同行为

2022-09-03 01:46:10

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

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

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

您可能猜到 的构造函数有一个该泛型类型的参数,并且该方法仅返回该泛型类型。(有关完整的源代码,请参见下文。GenericDatagetData()

现在困扰我的是,在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失败。

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

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

所以我在这里有两个问题是:

  1. 为什么 Java 8 不推断出像 Java 7 这样的类型,并在自动装箱双倍之前将我的 int 转换为双倍?
  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;
    }
}

要对其进行测试,请按如下方式运行编译器:

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


答案 1

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

请考虑以下示例:

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

这可以毫无问题地编译

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

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

这是第一个不一致之处。虽然您的方法引用了 的类型参数,但它不是泛型方法(不涉及/不应该涉及类型推断来确定此处。getData()TGenericDataTDouble

JLS §8.4.4.泛型方法

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

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

JLS §15.12.方法调用表达式

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

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

由于此方法调用不是 poly 表达式,因此它的行为应类似于调用的示例,而不是 .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.条件运算符 ?: 说道:

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

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

因此,根据这些规则,在不排除泛型方法调用的情况下,所有显示的条件表达式都是数字条件表达式并且应该有效,因为只有“否则”它们才被视为引用条件表达式。我测试过的Eclipse版本编译了所有这些版本,没有报告任何错误。

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

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

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

但如前所述,这仅适用于示例,因为该方法不是通用的。any()getData()


答案 2

这似乎是 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 中没有问题


推荐