You have two different kinds of polymorphism that are interacting here, in a confusing way.
The key to understanding this is that, in addition to the parametric polymorphism (that is, generics), you also have subtype polymorphism, that is, the classic object-oriented "is-a" relationship.
In Java, all objects are subtypes of Object
. So a container that can contain Object
values can contain any value.
If we rewrite all the generic bounds as just <Object>
then the code works the same way, and obviously so:
List<Object> objectList = new ArrayList<>();
objectList.add("str1");
List<Object> numberList = objectList;
numberList.add(1);
objectList.add("str2");
for (int i = 0; i < objectList.size(); i++) {
System.out.println(objectList.get(i) + "");
}
Specifically, objectList.get(i) + ""
gets evaluated into something that calls objectList.get(i).toString()
, and since toString()
is a method of Object
, that will work regardless of the type of objects in objectList
.
What won't work is this:
Number number = numberList.get(i); // error!
This is because, despite the misleading name, numberList
is not guaranteed to contain only Number
objects, and might in fact not contain any Number
objects at all!
Let's walk through and see why this must be so.
First we create a list of objects:
List<? super Object> objectList = new ArrayList<>();
What does that type mean? The type List<? super Object>
means "a list of objects of some type, I can't tell you what type, but I do know that whatever type it is is either Object
or a supertype of Object
". We already know that Object
is the root of the subtype hierarchy, so this is effectively the same as List<Object>
: that is, this object can only contain Object
objects.
But... that's not quite right. The list can only contain Object
objects, but an Object
object can be anything! The actual objects at runtype can be of any type that is a subtype of Object
(so, anything other than a primitive), but by putting them into this list, you're losing the ability to tell what kind of objects they are anymore -- they could be anything. That's OK for what the rest of this program does, though, because all it needs to be able to do is call toString()
on the objects, and it can do that because they all extend Object
.
Now let's look at the other variable declaration:
List<? super Number> numberList = objectList;
Again, what does the type List<? super Number>
mean? Crucially, it means "a list of objects of some type, I can't tell you what type it is, but I do know that whatever type it is, it's either Number
or some supertype of Number
". Well, on the left hand we have a list of "Number
or some supertype of Number
" and on the right side we have a list of Object
-- clearly Object
is a supertype of Number
and so this list is a list of Object
. Everything type checks (and, contrary to my initial comment, without any warnings).
So the question becomes: why can a List<? super Number>
contain a String
? Because a List<? super Number>
can be just a List<Object>
, and a List<Object>
can contain a String
because String
is-a Object
.