English Answer:
If 'List<Dog>
is a List<Animal>
', the former must support (inherit) all operations of the latter. Adding a cat can be done to latter, but not former. So the 'is a' relationship fails.
Programming Answer:
Type Safety
A conservative language default design choice that stops this corruption:
List<Dog> dogs = new List<>();
dogs.add(new Dog("mutley"));
List<Animal> animals = dogs;
animals.add(new Cat("felix"));
// Yikes!! animals and dogs refer to same object. dogs now contains a cat!!
In order to have a subtype relationship, must sastify 'castability'/'substitability' criteria.
Legal object substition - all operations on ancestor supported on decendant:
// Legal - one object, two references (cast to different type)
Dog dog = new Dog();
Animal animal = dog;
Legal collection substitution - all operations on ancestor supported on descendant:
// Legal - one object, two references (cast to different type)
List<Animal> list = new List<Animal>()
Collection<Animal> coll = list;
Illegal generic substitution (cast of type parameter) - unsupported ops in decendant:
// Illegal - one object, two references (cast to different type), but not typesafe
List<Dog> dogs = new List<Dog>()
List<Animal> animals = list; // would-be ancestor has broader ops than decendant
However
Depending on the design of the generic class, type parameters can used in 'safe positions', meaning that casting/substitution can sometimes succeed without corrupting type safety. Covariance means generic instatition G<U>
can substitute G<T>
if U is a same type or subtype of T. Contravariance means generic instantion G<U>
can substitue G<T>
if U is a same type or supertype of T. These are the safe positions for the 2 cases:
covariant positions:
- method return type (output of generic type) - subtypes must be equally/more restrictive, so their return types comply with ancestor
- type of immutable fields (set by owner class, then 'internally output-only') - subtypes must be more restrictive, so when they set immutable fields, they comply with ancestor
In these cases it's safe to allow substitutability of a type parameter with a decendant like this:
SomeCovariantType<Dog> decendant = new SomeCovariantType<>;
SomeCovariantType<? extends Animal> ancestor = decendant;
The wildcard plus 'extends' gives usage-site specified covariance.
contrvariant positions:
- method parameter type (input to generic type) - subtypes must be equally/more accommodating so they don't break when passed parameters of ancestor
- upper type parameter bounds (internal type instantiation) - subtypes must be equally/more accommodating, so they don't break when ancestors set variable values
In these cases it's safe to allow substitutability of a type parameter with an ancestor like this:
SomeContravariantType<Animal> decendant = new SomeContravariantType<>;
SomeContravariantType<? super Dog> ancestor = decendant;
The wildcard plus 'super' gives usage-site specified contravariance.
Using these 2 idioms takes extra effort and care from the developer to gain 'substitutability power'. Java requires manual developer effort to ensure the type parameters are truly used in covariant/contravariant positions, respectively (hence type-safe). I know not why - e.g. scala compiler checks this :-/. You're basically telling the compiler 'trust me, I know what I'm doing, this is type-safe'.
invariant positions
- type of mutable field (internal input and output) - can be read and written by all ancestor and subtype classes - reading is covariant, writing is contravariant; result is invariant
- (also if type parameter is used in both covariant and contravariant positions, then this results in invariance)