运算符“+”不能应用于 '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)。还要考虑扩展 NumberBigIntegerBigDecimal,但是它们不支持自动装箱,因此没有直接处理它们的操作码。可能还有许多其他 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 的工具)证明这些观点。如果你认为你需要它们,我可以提供更好的例子,但你最好自己尝试一下,看看幕后发生了什么:-)