下面的代码片段告诉我错误,如标题中所示,我没有弄清楚为什么它不起作用,因为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'
}
}
如果有人能提供一些线索,那将不胜感激,谢谢!
答案 0 :(得分:4)
这种泛型算法的实现存在一个基本问题。问题不在于你在数学上如何说这应该起作用的推理,而在于它如何被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)如果你考虑它,那完全有意义,因为毕竟,整数运算和浮点运算完全不同。不同类型也有不同的大小等(例如,参见ladd)。另外请考虑BigInteger
和BigDecimal
扩展Number
,但那些不支持自动装箱,因此没有操作码直接处理它们。可能还有许多其他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 MathOperationV1 {
public Integer add(Integer a, Integer b) {
return a + b;
}
}
鉴于这种类型的擦除,似乎使用Integer
的子类应该在这里工作,但这不是真的,因为它会使类型系统不健全。让我试着证明这一点。
编译器不仅可以担心声明站点,还必须考虑多个调用站点中发生的情况,可能使用{{1}的不同类型参数}}。
例如,想象(为了我的论点)有一个T
的子类,我们称之为Integer
。并假设我们的代码编译得很好(这实际上是你的问题:它为什么不编译?)。
如果我们做了以下事情会怎么样?
SmallInt
正如您所看到的,MathOperationV1<SmallInt> op = new MathOperationV1<>();
SmallInt res = op.add(SmallInt.of(1), SmallInt.of(2));
方法的结果应该是op.add()
,而不是SmallInt
。但是,我们上面的Integer
的结果,从我们擦除的类定义,总是会返回a + b
而不是Integer
(因为+使用JVM整数算术操作码),因此结果会不健全,对吗?
您现在可能想知道,但如果SmallInt
的类型擦除总是返回MathOperationV1
,那么在调用网站的世界中,它可能会期望其他东西(如Integer
) ?
嗯,编译器通过将SmallInt
的结果转换为add
来增加一些额外的魔力,但这只是因为它已经确保操作不能返回除了SmallInt
以外的任何其他内容。期望的类型(这就是你看到编译器错误的原因)。
换句话说,您的通话网站在删除后会如下所示:
MathOperationV1 op = new MathOperationV1<>(); //with Integer type erasure
SmallInt res = (SmallInt) op.add(SmallInt.of(1), SmallInt.of(2));
但是,只有在add
始终返回SmallInt
(我们原来的答案中描述的操作员问题无法解决)时,这才有效。
因此,正如您所看到的,您的类型擦除只是确保根据子类型规则,您可以返回任何扩展Integer
的内容,但是一旦您的调用站点为{{1}声明了类型参数在原始代码中出现T
的地方,你应该总是假设相同的类型,以保持类型系统的声音。
您可以使用Java反编译器(JDK bin目录中名为javap的工具)来实际证明这些要点。如果你认为你需要它们,我可以提供更好的例子,但你最好自己尝试一下,看看幕后发生了什么: - )
答案 1 :(得分:1)
自动(联合)装箱仅适用于可以转换为其原始等效项的类型。仅为数字基元类型和String定义添加。即:int,long,short,char,double,float,byte。 Number没有原始的等价物,因此无法取消装箱,这就是你无法添加它们的原因。
答案 2 :(得分:1)
+
未定义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
在标准类之上添加任何内容(实际上,它提供的内容更少)。