Search code examples
javagenericsincompatibletypeerror

Incompatible types when using recursive generics in Java


I have written java code where I use a recursive form of generics to achieve a clean way of making a Builder pattern inheritable.

This works but I do not understand some of the warnings and errors I get from the java compiler.

This is the severely simplified version of the part I don't understand:

package nl.basjes.test;

public class Foo<X extends Foo<X>> {
  public X doSomething() {
    return this;
  }
}

For the "return this;" I get the error

Incompatible Types
Required: X
Found   : nl.basjes.test.Foo <X>

Now 'this' is always a subclass of Foo (or even Foo itself) and 'X' is defined as X extends Foo<X>. To my knowledge these should be "the same" but apparently they are not.

So in my code I added a cast to the return statement like this:

package nl.basjes.test;

public class Foo<X extends Foo<X>> {
  public X doSomething() {
    return (X)this;
  }
}

which makes the code compile and work as expected and intended.

I do however still get a warning about "Unchecked cast" for the same reason as above (but now it is just a warning).

$ javac -Xlint:unchecked nl/basjes/test/Foo.java 
nl/basjes/test/Foo.java:5: warning: [unchecked] unchecked cast
        return (X)this;
                  ^
  required: X
  found:    Foo<X>
  where X is a type-variable:
    X extends Foo<X> declared in class Foo
1 warning

Why doesn't Java see that X (which extends Foo<X>) and this (which extends Foo<X>) are compatible?

At this point my best guess is that this has to do with a part of the type erasure that I do not understand yet.


Solution

  • When you consider the concrete type arguments, it becomes easier to see the problem:

    Suppose

    Foo<Bar> barFoo = ...;
    

    When you call barFoo.doSomething(), you expect to get a Bar object:

    Bar bar = barFoo.doSomething()
    

    However, your actual implementation:

    public X doSomething() {
      return this;
    }
    

    Can roughly be filled with the following concrete parameters:

    public Bar doSomething() {
      return this; //But "this" is a Foo<Bar>, not a Bar.
    }
    

    Here's a different example to make it even more obvious:

    class Bar extends Foo<Bar> {
    }
    class Baz extends Foo<Bar> { //note this is a Foo<Bar>
    }
    

    And:

    Baz baz = new Baz();
    Bar bar = baz.doSomething();
    

    In the above, you expect baz.doSomething() to return a Bar, but the code in doSomething() is returning a Baz, but casting it to Bar, which has a type safety problem (in fact, these types are incompatible, but you only get a classcastexception when you have different classes as in the last example).