31

Consider the snippet:

Number[] numbers = {1, 2.3, 4.5f, 6000000000000000000L};

It's perfectly okay to do the above, Number is an abstract class.

Going ahead,

List<Long> listLong = new ArrayList<Long>();
listLong.add(Long.valueOf(10));

List<Number> listNumbers = listLong; // compiler error    - LINE 3

listNumbers.add(Double.valueOf(1.23));

Had Line 3 was designed to be compiled successfully, we would end up with a List of Numbers, i.e,

for(Number num: listNumbers ){
    System.out.println(num);
}

// 10
// 1.23

which are all numbers.

I came across this in a book,

Generics doesn’t support sub-typing because it will cause issues in achieving type safety. That’s why List<T> is not considered as a subtype of List<S> where S is the super-type of T

Which type safety would have lost in this specific case as discussed above, were the Line 3 was to be compile successfully?

Maroun
  • 94,125
  • 30
  • 188
  • 241
Farhan stands with Palestine
  • 13,890
  • 13
  • 58
  • 105

4 Answers4

43
List<Long> listLong = new ArrayList<Long>();
List<Number> listNumbers = listLong;

So, listNumbers and listLong would be two references to the same list, if that was possible, right?

listNumbers.add(Double.valueOf(1.23));

So, you would be able to add a Double to that list. listLong, of type List<Long>, would thus contain a Double. The type-safety would thus be broken.

JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • 1
    How can I be so stupid this morning? – Farhan stands with Palestine Feb 28 '16 at 11:30
  • 14
    @ShirgillFarhanAnsari you're not, it's actually a good question :) – Maroun Feb 28 '16 at 11:30
  • @ShirgillFarhanAnsari In C# they created `in` and `out` (Generic Modifier) for this reason. Look it up if you want to know a strategy that avoids the problem pointed out in this answer. – WorldSEnder Feb 28 '16 at 13:57
  • It is certainly a good question, and one point of confusion might be because arrays are *not* invariant. This is a bug: they *should* be invariant, because the exact same issue occurs (`long[] longs = { 1, 2 }; Number[] nums = longs; nums[0] = 0.5; }`). However, when Java didn't have generics, arrays were the only way to pass a typed collection, so they wanted *something* to be covariant. Such is life. – wchargin Feb 28 '16 at 13:57
  • @WorldSEnder For reference, Java has those, too: they're called `? extends T` and `? super T` respectively. Search "generic wildcards constrained" or something. (If `in` and `out` do what I assume they do?) – wchargin Feb 28 '16 at 13:58
  • @ShirgillFarhanAnsari thats a "problem" that relates to mutability. If a List was ummutable, adding a supertype to the List would not modify the List and instead return a List with the parametrized supertype. – vicaba Feb 28 '16 at 14:15
  • It is beyond me why Oracle chose to no longer host the superb [generics-tutorial.pdf](https://web.archive.org/web/20050715000000*/http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf). It answers all questions. – VGR Feb 28 '16 at 15:41
  • @WChargin C# works differently, but the general idea is the same. For example, there is no way to write `IList extends Number>`, because `IList` doesn't (and can't) have `out`. You can use `IReadOnlyList` for the same purpose, but the object has to actually implement that interface (in addition to `IList`). Using that, you can write `IReadOnlyList listNumbers = listLong;`. – svick Feb 28 '16 at 15:49
  • @svick: I must be missing something, because I thought there is a way, namely `IList where T : Number` Of course, since it is invariant, it can't be used as a `IList` – Ben Voigt Feb 28 '16 at 16:28
  • 1
    @svick Basically the difference is where you declare contra- respectively covariance. C# has declaration-site variance, while Java has use-site variance. Use-set declarations are more powerful (you avoid lots of code duplication), but at the same time more confusing for your average user. As much as it pains me, declaration-site variance is probably the better approach for mainstream languages. – Voo Feb 28 '16 at 16:29
  • @BenVoigt That restricts what `T`s can be used with `IList`. I don't see how is that in any way similar to what you can do in Java: `List extends Number> listNumbers = listLong;`. – svick Feb 28 '16 at 16:32
  • @svick: Ahh, it's true that C# doesn't allow it in a variable declaration (only in a class or method signature). Weird that Java does. – Ben Voigt Feb 28 '16 at 16:33
8

If that was the case, then we could add other different subtypes of Number into listNumbers, which must be forbidden.

Imagine you're now inserting objects of type Double and Long, and later you try to use Long#reverse. Your code will compile but of course will fail at runtime (bad) the first Double it'll come through.

Maroun
  • 94,125
  • 30
  • 188
  • 241
5

Let's use an example with a non-abstract base class:

public class Human {
    public string getName() {
        // ...
    }
}

public class Student extends Human {
    public void learn(Subject subject) {
        // ...
    }
}

public class Teacher extends Human {
    public void teach(Subject subject) {
        // ...
    }
}

At any place where a Human is expected, a Student or Teacher will do just as well, as they fully implement the Human interface. (In this case, that getName() can be called on them.) Java inheritance guarantees that this is the case technically. Making it work semantically is the class author's job, so that his code fulfils the Liskov substitution principle.

So doesn't this mean that we can also substitute Collection<Teacher> where a Collection<Human> is expected? Not always. Consider the following method:

public class Human {
    // ...

    public void join(Set<Human> party) {
        party.add(this);
    }
}

Now, if Java allowed a Set<Student> to be passed as party, any attempts of non-Student Humans to join that party would have to fail at runtime.

As a general rule, a container of a subtype is unsuitable if the receiver (callee in case of a function argument, caller in case of a function return value) wants to put something into it, but acceptable if the receiver only want to take stuff out and use it. A container of a supertype is unsuitable if the receiver wants to take stuff out and use it, but acceptable if the receiver only ever puts stuff into it. As a result, if the receiver both takes stuff out of the collection and puts stuff into the collection, they usually must require a collection of a fixed type.

Our join method only puts Humans into the party, so we could also allow a Set<Object> or a non-generic Set or equivalently a Set<?>. Java allows us to do that with lower-bounded wildcards:

public class Human {
    // ...

    public void join(Set<? super Human> party) {
        party.add(this);
    }
}

For opening up the possibilities towards subclasses, there's upper-bounded wildcards:

public class Teacher extends Human {
    public void teach(Subject subject, Set<? extends Student> schoolClass) {
        for (Student student : class) {
            student.learn(subject);
        }
    }
}

Now, if we ever subclass Student, the passed schoolClass can be a Set of that subtype, too.

das-g
  • 9,718
  • 4
  • 38
  • 80
3

The concept you are referring to is variance.

In other words, if S is a supertype of T, is List<S> a subtype, supertype, equal type, or unreleted to List<T>?

The answer for List -- and all other Java generics* -- is "unrelated", i.e. invariant.

class SuperType {}

class Type extends SuperType {}

class SubType extends Type {}

List<Type> list = ...

List<SuperType> superList = list;
superList.add(new SuperType());
// no, we shouldn't be able to add a SuperType to list

List<SubType> subList = list;
SubType item = subList.get(0);
// no, there's not necessarily only SubType items in list

*Java does have the notion of "use-site" variance, with wildcards (?). This will limit what methods are possible to call.

List<Type> list = ...

List<? super SubType> wildcardList = list;
wildcardList.add(new SubType());
// but...everything we get() is an Object

or

List<Type> list = ...

List<? extends SuperType> wildcardList = list;
SuperType item = wildcard.get(0);
// but...it's impossible to add()

FYI, some languages have the notion of definition-site variance, e.g. Scala. So List[Int] is indeed a subtype of List[Number]. That's possible with immutable collections (again, a limited set of methods), but obviously not for mutable ones.

Paul Draper
  • 78,542
  • 46
  • 206
  • 285