Learning Java

Java 8 type inference in generic methods chain call - what might go wrong?

Yesterday I have found this interesting question on Stack Overflow asked by Opal. He faced some unexpected compilation errors when dealing with Java generics and Vavr library. It turned out the root cause of the issue there was not the library, but Java compiler itself. This was pretty interesting use case and it motivated me to investigate it even further. This blog post reveals untold truth about Java generics type inference. Are you ready? :)

An example

Let’s define a simple Some<T> generic class:

import java.util.function.Consumer;
import java.util.function.Supplier;

public final class Some<T> {

    private final T value;

    private Some(final T t) {
        this.value = t;
    }

    static <T> Some<T> of(final Supplier<T> supplier) {
        return new Some<>(supplier.get());
    }

    public Some<T> peek(final Consumer<T> consumer) {
        consumer.accept(value);
        return this;
    }

    public T get() {
        return value;
    }
}

Some<T> represents some value provided by a supplier function. There is not much we can do we this object - its class provides only two additional methods, peek(Consumer<T> consumer) and get(). But that’s enough for this demo.

There is one useful thing we would like to take advantage of - peek() method returns Some<T> which in this case is the reference to the caller object. This is very handy and it allows us to create a chain of methods. Let’s try it out and create Some<List<? extends CharSequence>> object:

Listing 1. Example 1
final class SomeExample {

    public static void main(String[] args) {
        Some<List<? extends CharSequence>> some =
                Some.of(() -> Arrays.asList("a", "b", "c"));

        System.out.println(some.get());
    }
}

So far so good - this simple example compiles and produces [a, b, c] in the console log when executed. Let’s modify the code a bit and use peek() method instead System.out.println(some.get()):

Listing 2. Example 2
final class SomeExample {

    public static void main(String[] args) {
        Some<List<? extends CharSequence>> some =
                Some.of(() -> Arrays.asList("a", "b", "c")).peek(System.out::println);
    }
}

And now something unexpected happens. Suddenly, compiler started complaining about incompatible types:

At some point it makes sense, because Java generics are invariant[1] - Java Language Specification in chapter 4.10. Subtyping says clearly:

Subtyping does not extend through parameterized types: T <: S does not imply that C<T> <: C<S>.

So why does the example without peek() method invocation worked?

Generalized Target-Type Inference

In the first example we have took an advantage of the feature introduced in Java 8 with JSR-335[2] - Generalized Target-Type Inference, proposed as JEP-101[3]. It added a whole new chapter to the language specification - Chapter 18. Type Inference[4]. It made Java compiler context aware when it comes to type inference, so it can deduct the expected type from the left side of the expression and subtype[5] if needed.

It explains why Java compiler is satisfied by reducing expression on the right side to type Some<List<? extends CharSequence>> while assigning value to a some variable:

It shows that even if:

  • Arrays.asList("a", "b", "c") returns List<String>,

  • Some.of(() → Arrays.asList("a", "b", "c")) returns Some<List<String>>,

then assigning it to a type like Some<List<? extends CharSequence>> changes the invocation context and in this context Some.of() returns type defined on the left side of the assignment.

Limitations

Java 8 type inference system has some limitations. One of them is type inference in chain methods call. JEP-101 mentions this problem, and our second example proves this limitation exists:

When we start chaining methods, compiler delays type inference until the expression on the right side gets evaluated. Last method in the chain receives Some<List<String>> from the first method and it passes it to variable assignment. This is why we see compiler error in this case.

Solutions

There are two solutions (or workarounds) to this limitation.

1) We can specify explicitly generic type:

final class SomeExample {

    public static void main(String[] args) {
        Some<List<? extends CharSequence>> some =
                Some.<List<? extends CharSequence>>of(() -> Arrays.asList("a", "b", "c")).peek(System.out::println);
    }
}

In this case we instruct compiler that we expect Some.of() to return this specific type and it gets passed to peek() method which returns previously specified type back.

2) We can break the chain and split assignment from the rest chain calls

final class SomeExample {

    public static void main(String[] args) {
        Some<List<? extends CharSequence>> some = Some.of(() -> Arrays.asList("a", "b", "c"));
        some.peek(System.out::println);
    }
}

Did you like this article?

Consider buying me a coffee

0 Comments