简体   繁体   中英

Java Generics of the form <T extends A<T>> and Eclipse Compiler

I have another interesting problem with Java Generics. I'm sure, some of you might have the right answer to it.

Context. First, to understand the context, let's look at the definition of the objects Animal, Mammal, and Cat, which are defined in terms of each other:

public abstract class Animal<T extends Animal<T>> {
    private final int age;

    public Animal(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    public abstract T setAge(int age);

    public static void main(String[] args) {
        List<Mammal<?>> mammals = new ArrayList<Mammal<?>>();
        mammals = Animal.increaseAge(mammals, 1);
        Map<Integer, List<Mammal<?>>> mammalsByAge = Animal.groupByAge(mammals);
    }

    public static <T extends Animal<T>> List<T> increaseAge(List<T> animals, int augment) {
        List<T> list = new LinkedList<T>();
        for (T animal : animals) {
            list.add(animal.setAge(animal.getAge() + augment));
        }
        return list;
    }

    public static <T extends Animal<T>> Map<Integer, List<T>> groupByAge(List<T> animals) {
        Map<Integer, List<T>> animalsPerAge = new TreeMap<Integer, List<T>>();
        animals.forEach((animal) -> {
            int age = animal.getAge();
            animalsPerAge.putIfAbsent(age, new LinkedList<T>());
            animalsPerAge.get(age).add(animal);
        });
        return animalsPerAge;
    }

};

abstract class Mammal<T extends Mammal<T>> extends Animal<Mammal<T>> {
    public Mammal(int age) {
        super(age);
    }
};

class Cat extends Mammal<Cat> {
    public Cat(int age) {
        super(age);
    }

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

Later, I will have different animals and mammals, obviously. Imported also, all elements must implement a 'copy-on-write' policy, ie, changing a property of an animal creates a new animal of the same type with the new property in place.

Finally, I need to implement two helper functions, groupByAge , and increaseAge .

Problem. In the beginning, I realized that the Eclipse IDE can compile and execute the program, whereas the Maven compiler failed with a compiler error.

[ERROR] Failed to execute goal
org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
(default-compile) on project BCBS248Calculator: Compilation failure:
Compilation failure:

[ERROR] Animal.java:[25,33] method increaseAge in class
com.ibm.ilm.bcbs248.Animal<Tcannot be applied to given types;

[ERROR] required: java.util.List<T>,int

[ERROR] found: java.util.List<com.ibm.ilm.bcbs248.Mammal<?>>,int

[ERROR] reason: inferred type does not conform to equality
constraint(s)

[ERROR] inferred: com.ibm.ilm.bcbs248.Mammal<capture#1 of ?>

[ERROR] equality constraints(s): com.ibm.ilm.bcbs248.Mammal<capture#1
of ?>,com.ibm.ilm.bcbs248.Mammal<?>

[ERROR] Animal.java:[27,68] method groupByAge in class
com.ibm.ilm.bcbs248.Animal<Tcannot be applied to given types;

[ERROR] required: java.util.List<T>

[ERROR] found: java.util.List<com.ibm.ilm.bcbs248.Mammal<?>>

[ERROR] reason: inferred type does not conform to equality
constraint(s)

[ERROR] inferred: com.ibm.ilm.bcbs248.Mammal<capture#2 of ?>

[ERROR] equality constraints(s): com.ibm.ilm.bcbs248.Mammal<capture#2
of ?>,com.ibm.ilm.bcbs248.Mammal<?>

At first glance, I thought this is a Maven-Problem (because Eclipse can compile and execute the code), but in reality, it seems to be a bug (?) in the Eclipse compiler. Even the normal Java compiler is not able to compile the program, it terminates with a type error as the program cannot be verified. This a well-known problem with Java generics, ie, the Java type checker cannot verify the program at this point. This is simply due to the approximation of the static type checker.

As far as I know, Eclipse uses an own Java compiler, ie, a wrapper around the original Java compiler that allowed to compile malicious programs? However, in this situation, it does not show any error message? Can anybody verify that is this a problem with the Eclipse compiler?

Moreover, does anybody have another elegant idea on how to solve the coding problem? As a simple word-around, I can change the type signature of both helper function as follows:

public static <T extends Animal<?>> List<T> increaseAge(List<T> animals, int augment) {
    List<T> list = new LinkedList<T>();
    for (T animal : animals) {
        list.add((T) animal.setAge(animal.getAge() + augment));
    }
    return list;
}

public static <T extends Animal<?>> Map<Integer, List<T>> groupByAge(List<T> animals) {
    Map<Integer, List<T>> animalsPerAge = new TreeMap<Integer, List<T>>();
    animals.forEach((animal) -> {
        int age = animal.getAge();
        animalsPerAge.putIfAbsent(age, new LinkedList<T>());
        animalsPerAge.get(age).add(animal);
    });
    return animalsPerAge;
}

Unfortunately, I also need to add a type cast in line list.add((T) animal.setAge(animal.getAge() + augment)); . I can live with the changed type signature, but I will get rid of the typecast as it ends in uncertainties.

Re-adding an element of the same type seems not to be a problem with the changes type-signature. However, after calling setAge some type of information seems to be lost. Maybe I can change the type of the function or object in a way that this works?

--

Maven version 3.1

Java version 1.8.0_201 Java(TM) SE Runtime Environment (build 1.8.0_201-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)

Eclipse IDE for Java Developers Version: Oxygen.3a Release (4.7.3a) Build id: 20180405-1200

I thought this is a Maven-Problem

Maven is just a build tool. If you're getting a compiler error, it's nothing to do with Maven.

Eclipse uses an own Java compiler, ie, a wrapper around the original Java compiler that allowed to compile malicious programs

Eclipse has a different compiler, yes. 'Malicious' is not correct. It supports incremental compilation (only re-compile code when it has changed). It can compile and run code with compile-time errors, deferring them to a runtime exception instead.

Can anybody verify that is this a problem with the Eclipse compiler?

As you said yourself: "Even the normal Java compiler is not able to compile the program". So the answer is no. Your program is just incorrect.


Your problem basically stems from the fact that I don't think these are meaningful generic type parameters

Animal<T extends Animal<T>>
Mammal<T extends Mammal<T>>

When I see a generic type parameter, I try to insert the words 'to' or 'of' and see if it makes sense.

List<String> is a list of strings. Comparable<String> is something that can be compared to strings. A Listener<Message> listens to messages.

So then what is Mammal<Cat> ? A mammal of cats? A mammal to cats? Do you see how it doesn't make sense in the same way?

You're trying to use generics to propagate up knowledge of the child class to the parent class. That is not what they're for.


Here is how your code looks when you remove the generic type parameters. There is one unchecked cast, but we know that any setter is guaranteed to return an object of the same type, so its safe to ignore it.

abstract class Animal {
    private final int age;

    public Animal(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    public abstract Animal setAge(int age);

    public static <T extends Animal> List<T> increaseAge(List<T> animals, int augment) {
        List<T> list = new LinkedList<>();
        for (T animal : animals) {
            list.add((T) animal.setAge(animal.getAge() + augment));
        }
        return list;
    }

    public static <T extends Animal> Map<Integer, List<T>> groupByAge(List<T> animals) {
        Map<Integer, List<T>> animalsPerAge = new TreeMap<>();
        animals.forEach((animal) -> {
            int age = animal.getAge();
            animalsPerAge.putIfAbsent(age, new LinkedList<>());
            animalsPerAge.get(age).add(animal);
        });
        return animalsPerAge;
    }

    public static void main(String[] args) {
        List<Mammal> mammals = new ArrayList<>();
        mammals = Animal.increaseAge(mammals, 1);
        Map<Integer, List<Mammal>> mammalsByAge = Animal.groupByAge(mammals);
    }
};

abstract class Mammal extends Animal {
    public Mammal(int age) {
        super(age);
    }
}

class Cat extends Mammal {
    public Cat(int age) {
        super(age);
    }

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

To sum up, the problem is not the method body. The type error says that the list given as argument does not fit the type signature of the method(s). In the end, it boils down to the standard covariance and contravariance problem of lists with generics. Somehow I thought that I could be clever and bypass the uncertainties by using nested generics like Animal<Mammal<T>> and concrete method signatures, but it seems that it only grafted the problem to another place.

To sum up possible solutions:

The second method call ( groupByAge ) can be fixed by using the wildcard ? , like <T extends Animal<?>> . This works as we only move an element of type T to another list of type T .

The first method call ( increaseByAge ) is trickier. We can also add the wildcard ? to the type signature of the method, however, then we lose the information that we have an element of type T after calling setAge ( setAge returns then an element of type Animal<?> ).

Someone another idea how to address this, maybe by using another type signature for Animal?

Obviously, a typecast would solve this problem, but type casts are not acceptable (even if their only purpose is to make the type checker happy).

Another possible solution would be to completely remove the generic types from Mammal at the list definition, like List<Mammal> mammals = new ArrayList<Mammal>(); . However, this also leads to unchecked operations and is therefore out.

Obviously, the code also types-checks if we use concrete lists, like a list of Cat 's or a list of Dog 's, like List<Cat> cats = new ArrayList<Cat>(); . However, the situation requires to have a common list for cats and dogs in this situation.

Finally, it remains to have type-specific increaseByAge -methods, for example, an increaseByAge methods for lists of mammals, maybe also implement in the Mammal class.

public static  List<Mammal<?>> increaseByAge(List<Mammal<?>> mammals, int augment) {
     List<Mammal<?>> list = new LinkedList<Mammal<?>>();
     for (Mammal<?> mammal : mammals) {
         mammal = mammal.setAge(mammal.getAge() + augment);
         list.add(mammal);
     }
     return list;
 }

One can remove the generic type from the method and focus directly on lists of mammals. Unfortuantly, this requires to have multiple increaseByAge methods, one for each group of animals.

I'm still open to other solutions. Maybe somebody also sees another idea of how to write the generic types of the objects to make this easier.

Despite all the discussion on the generic types, it remains to mention that this seems to be a bug in the Eclipse type checker. Whereas the standard Java compiler cannot compile this code, the Eclipse IDE compiled the code nor does it show any type error.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM