简体   繁体   中英

Why does this code involving “? super T” compile successfully?

I am trying to understand ? super T ? super T to see how it works, and I'm stuck at the below example:

class Thing {
  AnotherThing change() { return null; }
}

class AnotherThing {}

interface Fn<A, B> {
  B run(A a);
}

class Stream<T> {
  <R> Stream<R> map(Fn<? super T, ? extends R> fn) {
     return null;
  }
}

void method() {
  Stream<Thing> s = new Stream<>();
  s.map(a -> a.change());
}

The strangest point is that Java can infer that a is Thing and, therefore, can invoke change() .

But that is not true. From Fn<? super T, ? extends R> fn Fn<? super T, ? extends R> fn Fn<? super T, ? extends R> fn , a could be Thing or java.lang.Object ; Both are "super" of T (or, in this case, Thing ).

I am missing some lesson around ? super T ? super T . I was wondering if anyone can explain why Java can deduce that a is Thing . Thank you!

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> 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 (ie CharSequence ) or a supertype of T (eg Object ). But the actual type passed to the accept method will still be T (ie 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 ? extends instead of ? super ? 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 (ie T ).

But that is not true. From Fn<? super T, ? extends R> fn Fn<? super T, ? extends R> fn Fn<? super T, ? extends R> fn , a could be Thing or java.lang.Object ; Both are "super" of T (or, in this case, Thing ).

You shouldn't say " a could be Thing or some some supertype," you should say " a can be Thing or some supertype". In the definition of map , where you take the Fn<? super T, ? extends R> fn Fn<? super T, ? extends R> fn Fn<? super T, ? extends R> fn as a given, the argument type of fn is already chosen by the caller of map . Inside the implementation of map you should use the past tense, because someone else has already chosen the type for you, and you have to make sure the code in map works no matter what it is. But, when you are calling map , you get to choose what the type of a is, as long as it is a supertype of Thing . In your code, a 's type can be Thing or Object , it's your choice. Because you haven't specified which, Java is nice and chooses the most specific possible type, Thing , for you, so that you have the biggest possible repertoire of methods available.

In general, if you have a F<? extends/super T> f F<? extends/super T> f , then the type parameter has already been chosen for you, and just you have to deal with whatever it is. The type bound is then your "friend"; it gives you additional information about what the parameter could be. In the opposite situation, if you need a F<? extends/super T> f F<? extends/super T> f , then you can choose the type parameter to be whatever it needs to be to fit your needs. The type bound is then your "enemy"; it limits the choices you can make for the parameter.

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