9

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 in StrLastError is overriding setError() in the LastError class. However, it is not the case. At the time of compilation, the knowledge of type S is not available. Therefore, the compiler records the signatures of these two methods as setError(String) in superclass and setError(S_extends_CharSequence) in subclass—treating them as overloaded methods (not overridden). In this case, when the call to setError() 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:

  1. 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?
  2. 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?

dimo414
  • 47,227
  • 18
  • 148
  • 244
George Chou
  • 187
  • 10

3 Answers3

5

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)

muued
  • 1,666
  • 13
  • 25
2

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.

Community
  • 1
  • 1
Makoto
  • 104,088
  • 27
  • 192
  • 230
  • Thanks, I think I've missed a in my 2nd point, will revise the post, but I still don't get why when is explicitly used in the declaration in the main, S can't be inferred at compile time. – George Chou Apr 25 '15 at 22:37
  • The generic type bound to the child isn't the same as the type bound to the parent. Heck, if you had a type parameter on a generic method, those two types wouldn't be the same, either. If you're curious about that sort of behavior (it slightly goes out of scope here), feel encouraged to ask a new question. – Makoto Apr 25 '15 at 22:39
  • So the compiler first compiled class "LastError", then "StrLastError", and in the main's body, when the constructor was invoked, LastError was explicitly told that T should be String from the declaration of the subtype, and leaves the subclass Type parameter S undetermined? – George Chou Apr 25 '15 at 22:59
  • No. `S` is bound to `CharSequence` as an upper bound by your constraint. This is the primary reason that Java is getting it mixed up; a `String` is a `CharSequence`, and so both the superclass and subclass methods are valid to use when passing a `String` through. – Makoto Apr 25 '15 at 23:36
  • "At the time of compilation, the knowledge of type S is not available. " This is what confuses me the most, so suppose there's no superclass, and class StrLastError exists alone without changing anything else, e.g. its upper bound, main still the same, S still remains undetermined? Thank you very much, I think I'm getting there. – George Chou Apr 26 '15 at 00:10
-2

This is a tricky example, to fix it you need to change the following line:

class StrLastError<S extends CharSequence> extends LastError<S>
Crazyjavahacking
  • 9,343
  • 2
  • 31
  • 40
  • 2
    I think you are right about the fix, but the example was said to be intentionally wrong and the OP wants to understand why its wrong. – paisanco Apr 25 '15 at 14:34