运算符“+”不能应用于 'T','T' 有界泛型
Operator '+' cannot be applied to 'T', 'T' for bounded generic type
下面的代码片段向我抛出了 header 中所示的错误,我没有弄清楚为什么它不起作用,因为 T 是 Number 类型,我希望运算符 '+' 没问题.
class MathOperationV1<T extends Number> {
public T add(T a, T b) {
return a + b; // error: Operator '+' cannot be applied to 'T', 'T'
}
}
如果有人能提供一些线索,我们将不胜感激!
自动(取消)装箱仅适用于可以转换为原始等价物的类型。 Addition 仅针对数字原始类型和 String 定义。即:int、long、short、char、double、float、byte。 Number 没有原始等价物,因此无法拆箱,这就是您无法添加它们的原因。
+
没有为 Number
定义。你可以通过写(没有泛型)来看到这一点:
Number a = 1;
Number b = 2;
System.out.println(a + b);
这根本无法编译。
您不能直接进行一般加法:您需要一个 BiFunction
、一个 BinaryOperator
或类似的,它能够将操作应用于输入:
class MathOperationV1<T extends Number> {
private final BinaryOperator<T> combiner;
// Initialize combiner in constructor.
public T add(T a, T b) {
return combiner.apply(a, b);
}
}
但是话又说回来,您也可以直接使用 BinaryOperator<T>
:MathOperationV1
没有在标准 class 之上添加任何东西(实际上,它提供的更少)。
泛型算术思想的实现存在一个根本问题。问题不在于您从数学上讲应该如何工作的推理,而在于 Java 编译器应如何将其编译为字节码的含义。
在你的例子中你有这个:
class MathOperationV1<T extends Number> {
public T add(T a, T b) {
return a + b; // error: Operator '+' cannot be applied to 'T', 'T'
}
}
撇开装箱和拆箱不谈,问题是编译器不知道应该如何编译您的 +
运算符。编译器应该使用 +
的多个重载版本中的哪个? JVM 对不同的原始类型有不同的算术运算符(即操作码);因此,整数的求和运算符是与双精度运算符完全不同的操作码(例如,参见 iadd vs dadd) If you think about it, that totally makes sense, because, after all, integer arithmetic and floating-point arithmetic are totally different. Also different types have different sizes, etc (see, for example ladd)。还要考虑扩展 Number
的 BigInteger
和 BigDecimal
,但是它们不支持自动装箱,因此没有直接处理它们的操作码。可能还有许多其他 Number
实现,就像其他库中的实现一样。编译器怎么知道如何处理它们?
因此,当编译器推断出 T
是一个 Number
时,这不足以确定哪些操作码对操作(即装箱、拆箱和算术)有效。
稍后您建议将代码稍微更改为:
class MathOperationV1<T extends Integer> {
public T add(T a, T b) {
return a + b;
}
}
现在 +
运算符可以用整数求和操作码来实现,但是求和的结果将是 Integer
,而不是 T
,这仍然会使此代码无效,因为从编译器的角度来看 T
可以是除 Integer
.
之外的其他内容
我相信没有办法让您的代码足够通用,以至于您可以忘记这些底层实现细节。
--编辑--
要在评论部分回答您的问题,请考虑以下基于上面 MathOperationV1<T extends Integer>
的最后定义的场景。
当你说编译器将对 class 定义进行类型擦除时,你是正确的,它将被编译为
class MathOperationV1 {
public Integer add(Integer a, Integer b) {
return a + b;
}
}
考虑到这种类型擦除,似乎使用 Integer
的子 class 应该可以在这里工作,但事实并非如此,因为它会使类型系统不健全。让我试着证明这一点。
编译器不仅要担心声明站点,它还必须考虑多个调用站点中发生的情况,可能使用T
的不同类型参数。
例如,假设(为了我的论点)有一个 Integer
的子 class,我们称之为 SmallInt
。并假设我们上面的代码编译正常(这实际上是你的问题:为什么它不编译?)。
如果我们执行以下操作会发生什么?
MathOperationV1<SmallInt> op = new MathOperationV1<>();
SmallInt res = op.add(SmallInt.of(1), SmallInt.of(2));
正如您所见,op.add()
方法的结果预计是 SmallInt
,而不是 Integer
。但是,根据我们删除的 class 定义,我们上面 a + b
的结果总是 return 一个 Integer
而不是 SmallInt
(因为 + 使用 JVM整数算术操作码),因此这个结果是不合理的,对吧?
你现在可能想知道,但是如果 MathOperationV1
的类型擦除总是 return 是 Integer
,那么在调用站点的世界中它可能会期待其他东西(比如SmallInt
) 无论如何?
好吧,编译器通过将 add
的结果转换为 SmallInt
在这里添加了一些额外的魔法,但这只是因为它已经确保操作不能 return预期类型以外的任何其他类型(这就是您看到编译器错误的原因)。
换句话说,您的调用站点在擦除后将如下所示:
MathOperationV1 op = new MathOperationV1<>(); //with Integer type erasure
SmallInt res = (SmallInt) op.add(SmallInt.of(1), SmallInt.of(2));
但这只有在您可以确保 add
return 始终是 SmallInt
时才有效(由于我的原始答案中描述的操作员问题,我们不能这样做)。
因此,如您所见,您的类型擦除只是确保,根据子类型规则,您可以 return 任何扩展 Integer
的内容,但是一旦您的调用站点声明了T
的类型参数,你应该总是假定原始代码中出现 T
的相同类型,以保持类型系统的健全。
您实际上可以使用 Java 反编译器(JDK bin 目录中名为 javap 的工具)证明这些观点。如果你认为你需要它们,我可以提供更好的例子,但你最好自己尝试一下,看看幕后发生了什么:-)
下面的代码片段向我抛出了 header 中所示的错误,我没有弄清楚为什么它不起作用,因为 T 是 Number 类型,我希望运算符 '+' 没问题.
class MathOperationV1<T extends Number> {
public T add(T a, T b) {
return a + b; // error: Operator '+' cannot be applied to 'T', 'T'
}
}
如果有人能提供一些线索,我们将不胜感激!
自动(取消)装箱仅适用于可以转换为原始等价物的类型。 Addition 仅针对数字原始类型和 String 定义。即:int、long、short、char、double、float、byte。 Number 没有原始等价物,因此无法拆箱,这就是您无法添加它们的原因。
+
没有为 Number
定义。你可以通过写(没有泛型)来看到这一点:
Number a = 1;
Number b = 2;
System.out.println(a + b);
这根本无法编译。
您不能直接进行一般加法:您需要一个 BiFunction
、一个 BinaryOperator
或类似的,它能够将操作应用于输入:
class MathOperationV1<T extends Number> {
private final BinaryOperator<T> combiner;
// Initialize combiner in constructor.
public T add(T a, T b) {
return combiner.apply(a, b);
}
}
但是话又说回来,您也可以直接使用 BinaryOperator<T>
:MathOperationV1
没有在标准 class 之上添加任何东西(实际上,它提供的更少)。
泛型算术思想的实现存在一个根本问题。问题不在于您从数学上讲应该如何工作的推理,而在于 Java 编译器应如何将其编译为字节码的含义。
在你的例子中你有这个:
class MathOperationV1<T extends Number> {
public T add(T a, T b) {
return a + b; // error: Operator '+' cannot be applied to 'T', 'T'
}
}
撇开装箱和拆箱不谈,问题是编译器不知道应该如何编译您的 +
运算符。编译器应该使用 +
的多个重载版本中的哪个? JVM 对不同的原始类型有不同的算术运算符(即操作码);因此,整数的求和运算符是与双精度运算符完全不同的操作码(例如,参见 iadd vs dadd) If you think about it, that totally makes sense, because, after all, integer arithmetic and floating-point arithmetic are totally different. Also different types have different sizes, etc (see, for example ladd)。还要考虑扩展 Number
的 BigInteger
和 BigDecimal
,但是它们不支持自动装箱,因此没有直接处理它们的操作码。可能还有许多其他 Number
实现,就像其他库中的实现一样。编译器怎么知道如何处理它们?
因此,当编译器推断出 T
是一个 Number
时,这不足以确定哪些操作码对操作(即装箱、拆箱和算术)有效。
稍后您建议将代码稍微更改为:
class MathOperationV1<T extends Integer> {
public T add(T a, T b) {
return a + b;
}
}
现在 +
运算符可以用整数求和操作码来实现,但是求和的结果将是 Integer
,而不是 T
,这仍然会使此代码无效,因为从编译器的角度来看 T
可以是除 Integer
.
我相信没有办法让您的代码足够通用,以至于您可以忘记这些底层实现细节。
--编辑--
要在评论部分回答您的问题,请考虑以下基于上面 MathOperationV1<T extends Integer>
的最后定义的场景。
当你说编译器将对 class 定义进行类型擦除时,你是正确的,它将被编译为
class MathOperationV1 {
public Integer add(Integer a, Integer b) {
return a + b;
}
}
考虑到这种类型擦除,似乎使用 Integer
的子 class 应该可以在这里工作,但事实并非如此,因为它会使类型系统不健全。让我试着证明这一点。
编译器不仅要担心声明站点,它还必须考虑多个调用站点中发生的情况,可能使用T
的不同类型参数。
例如,假设(为了我的论点)有一个 Integer
的子 class,我们称之为 SmallInt
。并假设我们上面的代码编译正常(这实际上是你的问题:为什么它不编译?)。
如果我们执行以下操作会发生什么?
MathOperationV1<SmallInt> op = new MathOperationV1<>();
SmallInt res = op.add(SmallInt.of(1), SmallInt.of(2));
正如您所见,op.add()
方法的结果预计是 SmallInt
,而不是 Integer
。但是,根据我们删除的 class 定义,我们上面 a + b
的结果总是 return 一个 Integer
而不是 SmallInt
(因为 + 使用 JVM整数算术操作码),因此这个结果是不合理的,对吧?
你现在可能想知道,但是如果 MathOperationV1
的类型擦除总是 return 是 Integer
,那么在调用站点的世界中它可能会期待其他东西(比如SmallInt
) 无论如何?
好吧,编译器通过将 add
的结果转换为 SmallInt
在这里添加了一些额外的魔法,但这只是因为它已经确保操作不能 return预期类型以外的任何其他类型(这就是您看到编译器错误的原因)。
换句话说,您的调用站点在擦除后将如下所示:
MathOperationV1 op = new MathOperationV1<>(); //with Integer type erasure
SmallInt res = (SmallInt) op.add(SmallInt.of(1), SmallInt.of(2));
但这只有在您可以确保 add
return 始终是 SmallInt
时才有效(由于我的原始答案中描述的操作员问题,我们不能这样做)。
因此,如您所见,您的类型擦除只是确保,根据子类型规则,您可以 return 任何扩展 Integer
的内容,但是一旦您的调用站点声明了T
的类型参数,你应该总是假定原始代码中出现 T
的相同类型,以保持类型系统的健全。
您实际上可以使用 Java 反编译器(JDK bin 目录中名为 javap 的工具)证明这些观点。如果你认为你需要它们,我可以提供更好的例子,但你最好自己尝试一下,看看幕后发生了什么:-)