Search code examples
javagenericscollectionsoption-type

Java generics: Does "T2 extends T1" imply "T1 super T2"?


If I have a generic <T2 extends T1>, does the compiler infer T1 super T2?

I have a more complex collection, which I reduced to the MWE below. The collection shall be mergeable with any such collection with elements of any subtype.

Now I'm wondering, why the call to forEach in merge is not accepted. It fails with

java.util.function.Consumer<java.util.Optional> cannot be converted to java.util.function.Consumer<java.util.Optional<? super T2>>

I've depicted the type relationships in the diagram below. In merge, T2 extends T1. The call to forEach is applied on the other object, hence the T1 of this becomes the ? of other.forEach and the T2 of this.merge is the T1 of other. Hence, this' T1 should be accepted as super of other's T1.

I also tried public void merge(Group<? extends T> other) with the same result. And public void merge(Group<T> other) does not accept such collections with elements of any subtype of T1.

MWE:

class Group<T1> {
    public <T2 extends T1> void merge(Group<T2> other) {
        Consumer<Optional<T1>> task = t -> t.ifPresentOrElse(this::foo, this::bar);
        other.forEach(task);
    }

    public Collection<Optional<T1>> values() {
        return List.of();
    }

    public void forEach(Consumer<Optional<? super T1>> consumer) {
        values().forEach(consumer);
    }

    private void foo(T1 t) {}

    private void bar() {}
}

Relationships:

   this.T1      -becomes->  other.forEach.?
      ^                            |
      |                          super
   extends                         |
      |                            v
this.merge.T2     -is->        other.T1

Solution

  • You can solve your problem using

    class Group<T1> {
        public <T2 extends T1> void merge(Group<T2> other) {
            Consumer<Optional<? extends T1>> task=t->t.ifPresentOrElse(this::foo, this::bar);
            other.forEach(task);
        }
    
        public Collection<Optional<T1>> values() {
            return List.of();
        }
    
        public void forEach(Consumer<? super Optional<T1>> consumer) {
            values().forEach(consumer);
        }
    
        private void foo(T1 t) {}
    
        private void bar() {}
    }
    

    This is an application of the PECS rule.

    Your task will treat the Optional as a producer, hence, has to declare Optional<? extends T1> to allow the optional to produce subtypes of T1. We know that optional will always act as a producer, but Java’s generic type system does not have the concept of classes always acting like a producer or consumer and needs an explicit ? extends T1 here.

    The forEach receives a consumer, even literally in the interface name, so ? super … is the right way to declare that this consumer may be a consumer of supertypes of Optional<T1>. This does not only include Object, the superclass of Optional, but also Optional<? extends T1> when T1 := T2, as in the invocation within merge, as Optional<? extends T1> is a supertype of Optional<T2>.


    Of course, you could also solve the issue by simply using

    Consumer<Optional<T2>> task = t -> t.ifPresentOrElse(this::foo, this::bar);
    

    as this only requires foo to accept a T2 argument, which it always does, as T2 is a subtype of T1. But changing the signature of forEach to raise the flexibility is always a good move. Since it’s a public method, there might be other callers benefitting from it.