Operator '+' cannot be applied to 'T', 'T' for bounded generic type [duplicate]
Asked Answered
H

3

11

The following code snippet throw me the error as shown in the header, I didn't figure out why it does not work as T is of type Number, I expected operator '+' to be fine.

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

Would be appreciate if anyone can provide some clues, thx !

Howrah answered 17/7, 2017 at 20:53 Comment(3)
Please add the relevant language tag to your question.Garniture
This is Java, isn't it?Carnassial
I just updated the title, thx for reminding!Howrah
C
13

There is a fundamental problem with the implementation of this idea of generic arithmetic. The problem is not in your reasoning of how, mathematically speaking, this ought to work, but in the implications of how it should be compiled to bytecodes by the Java compiler.

In your example you have this:

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

Leaving boxing and unboxing aside, the problem is that the compiler does not know how it should compile your + operator. Which of the multiple overloaded versions of + should the compiler use? The JVM has different arithmetic operators (i.e. opcodes) for different primitive types; hence the sum operator for integers is an entirely different opcode than the one for doubles (see, for example, 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). Also think about BigInteger and BigDecimal which extend Number as well, but those do not have support for autoboxing and therefore there is no opcode to deal with them directly. There are probably dozens of other Number implementations like those in other libraries out there. How could the compiler know how to deal with them?.

So, when the compiler infers that T is a Number, that is not sufficient to determine which opcodes are valid for the operation (i.e. boxing, unboxing and arithmetic).

Later you suggest to change the code a bit to:

class MathOperationV1<T extends Integer> {
        public T add(T a, T b) {
            return a + b;
        }
    }

And now the + operator can be implemented with an integer sum opcode, but the result of the sum would be an Integer, not a T, which would still make this code invalid, since from the compiler standpoint T can be something else other than Integer.

I believe there is no way to make your code generic enough that you can forget about these underlying implementation details.

--Edit--

To answer your question in the comment section consider the following scenario based on the last definition of MathOperationV1<T extends Integer> above.

You're correct when you say that the compiler will do type erasure on the class definition, and it will be compiled as if it was

class MathOperationV1 {
        public Integer add(Integer a, Integer b) {
            return a + b; 
        }
}

Given this type erasure it would seem as if using a subclass of Integer ought to work here, but that's not true since it would make the type system unsound. Let me try to demonstrate that.

The compiler cannot only worry for the declaration site, it also has to consider what happens in the multiple call sites, possibly using a different type argument for T.

For example, imagine (for the sake of my argument) that there is a subclass of Integer that we'll call SmallInt. And assume our code above compiled fine (this is actually you question: why it doesn't compile?).

What would happen then if we did the following?

MathOperationV1<SmallInt> op = new MathOperationV1<>();
SmallInt res = op.add(SmallInt.of(1), SmallInt.of(2));

And as you can see the result of the op.add() method is expected to be a SmallInt, not an Integer. However, the result of our a + b above, from our erased class definition, would always return an Integer not a SmallInt (because the + uses the JVM integer arithmetic opcodes), and therefore this result would be unsound, right?.

You may now wonder, but if the type erasure of MathOperationV1 always returns an Integer, how in the world in the call site it might expect something else (like SmallInt) anyways?

Well, the compiler adds some extra magic here by casting the result of add to a SmallInt, but only because it has already ensured that the operation can't return anything else other than the expected type (this is why you see a compiler error).

In other words, your call site would look like this after erasure:

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

But that would only work if you could ensure that add returns always a SmallInt (which we cannot due to the operator problems described in my original answer).

So, as you can see, your type erasure just ensures that, as per the rules of subtyping, you can return anything that extends an Integer, but once your call site declares a type argument for T, you're supposed to always assume that same type wherever T appeared in the original code in order to keep the type system sound.

You can actually prove these points by using the Java decompiler (a tool in your JDK bin directory called javap). I could provide finer examples if you think you need them, but you would do well to try it yourself and see what's happening under the hood :-)

Carpometacarpus answered 18/7, 2017 at 0:12 Comment(3)
For the second code snippet, from what I understand, compiler would do type erasure here, which replace type parameter T with bounded type, which is Integer in my case, and it becomes class MathOperationV1 { public Integer add(Integer a, Integer b) { return a + b; } } What do I misunderstand here?Howrah
@PhoebeLi This is hard to answer in a short comment, so I enriched my answer with further details. I hope I had been able to explain myself, but otherwise, feel free to ask more questions and I will do my best to clarify any arguments.Carpometacarpus
Many many thanks here! I wish I could give you 10 more upvotes! :) It's much clearer to me, I think I'll go do some experiments with the decompiler as you suggested as next step :-)Howrah
D
1

Auto(un)boxing only works for types that can be converted to their primitive equivalents. Addition is only defined for numeric primitive types plus String. i.e: int, long, short, char, double, float, byte . Number does not have a primitive equivalent, so it can't be unboxed, that's why you can't add them.

Dulla answered 17/7, 2017 at 21:9 Comment(14)
According to your answer, I changed the code to be <T extends Integer>, but it's still not working, I didn't quite get it.Howrah
First, the numeric classes are final, so you won't be able to create any subclasses. Second, type erasure means that generic types are only checked at compile time, at runtime that information is lost. Hence the JVM won't be able to prevent you from passing an "Object" that cannot be unboxed to a primitive type. In other words, you cannot do what you want to do @see docs.oracle.com/javase/tutorial/java/generics/erasure.htmlDulla
At best you can create a MathOperation interface and then have classes implementing the interface for each numeric typeDulla
@Phoebe, your problem is in a + b because that yields an Integer not a T.Carpometacarpus
@edwin-dalorzo if that was the case the following would compile: private <T extends Integer> Integer add(T a, T b) { return a + b; } But it doesn't.Dulla
@Dulla He was responding to exactly that, and he's correct. a + b is valid, but return a + b; isn't, because the return type needs to be T, not Integer.Thievery
Side note: + with string operands is called concatenation, not addition.Thievery
@Dulla The type information is erased down to the upper bound. This code is compiled as though T was Integer or Number. Passing an Object would cause a ClassCastException if you could get it past the compiler.Sundin
@Dulla it compiles and runs fine for me.Carpometacarpus
@EdwinDalorzo You're right, it does compile, my bad. Funny enough, Intellij does give me a red squiggle under the operation with the error message "Operator + cannot be applied to 'T', 'T'Dulla
@EJP That's correct, I was referring to the fact that he was using 'Number' as the upper bound. I should have been more specific.Dulla
@EJP There's one more thing I'm unsure. If the type information is erased down to upper bound, the code( private <T extends Integer> T add(T a, T b) { return a + b; } ) would become` private <T extends Integer> Integer add(Integer a, Integer b) { return a + b; }` after compilation. But instead compiler thrown an error saying 'int cannot be converted to T'.Howrah
@Dulla It is indeed correct, and you were not referring to Number as an upper bound anywhere I can see, and your statement about passing an Object remains incorrect.Sundin
@PhoebeLi Well it can't. It can be converted to Integer, but not to T extends Integer.Sundin
D
1

+ isn't defined for Number. You can see this by writing (with no generics):

Number a = 1;
Number b = 2;
System.out.println(a + b);

This simply won't compile.

You can't do addition generically directly: you need a BiFunction, a BinaryOperator, or similar, which is able to apply the operation to the inputs:

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

But then again, you may as well just use the BinaryOperator<T> directly: MathOperationV1 adds nothing over and above that standard class (actually, it provides less).

Dogtooth answered 17/7, 2017 at 21:59 Comment(2)
Can you provide an implementation of your combiner lambda? I reckon you just move the problem without solving it.Carpometacarpus
Sure: (a, b) -> a + b. When you use this in context, e.g. BinaryOperator<Integer> plusInt = (a, b) -> a + b;, the Integer-ness is inferred.Dogtooth

© 2022 - 2024 — McMap. All rights reserved.