Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Generics of the form <T extends A<T>> and Inheritance

Tags:

java

generics

I have a tricky problem with Java generics, or maybe I can't see the wood for the trees.

I have three classes, A, B, and C that look as follows.

abstract class A<T extends A<T>> {
    abstract T sefl();
};

abstract class B<T extends B<T>> extends A<T> {
};

class C extends B<C> {
    @Override
    C sefl() {
        return this;
    }
}

Later I have different versions of B's as well as different versions of C's. Furthermore, I have a function test that should accept a list of B's (or of one of its cousins). Or in general, it should accept any list of elements that inherit from A. Unfortunately, I need to know the type of the list's elements in the function's body, i.e. the uppermost type a.self can return (or the type T). Function test looks as follows:

static <T extends A<T>> void test(List<T> list) {

    for (A<T> a : list) {
        @SuppressWarnings("unused")
        T t = a.sefl();
    }

}

Now, calling test with a list of C's works.

List<C> cs = new LinkedList<C>();
test(cs);

But

List<B> bs = new LinkedList<B>();
test(bs);

results in a warning requiring a type parameter and

List<B<?>> bs = new LinkedList<B<?>>();
test(bs);

is not valid. Where is my mistake, or how can I create a list of B's that is accepted by function test?


Some words to the motivation behind this problem. The classes A, B, and C (or Animal, Mammal, and Cat) implement a tree-like data structure, where each class extends the structure with some properties. Typically, all super-classes are abstract and you can only create instances from leaf-classes, e.g. cat. Now, the difficulty is that the classes implement a copy-on-write policy (COW), i.e. modifying an object creates and returns a new instance of itself with the modified property.

For example, let’s say all animals have an age property. You can easily define this property in Animal, and you can provide a getter method to return the age.

abstract class Animal<T extends Animal<T>> {
    private int age;
    public int getAge(int age) {
        return age;
    }
};

However, how do you define the setter method? You can write it as follows:

public abstract Animal setAge();

This requires that (at least) each non-abstract element must implement the setter function. For example:

class Cat extends Mammal<C> {

    @Override
    public Animal setAge(int age) {
        return new Cat(/* .. */);
    }
}

Remember, as we implement a COW policy, we must create a new instance. So, in the setter function (e.g. implement in Cat) we return a new cat with the new age. Calling cat.setAge(4) on a Cat element returns a new Cat. Unfortunately, because of the type signature, we only now that we got an Animal returned from setAge, even if we call it on a Cat directly. The twist with the generics helps to reveal the concrete type when calling setAge. So, we can construct Animal like this:

abstract class Animal<T extends Animal<T>> {
    private int age;
    public int getAge(int age) {
        return age;
    }
    public abstract T setAge();
};

And in Cat we can say:

class Cat extends Mammal<C> {

    @Override
    public Cat setAge(int age) {
        return new Cat(/* .. */);
    }
}

So, back to the problem. Your right, using List<? extends Animal<?>> as the type of the list works, but unfortunately, I need some way to know the type of the elements. Or more concrete: Function test must replace the old element with the new one. For example:

static void test2(List<? extends Animal<?>> list) {
    for (Animal<?> animal : list) {
        @SuppressWarnings("unused")
        Animal<?> a = animal.setAge(4711);
        list.add(a);
    }
}

And unfortunately, the list extension list.add(a); ist the statement that doesn't work with this signature.

like image 376
Matthias Keil Avatar asked Apr 24 '18 09:04

Matthias Keil


1 Answers

Well, they are two very different implementations:

class C ...

and

class B<T extends B<T>> ...

The class C doesn't declare any generic type.


Simple letters for class names are a bit confusing here, so let's do:

abstract class Animal<T extends Animal<T>> {
    abstract T sefl();
};

abstract class Mammal<T extends Mammal<T>> extends Animal<T> {
};

class Cat extends Mammal<Cat> {
    @Override
    Cat sefl() {
        return this;
    }
}

So:

List<Cat> catList = new LinkedList<>();

works well, as there is no generic type involved. The compiler determines that

Cat extends Mammal<Cat> ( == Cat extends Animal<Cat> )

fits within the bounds <T extends Animal<T>>

On the other hand for

List<Mammal> mammalList = new LinkedList<>();
test(mammalList); // ok, but mammal list of what???

the compiler can't match the bounded types.

In fact, Mammal<T extends Mammal<T>> extends Animal<T> doesn't have anything to do with <T extends Animal<T>>.

Even by providing a wildcard, you'll never be able to pass a List<Mammal<?> to test. The method signature rejects it!


A possible solution:

A more generic test method

static void test2(List<? extends Animal<?>> list) {
    for (Animal<?> animal : list) {
        Animal a = animal.sefl();
    }
}

can be used along with different List types:

List<? extends Mammal<?>> bs = new LinkedList<>();
test2(bs);

List<Cat> catList = new LinkedList<>();
test2(catList);

List<Animal<Cat>> animalList = new LinkedList<>();
test2(animalList);

Java version:

java 9.0.4
Java(TM) SE Runtime Environment (build 9.0.4+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode) 
like image 124
Marko Pacak Avatar answered Oct 12 '22 12:10

Marko Pacak