and in Java – why it works this way?

The way I look at it is this – the placeholder T stands in for a definite type and in places where we need to know the actual type we need to be able to work it out. In contrast the wildcard ? means any type and I will never need to know what that type is. You can use the extends and super bounds to limit that wildcard in some way but there’s no way to get the actual type.

So, if I have a List<? extends MySuper> then all I know about it is that every object in it implements the MySuper interface, and all the objects in that list are of the same type. I don’t know what that type is, only that it’s some subtype of MySuper. That means I can get objects out of that list so long as I only need to use the MySuper interface. What I can’t do is to put objects into the list because I don’t know what the type is – the compiler won’t allow it because even if I happen to have an object of the right type, it can’t be sure at compile time. So, the collection is, in a sense a read-only collection.

The logic works the other way when you have List<? super MySuper>. Here we’re saying the collection is of a definite type which is a supertype of MySuper. This means that you can always add a MySuper object to it. What you can’t do, because you don’t know the actual type, is retrieve objects from it. So you’ve now got a kind of write-only collection.

Where you use a bounded wildcard versus the ‘standard’ generic type parameter is where the value of the differences start to become apparent. Let’s say I have 3 classes PersonStudent and Teacher, with Person being the base that Student and Teacher extend. In an API you may write a method that takes a collection of Person and does something to every item in the collection. That’s fine, but you really only care that the collection is of some type that is compatible with the Person interface – it should work with List<Student> and List<Teacher> equally well. If you define the method like this

public void myMethod(List<Person> people) {
    for (Person p: people) {
        p.doThing();
    }
}

then it can’t take List<Student> or List<Teacher>. So, instead, you would define it to take List<? extends Person>

public void myMethod(List<? extends Person> people){
    for (Person p: people) {
        p.doThing();
    }
}

You can do that because myMethod never needs to add to the list. And now you find that List<Student> and List<Teacher> can both be passed into the method.

Now, let’s say that you’ve got another method which wants to add Students to a list. If the method parameter takes a List<Student> then it can’t take a List<People> even though that should be fine. So, you implement it as taking a List<? super Student> e.g.

public void listPopulatingMethod(List<? extends Student> source, List<? super Student> sink) {
    for (Student s: source) {
        sink.add(s);
    }
}

Leave a Comment