Instead of using a custom Fn
interface, let's talk about java.util.function.Consumer<T>
. If you're not aware, the Consumer
interface has a single abstract method: accept(T)
. When you use Consumer<? super T>
you're saying that the implementation of Consumer
can accept T
or a supertype of T
. What this doesn't mean, however, is that any supertype of T
can be passed to the accept
method—it must be of the type T
. You can see this with the following:
Consumer<? super CharSequence> con = System.out::println;
con.accept("Some string"); // String implements CharSequence
con.accept(new Object()); // compilation error
However, if you have some method like:
void subscribe(Consumer<? super CharSequence> con) { ... }
Then you could call it like so:
Consumer<Object> con = System.out::println;
subscribe(con);
This allows flexibility in the API. The caller can pass a Consumer
designed to accept T
(i.e. CharSequence
) or a supertype of T
(e.g. Object
). But the actual type passed to the accept
method will still be T
(i.e. CharSequence
), it's just the implementation of the Consumer
can be more general. The above wouldn't work if the parameter of subscribe
was declared Consumer<CharSequence>
instead.
A Consumer
is, unsurprisingly, a consumer. When something can be produced it is often best to use ? extends
instead of ? super
. This is known as Producer Extends Consumer Super (PECS). You can read more about it in this question.
Going back to your example, you have a class named Stream<T>
with a method Stream<R> map(Fn<? super T, ? extends R>)
and you ask how it knows that T
is a Thing
. It knows this because you've declared Stream<Thing>
which makes T
a Thing
. When you call the map
method you are implementing the Fn
inline. This makes the implementation use Thing
for T
and, based on the return signature inside the lambda, use AnotherThing
for R
. In other words, your code is equivalent to:
Fn<Thing, AnotherThing> f = a -> a.change(); // or you can use Thing::change
Stream<Thing> stream = new Stream<>();
stream.map(f);
But you could pass a Fn<Object, AnotherThing>
to the map
method. Note, however, that when using:
Fn<Object, AnotherThing> f = a -> a.change();
Stream<Thing> stream = new Stream<>();
stream.map(f);
The declared type of a
will now be Object
but the actual type will still be Thing
(i.e. T
).