badCoder
badCoder

Reputation: 770

Why bounds work so strange in Java?

I'm using Java 8. During training to passing Java OCP 8 I find some snippets of code that I don't understand and want to know, why it so strange for me.

I have next hierarchy:

class A {}
class B extends A {}
class C extends B {}

The first one, this code is work:

List<?> list1 = new ArrayList<A>() { 
    { 
        add(new A());
    }
};

But next code doesn't work, compilation error:

list1.add(new A());

So, why we can't add new record in this way?

The second one, this code is work:

List<? extends A> list2 = new ArrayList<A>() {
    {
        add(new A());
        add(new B());
        add(new C());
    } 
};

But next code doesn't work, compilation error:

list2.add(new A());
list2.add(new B());
list2.add(new C());

And the last one, this code is work:

List<? super B> list3 = new ArrayList<A>() {
    {
        add(new A());
        add(new B());
        add(new C());
    }
};

But in the next code, when we adding new A(), compilation error:

list3.add(new A()); // compilation error
list3.add(new B());
list3.add(new C());

Thanks for your answers!

Upvotes: 3

Views: 131

Answers (2)

Mark Adelsberger
Mark Adelsberger

Reputation: 45819

The short answer ("why it is strange") is that certain Java short-hand notations make two pieces of code look very similar when they are really very different.

So it can seem like "if this works, this should also work", but it's not so because important differences in the two bits of code are obscured.

Let's look at a few pieces of code separately:

public interface ListFactory {
    List<?> getList();
}

So someone's going to give us a way to request a List<?>. We can use it like this:

List<?> list1 = myListFactory.getList();

But if we do

list1.add(new A());

the compiler can't prove this is legal, because it depends on whether we happened to get a ListFactory implementation that returns a List<A>, or maybe a List<String>.

What if we replace the above with

List<?> list1 = new SpecialList();
list1.add(new A());

This is a little closer to your original code; but to the compiler the same problem exists: when it evaluates list1.add(new A()); it doesn't look for clues in the history of assignments to list1. It only knows that the compile-time reference type of list1 is List<?>, so if list1.add(new A()); was illegal before, it's still illegal here.

(At a glance this may feel like a shortcoming of the compiler, but I think it's not; it generally isn't practical or desirable for the compiler to try and know any more than the reference type directly tells it. If we'd wanted to refer to our object in a way that allows add(new A()) we'd use a different reference type - e.g. List<A> list2 = new SpecialList();.)

The above implies that we have a SpecialList.java; let's take a look at it:

public class SpecialList extends ArrayList<A> {
    public SpecialList() {
        add(new A());
    }
}

This might seem a little silly, but it's probably no surprise that there's nothing wrong with it as far as the compiler is concerned (as long as A is defined and java.util.ArrayList is imported).

Note that add(new A()); in this case is shorthand for this.add(new A());. In the SpecialList() constructor, this is a reference of type SpecialList - which is declared to extend ArrayList<A> - and certainly we can add(new A()) to a subclass of ArrayList<A>.

So now we have all the pieces to make something like your original code:

List<?> list1 = new ArrayList<A>() {
    {
        add(new A());
    }
}
list1.add(new A());

Lines 3 and 6 here look very similar now because of the Java syntactic sugar we've applied. But line 3 is really like our SpecialList() example - the reference type through which add() is invoked is an anonymous subclass of ArrayList<A>. Line 6, though, is pretty much what it appears to be - and so it fails for the same reason it did in the first couple examples.

Similar analysis will explain the other weird distinctions you're seeing.

Upvotes: 1

Klitos Kyriacou
Klitos Kyriacou

Reputation: 11664

This is a compilation error designed to enforce type safety. If the compiler allowed you to do it, imagine what could happen:

For issue 1, once the object list1 has been declared, the compiler only considers the declared type, which is List<?> and ignores the fact that it was most recently assigned to an ArrayList<A>.

List<?> list1 = ...;  // The compiler knows list1 is a list of a certain type
                      // but it's not specified what the type is. It could be
                      // a List<String> or List<Integer> or List<Anything>
list1.add(new A());   // What if list1 was e.g. a List<String>?

But:

List<?> list1 = new ArrayList<A>() { 
    { 
        add(new A());
    }
};

Here, you are assigning to list1 an expression. The expression itself, i.e. everything after =, doesn't use ?, and is in fact an anonymous class that extends ArrayList<A> and has an initializer block that calls add(new A()) which is ok.

The second issue (with list2) has the same cause.

In the third issue,

List<? super B> list3 = new ArrayList<A>() {
    {
        add(new A());
        add(new B());
        add(new C());
    }
};

list3.add(new A()); // compilation error

The compiler sees list3 as a List<? super B>. This means the generic parameter can be B or its superclass, A. What if it's a List<B>? You can't add an A to a List<B>; therefore the compiler rejects this code.

Upvotes: 4

Related Questions