Why can't I add elements to an "extends" List but can do so with a "super" List?

I don’t understand Java wildcards very well. I don’t understand why this doesn’t compile

Person marie = new Person("Marie", "Brown");
List<? extends Person> extendsPersonList = new ArrayList<Person>();

but this does

Person marie = new Person("Marie", "Brown");
List<? super Person> superPersonList = new ArrayList<Person>();

In both cases ? stands for an unknown type that must include Person (since it’s a subtype and a supertype of itself). But the lists behave completely differently. Why?

ChatGPT said the compiler can’t be sure it’s a list of Person and not a list of some Person subtype so it can’t approve any addition to the extends list. But even if that was the case (suppose, prior to the addition, it only contained Students), Person would still conform to the bound, wouldn’t it? Then why can’t it add that object?

UPD: I came up with a way to explain it. Is it an accurate explanation?

First, let me tell you about casting in Java. There are two kinds of casting: narrowing casting (casting down from a parent to a child) and widening casting (casting up from a child to a parent). Casting down is what the Java compiler never does. In other words, it can’t be performed implicitly. Why? Because the compiler can’t be sure it’s safe. Imagine Student has an extra field University that is absent in its parent, the Person class. Even if such fields (unique to a child) are initialized with default values, in this case null as University would be a reference type, accessing those fields later may result in exceptions, in this case NullPointerException. So whenever you do downcasting you do it explicitly, taking full responsibility for any potential repercussions. You basically tell the compiler, “I know more about this object than you do, I’m sure it’s safe to do, and I’ll do it”. Explicit casting requires a cast operator, which is a name of a class in parentheses, for example Person person = (Person) object. It often happens in equals() implementations immediately after you test an object type with instanceof.

Now, let’s get back to wildcards. One thing you should understand is that List<? extends Person> doesn’t mean “a List of anything that is Person” (List<Person> means that). Instead, it means “a List of some specific but unknown type that extends Person”. Some options are: a List of Person, a List of Student, a List of Employee (assuming Student and Employee are Person subclasses). Whenever you add an element to a List, the compiler has to perform an implicit cast. As you already know, an implicit cast can only be up. So whenever you add Person to List<? super Person>, the compiler is happy: it knows that even if the actual type of the List is some Person supertype, for example Object, it’s guaranteed that it will have no problem upcasting Person to Object. But whenever you try to add a Person to List<? extends Person>, the compiler is not happy. If the actual type of the List turns out to be List<Student>, for example, there would arise a need for an unsafe downcasting which is off the table with the Java compiler. As a result, it refuses to greenlight the addition

UPD2: For some reason, this post was flagged as needing “moderator attention”. Please let me know which part of it is problematic

Hey @nad.chel ,

Wow I did now know this was how the compiler worked! I did some research and I think your explanation is spot on.

Basically the compiler finds narrow casting unsafe, and naturally doesn’t allow it when it encounters the .add() method. Unless you specifically override it through casting.

Good job!