对于有界泛型类型,运算符'+'不能应用于'T','T'

Pho*_* Li 9 java generic-type-argument

下面的代码片段告诉我错误,如标题中所示,我没有弄清楚为什么它不起作用,因为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' 
        }
    }
Run Code Online (Sandbox Code Playgroud)

如果有人能提供一些线索,那将不胜感激,谢谢!

Edw*_*rzo 10

这种泛型算法的实现存在一个基本问题.问题不在于你在数学上如何说这应该起作用的推理,而在于它如何被Java编译器编译成字节码的含义.

在你的例子中你有这个:

class MathOperationV1<T extends Number> {
        public T add(T a, T b) {
            return a + b; // error: Operator '+' cannot be applied to 'T', 'T' 
        }
}
Run Code Online (Sandbox Code Playgroud)

抛开装箱和拆箱,问题是编译器不知道它应该如何编译你的+运算符.+编译器应该使用多个重载版本中的哪一个?JVM具有针对不同基元类型的不同算术运算符(即操作码); 因此整数的和运算符是一个完全不同的操作码而不是双精度的操作码(例如,参见iadd vs dadd)如果你考虑它,这完全有意义,因为毕竟整数算术和浮点运算是完全不同.不同类型也有不同的尺寸等(参见例如ladd).同时考虑BigIntegerBigDecimal扩展Number,但那些不支持自动装箱,因此没有操作码直接处理它们.可能有许多其他Number实现,如其他库中的实现.编译器怎么知道如何处理它们?

因此,当编译器推断T为a时Number,这不足以确定哪些操作码对操作有效(即装箱,拆箱和算术).

稍后您建议将代码更改为:

class MathOperationV1<T extends Integer> {
        public T add(T a, T b) {
            return a + b;
        }
    }
Run Code Online (Sandbox Code Playgroud)

现在,+运算符可以用整数和操作码实现,但总和的结果将是Integera T,而不是a ,这仍然会使这段代码无效,因为从编译器的角度来看,T除了Integer.

我相信没有办法使你的代码足够通用,你可以忘记这些底层的实现细节.

- 编辑 -

要在评论部分回答您的问题,请根据上述最后一个定义考虑以下方案MathOperationV1<T extends Integer>.

当你说编译器会对类定义进行类型擦除时,你是正确的,它将被编译为就像它是

class MathOperationV1 {
        public Integer add(Integer a, Integer b) {
            return a + b; 
        }
}
Run Code Online (Sandbox Code Playgroud)

鉴于这种类型的擦除,似乎使用了Integer应该在这里工作的子类,但这不是真的,因为它会使类型系统不健全.让我试着证明这一点.

编译器不仅可以担心声明站点,还必须考虑多个调用站点中发生的情况,可能使用不同的类型参数T.

例如,想象(为了我的论点)Integer,我们将调用一个子类SmallInt.并假设我们的代码编译得很好(这实际上是你的问题:它为什么不编译?).

如果我们做了以下事情会怎么样?

MathOperationV1<SmallInt> op = new MathOperationV1<>();
SmallInt res = op.add(SmallInt.of(1), SmallInt.of(2));
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,该op.add()方法的结果预计是一个SmallInt,而不是一个Integer.但是,我们a + b上面的结果,从我们擦除的类定义,总是返回一个Integernot a SmallInt(因为+使用JVM整数算术操作码),因此这个结果会不健全,对吗?

你现在可能想知道,但如果类型擦除MathOperationV1总是返回一个Integer,那么在调用网站的世界中它可能会期待别的东西(比如SmallInt)呢?

好吧,编译器通过将结果转换add为a来增加一些额外的魔力SmallInt,但这只是因为它已经确保操作不能返回除预期类型之外的任何其他内容(这就是为什么你看到编译器错误).

换句话说,您的调用网站在删除后将如下所示:

MathOperationV1 op = new MathOperationV1<>(); //with Integer type erasure
SmallInt res = (SmallInt) op.add(SmallInt.of(1), SmallInt.of(2));
Run Code Online (Sandbox Code Playgroud)

但是,只有当你能确保add返回总是一个SmallInt(我们不能因为我原来的答案中描述的操作符问题)时,这才有效.

所以,正如你所看到的,你的类型擦除只是确保,根据子类型的规则,你可以返回任何扩展的东西Integer,但是一旦你的调用站点声明了一个类型参数T,你应该总是假设相同的类型T原始代码中出现的任何地方,以保持类型系统的声音.

实际上,您可以使用Java反编译器(JDK bin目录中名为javap的工具)来证明这些要点.如果你认为你需要它们,我可以提供更好的例子,但你最好自己尝试一下,看看幕后发生了什么:-)