I'm reading about type inference for generics, and this code was provided as an example that fails to compile.
import java.io.*;
class LastError<T> {
private T lastError;
void setError(T t){
lastError = t;
System.out.println("LastError: setError");
}
}
class StrLastError<S extends CharSequence> extends LastError<String>{
public StrLastError(S s) {
}
void setError(S s){
System.out.println("StrLastError: setError");
}
}
class Test {
public static void main(String []args) {
StrLastError<String> err = new StrLastError<String>("Error");
err.setError("Last error");
}
}
And the explanation given in the book was:
"(It looks like the
setError()
method inStrLastError
is overridingsetError()
in theLastError
class. However, it is not the case. At the time of compilation, the knowledge of typeS
is not available. Therefore, the compiler records the signatures of these two methods assetError(String)
in superclass andsetError(S_extends_CharSequence)
in subclass—treating them as overloaded methods (not overridden). In this case, when the call tosetError()
is found, the compiler finds both the overloaded methods matching, resulting in the ambiguous method call error."
I really don't understand why type S
can't be inferred at compile time.
String
is passed when invoking the constructor of class StrLastError
,
and from the API docs, String
does implement interface CharSequence
,
so doesn't that mean that S
for <S extends CharSequence>
actually is of type String
?
I've read the Java online tutorial on the topic of generics several times. I've checked "Type Inference", and Inheritance, I just don't know how the whole thing works. I really need an explanation on this question.
The points I'm stuck at are:
S
, how come the super type can decide T
, because the superclass does not have an upper bound? Or does it infer that T
is String
because the subtype calls the supertype's constructor first?I understand that if the Constructor is invoked as:
StrLastError<CharSequence> err = newStrLastError<>((CharSequence)"Error");
there will be no ambiguity, since it's plain method overriding then. (Or am I even wrong here?)
However, like I said in the beginning, if String
is passed, why can't S
be inferred to be String
?
You have to remind yourself that the classes are compiled one by one. Java generics are not templates as in other languages. There will only be one compiled class and not one class per type it is used with.
This way you can see that the class StrLastError
will need to be compiled in a way such that it can also be used with other classes implementing CharSequence
as generic type S.
Thats why the compiler gets you two different methods instead of an overridden one. Now it would be a runtime-job to see that the subclass may have wanted to override the method in the parent just in those cases where the types suggest it. Since this behaviour is hard to understand for the developer and would possibly lead to programming mistakes, it raises an exception.
If you use CharSequence as the generic type parameter of the class StrLastError
, you will call the setError
method in the parent class, since the type of "Last Error"
is String
, which is more specific than CharSequence
and Java always chooses the most specific method in case it is overloaded. (I hope it is clear the the method was not overridden in this case either)
If the subtype can't decide S, how come the super type can decide T, because the superclass does not have an upper bound? Or does it infer that T is String because the subtype calls the supertype's constructor first?
The super type isn't deciding or inferring what T
is; you're explicitly telling it what T
is by this declaration:
class StrLastError<S extends CharSequence> extends LastError<String>
T
is now bound to String
for LastError
, which makes every reference to T
in the parent class a concrete String
.
Your child class now has a bound S extends CharSequence
attached to it, but this is independent to the bounds applied to the parent class.
What happens now is that Java will compile your child class, and the result of your child class is that two methods with a signature that matches String
will be created. (Key note here: A String
is-a CharSequence
.)
In your child class, setError(Ljava/lang/CharSequence;)V
is generated as the signature for setError
. Because of the way generics work, LastError#setError
will be treated as if it has a signature of setError(Ljava/lang/String;)V
. This is also why when you go to actually override the method, it will place a String
type as your parameter instead of anything else.
So, what we arrive at are two methods that have override-equivalent signatures.
void setError(CharSequence s)
void setError(String s)
JLS 8.4.8.4. applies here.
It is possible for a class to inherit multiple methods with override-equivalent signatures (§8.4.2).
It is a compile-time error if a class C inherits a concrete method whose signature is a subsignature of another concrete method inherited by C. This can happen if a superclass is generic, and it has two methods that were distinct in the generic declaration, but have the same signature in the particular invocation used.
I understand that if the Constructor is invoked as StrLastError err = new StrLastError<>((CharSequence)"Error"); there will be no ambiguity, since its plain method overriding then.(Or I'm even wrong here)
No, now you're messing with raw types. Interestingly enough it will work, primarily because the signatures for the two methods has become:
void setError(Object s)
void setError(String s)
You want to use generics to avoid a scenario like this; you may want to invoke the super class method at some point, but in this scenario with these bindings, it's very difficult to accomplish.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With