by Rahel Lüthy
While there are tons of articles explaining the concept of covariance in Java, this post attempts to highlight how and why Java and Scala differ when it comes to collection type safety.
Let’s start with Java arrays. They are covariant, meaning that an array of type S[] is a subtype of an array of type T[] if S is a subtype of T:
Things are simple and life is good. The fact that arrays are covariant makes code re-use possible. Utility methods inside java.util.Arrays are perfect examples, e.g.:
public static int binarySearch(Object[] a, Object key) {
// ...
}
binarySearch(new Integer[]{1, 2, 3}, 2); // VALIDAs usual, everything falls apart because of mutability. Java arrays are mutable, making it possible to store the wrong type of object into an array without the compiler being able to tell:
Cat[] cats = { new Cat("Gizmo"), new Cat("Bella") };
Animal[] animals = cats;
animals[0] = new Dog("Max"); // FAILS AT RUNTIMEOuch, heap pollution – an ArrayStoreException is thrown at runtime! People obsessed with types tend to call this unsound.
With Java 5 came generics, which allow “a type or method to operate on objects of various types while providing compile-time type safety.” (Oracle Java 1.5 Docs). Let me repeat that: Generics were introduced to improve type safety. It was thus not an option to accept the same unsound array behavior. Because Java collections are mutable, the only option was to make a List<T> invariant:
List<Cat> cats = Arrays.asList(new Cat("Gizmo"), new Cat("Bella"));
List<Animal> animals = cats; // DOES NOT COMPILEEven though Cat is a subtype of Animal, List<Cat> is not a subtype of List<Animal>. This is not very intuitive, but it is necessary to prevent heap pollution at runtime.
Obviously, we still want to write re-usable utility code. Wildcards are making that possible. Methods inside java.util.Collections are good examples, e.g.:
In Java, you thus get better compile-time safety when using generified collections than when using raw arrays.
In Scala, things are a lot different. Array[T] is Scala’s representation for Java’s T[]. Like in Java, a Scala Array is mutable. But in contrast to Java, Scala cares a lot more about compile-time type safety. The only way to prevent unsound heap pollution is to make Array[T] invariant:
val cats: Array[Cat] = Array(Cat("Gizmo"), Cat("Bella"))
val animals: Array[Animal] = cats // DOES NOT COMPILEScala arrays are thus safer to use than Java arrays.
Things are even better for Scala List, which is immutable. It is thus perfectly safe to make List[+T] covariant (notice the little +). There’s just no way to ever sneak in an object, let alone sneaking in an object with the wrong type:
In other words: In Scala, List[S] is a subtype of List[T] if S is a subtype of T.