Array covariance and type invariance trample records in Java

  • 2021-06-29 11:00:29
  • OfStack

Preface

Denaturation is a pit in the OOP language, and the array covariance of Java is an old pit.Because I recently stepped on it, I made a record.By the way, also mention the degeneration of paradigm 1.

Before explaining array covariance, three related concepts, covariance, invariance, and inversion, are defined.

Next, let's go into more detail.

1. Covariance, Invariance, Inversion

Suppose I wrote this code for a restaurant


class Soup<T> {
 public void add(T t) {}
}

class Vegetable { }

class Carrot extends Vegetable { }

There is a generic class Soup < T > The method add (T t) means to add the ingredient T to the soup.Class Vegetable denotes vegetables, class Carrot denotes carrots.Of course, Carrot is a subclass of Vegetable.

So here's the problem, Soup < Vegetable > And Soup < Carrot > What is the relationship between them?

Reaction 1, Soup < Carrot > Should be Soup < Vegetable > Because carrot soup is obviously a vegetable soup.If so, look at the code below.Tomato stands for tomato and is another subclass of Vegetable


Soup<Vegetable> soup = new Soup<Carrot>();
soup.add(new Tomato());

First sentence OK, Soup < Carrot > Is Soup < Vegetable > Subclass, so you can use Soup < Carrot > Instance assigned to variable soup.No problem with the second sentence, because soup is declared Soup < Vegetable > Type, its add method receives an Vegetable-type parameter, while Tomato is Vegetable, the correct type.

However, there is a problem with putting the two sentences together.The actual type of soup is Soup < Carrot > And we passed an instance of Tomato to its add method!In other words, we can't make carrot soup with tomatoes.So put Soup < Carrot > Treat as Soup < Vegetable > Although logically smooth, subclasses are defective in use.

So, Soup < Carrot > And Soup < Vegetable > What exactly should it be?Different languages have different understandings and implementations.To sum up, there are three situations.

(1) If Soup < Carrot > Is Soup < Vegetable > A subclass of the generic Soup < T > Is covariant

(2) If Soup < Carrot > And Soup < Vegetable > Are two unrelated classes, called generic Soup < T > Is unchanged

(3) If Soup < Carrot > Is Soup < Vegetable > The parent class of is called generic Soup < T > Is inverted. (But inversions are not common)

Understand the concepts of covariance, invariance and inversion, and then look at the implementation of Java.The general generic of Java is unchanged, that is, Soup < Vegetable > And Soup < Carrot >An instance of one class cannot be assigned to a variable of another class.So the above code for making carrot soup from tomatoes can't actually be compiled at all.

2. Array covariance

In Java, arrays are basic types, not generic, and there is no Array < T > Such a thing.But it is very similar to generics in that they are all types built with another type.So arrays are also denatured.

Unlike the invariance of generics, Java arrays are covariant.That is, Carrot[] is a subclass of Vegetable[].The examples in the previous section have shown that covariance can sometimes cause problems.Like the following code


Vegetable[] vegetables = new Carrot[10];
vegetables[0] = new Tomato(); //  Runtime Error 

Because the array is covariant, the compiler allows Carrot[10] to be assigned to a variable of type Vegetable[], so this code can be compiled successfully.Only during runtime did JVM really try to insert a tomato into a pile of carrots did it find something bad.Therefore, the code above throws an exception of type java.lang.ArrayStoreException at runtime.

Array covariance is a well-known historical burden of Java.Be careful when using arrays!

If you replace the array in the example with List, the situation will be different.Like this


ArrayList<Vegetable> vegetables = new ArrayList<Carrot>(); //  Compile-time error 
vegetables.add(new Tomato());

ArrayList is a generic class and is invariant.So, ArrayList < Carrot > And ArrayList < Vegetable > There is no inheritance between them, and this code will fail at compile time.

Both pieces of code will fail, but generally compile-time errors are better handled than run-time errors.

3. When generics also want covariance, contravariance

Generics are immutable, but in some scenarios we still want them to be covariant.For example, there is a little sister who drinks vegetable soup every day to lose weight


class Girl {
 public void drink(Soup<Vegetable> soup) {}
}

We hope that the drink method will accept a variety of vegetable soups, including Soup < Carrot > And Soup < Tomato > .However, they cannot be used as parameters for drink due to the constraints of invariance.

To achieve this, you should use a writing similar to covariance


public void drink(Soup<? extends Vegetable> soup) {}

This means that the type of parameter soup is the generic class Soup < T > T is a subclass of Vegetable (including Vegetable itself).At this time, Miss Sister can finally enjoy the carrot soup and tomato soup.

However, this approach has one limitation.The compiler only knows that a generic parameter is a subclass of Vegetable, but it does not know what it is.Therefore, all non-null generic type parameters are considered unsafe.It's hard to say, but it's really simple.Direct Up Code


public void drink(Soup<? extends Vegetable> soup) {
 soup.add(new Tomato()); //  error 
 soup.add(null); //  Correct 
}

The first sentence in a method will fail at compile time.Because the compiler only knows that the parameters of the add method are subclasses of Vegetable, it does not know what type it is Carrot, Tomato, or whatever.In this case, passing an instance 1 Law of a specific type is considered unsafe.Even if soup is really Soup < Tomato > Types are also not possible, because the specific type information for soup is only known at run time and not at compile time.

However, the second sentence in the method is correct.Because the parameter is null, it can be any legal type.The compiler considers it safe.

Similarly, there is a method similar to inversion


public void drink(Soup<? super Vegetable> soup) {}

At this point, Soup < T > The T in must be the parent of Vegetable.

In this case, there are no restrictions above, and the code below is fine


public void drink(Soup<? super Vegetable> soup) {
 soup.add(new Tomato());
}

Tomato is a subclass of Vegetable and, naturally, of Vegetable's parent.Therefore, at compile time, you can determine that the type is safe.

summary


Related articles: