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

时间:2017-07-17 20:53:42

标签: 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' 
        }
    }

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

3 个答案:

答案 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)。另外请考虑BigIntegerBigDecimal扩展Number,但那些不支持自动装箱,因此没有操作码直接处理它们。可能还有许多其他Number实现,就像其他库中的实现一样。编译器怎么知道如何处理它们?。

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

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

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);

这根本就不会编译。

您不能直接添加额外内容:您需要BiFunctionBinaryOperator或类似内容,以便将操作应用于输入:

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在标准类之上添加任何内容(实际上,它提供的内容更少)。