Mark L
Mark L

Reputation: 103

Why does this code only compile with a redundant map()?

I've come across an oddity I can't explain.

The following code (a minimal example) won't compile:

class Test {
  interface I {}

  Optional<I> findOne(ArrayList<? extends I> list) {
    return list.stream()
        .findFirst();
  }
}

javac (version 11 at least) says:

error: incompatible types: Optional<CAP#1> cannot be converted to Optional<I>
        .findFirst();
                  ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends I from capture of ? extends I

However, I sort of randomly found that adding a seemingly redundant .map() call to the stream [edit: it's actually added to the Optional returned by findFirst()] compiles fine:

class Test {
  interface I {}

  Optional<I> findOne(ArrayList<? extends I> list) {
    return list.stream()
        .findFirst()
        .map(a -> a);
  }
}

I'm curious about what is happening there that adding the .map() call lets it compile.

[I know that changing the method parameter to take an ArrayList<I> works, but that's not what I'm asking about.]

Upvotes: 3

Views: 89

Answers (1)

Zabuzard
Zabuzard

Reputation: 25903

Generics are invariant

The first does not compile because Java generics are invariant.

An Optional<Dog> is completely different to an Optional<Animal>, hence Optional<...> with ... being the type your ? captured is not compatible to Optional<I>.

To elaborate, consider the following example:

List<Dog> dogs = new ArrayList<>();

List<Animal> animals = dogs; // this does not compile, but pretend it would
animals.add(new Cat());

Dog dog = dogs.get(0); // should be safe, but its actually a cat!

Upcast

But with the map(a -> a) you have an implicit upcast of the type, like (Animal) dog which makes this actually an Optional<Animal>. So Optional<I> instead of Optional<...> in your case.

So your a -> a lambda, which looks innocent actually is a method taking Dog (or ... from ? in your case) and giving out an Animal (or I):

a -> (I) a

Consider the following example

Optional<Dog> dog = Optional.of(new Dog());

Optional<Animal> animal = dog.map(d -> d);

which demonstrates exactly your situation. Because of invariance you can not just assign it, you have to actively convert it. And because of context, Java can implicitly cast the d to Animal. So the code is equivalent to

dog.map(d -> (Animal) d);

Note

Note that the map method you call here is not the map from Stream but from Optional, which is the result of findFirst. So its purpuse is to map from an Optional<X> to an Optional<Y> by applying the given transformation.

Upvotes: 4

Related Questions