Reading this article you will learn:

  • What are deformation, covariant, contravariant and invariance
  • How are these transformations implemented in Java and Kotlin
  • Similarities and differences between Generics in Java and Kotlin

In Java/Kotlin, a subclass object can be assigned to a superclass type, but a superclass object cannot be assigned to a subclass type, for example:

// Dog is a subclass of Animal
class Animal {}
class Dog: Animal() {}

val animal: Animal = dog // It is ok to assign a subclass object to a superclass type. Dog is also an Animal
val dog: Dog = animal // It is not possible to assign a parent object to a subclass. Not all animals are dog
Copy the code

After the introduction of generics, the situation becomes more complicated: a generic type whose type parameters are subclasses is not a subclass of a generic type whose type parameters are a parent class.

// Dogs is not a subclass of animals
val dogs: List<Dog>
val animals: List<Animal>
// Dogs and animals do not have any inheritance relationship, so the following code will compile an error
val animals: List<Animal> = dogs
Copy the code

Type arguments: Arguments in Angle brackets in a generic type are called type arguments. For example, String in List

is a type argument. Unlike ordinary arguments, type arguments pass a type rather than an object

For ease of description, all generics whose type parameters are subclasses are referred to as “subclass generics” and all generics whose type parameters are superclasses are referred to as “superclass generics”

For developers who have moved from Java to Kotlin, it’s best to understand generics in Java first, and then look at Kotlin’s generics as a breeze.

Generics in Java

The deformation of generics(variance)

  • Covariance: Subclass generics are subtypes of the parent generics and can be assigned to the parent generics
  • Contravariance: Parent generics are (as it were) children of subclass generics, and can be assigned to subclass generics
  • Invariant: Subclass generics and parent generics have no inheritance and cannot assign to each other

👆🏻 is a description of a partial concept that sounds very convoluted and very anti-human. In human terms, Dog is known as the subclass of Animal:

  • Covariance (Covariance) :List<Dog>List<Animal>Subtype of,List<Dog>Objects of type can be assigned toList<Animal>Type variable
  • Inverter (Contravariance) :List<Animal>BeList<Dog>Subtype of,List<Animal>Objects of type can be assigned toList<Dog>Type variable
  • Invariant:List<Animal>List<Dog>It does not have any inheritance relationships and cannot assign to each other

Covariant, contravariant is originally a mathematical concept, in Java/Kotlin mainly used in generics.

Don’t type variable

List

is not a subclass of List

. Therefore, List

cannot be assigned to List

. Note that Java arrays are covariant:



List<Dog> dogs = new ArrayList<Dog>();
List<Animal> animals = dogs; // Compile error

Dog[] dogs = new Dog[] { new Dog() };
Animal[] animals = dogs; // Compile properly
animals[0] = new Animals(); // Runtime exception
Copy the code

Therefore, we prefer to use generic collections in Java

covariance

The purpose of invariance is to ensure type safety, but it comes at the cost of making your program less flexible. Sometimes we want to pass a subclass generic object as an argument to a parameter declared as a superclass generic, for example:

public int getAnimalsCount(List<Animal> animals) {
  return animals.size();
}

List<Dog> dogs = new ArrayList<Dog>();
int dogsCount = getAnimalsCount(dogs); // Because Java generics do not change, this will compile error
Copy the code

Above, passing dogs to the getAnimalsCount method is used to count dogs. This is a particularly reasonable requirement because Java does not want such requirements to be unrealized due to immutability, so Java generics introduce covariance through wildcards:

public int getAnimalsCount(List<? extends Animal> animals) {
  return animals.size();
}
Copy the code

? Extends Animal means that this method can accept a collection of Animal or Animal subclasses, which makes generic type covariant

inverter

Similarly, sometimes we want to pass a parent generic object as an argument to a subclass generic parameter, for example

// The listener for hungry animals uses a generic interface for reuse across different animals
interface OnHungryListener<T> {
  void onHungry(T who)
}

public void observeDogsHungry(listener: OnHungryListener<Dog>) {... }// The dog hunger monitor was broken one day and I wanted to use the animal hunger monitor insteadOnHungryListener<Animal> animalHungryListener = ... ; observeDogsHungry(animalHungryListener);// Compile error
Copy the code

Because of the invariance, the above code will still compile errors, so Java also uses wildcard arguments to implement inversion:

interface OnHungryListener<? super T> {
  void onHungry(T who)
}
Copy the code

This allows us to pass a Dog parent listener to the observeDogsHungry method, so any listener of the Dog parent type can be used to hear if the Dog is hungry.

Java generic wildcards

Java uses wildcards to represent type parameters, implementing covariant and contravariant, where

  • Upper bound wildcard? extends T: qualified for type parametersceiling, the type parameter isTAnd all theTCan be assigned to a generic object of a subtype of? extend TThe generic type of
  • Lower bound wildcard? super T: qualified for type parametersThe lower limit, the type parameter isTAnd all theTCan be assigned to a generic object of the parent type? super TThe generic type of
  • Unqualified wildcard?: indicates an unrestricted type parameter. The type parameter can be any type or any type?Class, so the type parameters are generic types of any type that can be assigned to?The generic

Unqualified wildcards? Is used in relatively few scenarios, and can be used when we find that we don’t need to place any restrictions on type parameters. For example, if we implement a method that returns the size of a collection of any type, we can define it as public int getListSize(List
list);

Notice we can’t define public int getListSize(ListList) because for example List

is not a subclass of List
, But List

and any other List of any type is List<, right? > a subclass of

Covariant and contravariant properties

  • Covariant: The upper bound determines the parent class, which is read-only and not writable for generic collections
  • Contravariant: The lower bound that can be determined is the subclass, for generic sets only write not read
// Covariant, read-only, not writable
private void covariance(ArrayList<? extends Animal> animals) {
  Animal animal = animals.get(0); // Animal is the parent of any class
  animals.add(new Animal()); // Compilation error, write to need parent class or parent class subclass, if write to require subclass, parent class cannot be assigned to subclass
}
Copy the code

List.get() returns type T, so animals.get(0) returns an Animal subtype. Subclasses can be assigned to a parent class

The list.add () method takes arguments of type T. The object obtained by new Animal() is an argument of a parent type and cannot be assigned to a subtype parameter, so compilation fails

// invert, write only, not read
private void contravariance(ArrayList<? super Dog> animals) {
  Dog animal = animals.get(0); // If the subclass is a parent class, the parent class cannot be assigned to the subclass
  animals.add(new Dog()); Dog is already a lower bound on all classes. It is a subclass of any type, and subclasses can be assigned to the parent class
}
Copy the code

Since the type argument of animals can be the parent of any Dog, let’s say T is the parent of any Dog. List.get() returns a value of type T. Therefore, animes.get (0) returns a Dog object of the parent type, which cannot be assigned to subclasses

The list.add () method takes arguments of type T, and new Dog() gets a Dog object that subclasses can assign to their parent class

Joshua Bloch, author of Effective Java, 3rd Edition, calls objects that you can only read from producers and objects that you can only write to consumers. So he came up with the following mnemonic:

PECSRepresents Producer Extends and Consumer Super (producer-extends, consumer-super)

In my opinion, if you remember the principle that subclasses can be assigned to superclasses, but superclasses cannot be assigned to subclasses, combined with the upper and lower limits of generic types, you can deduce exactly when to compile

Generics in Kotlin

Kotlin’s generics can be seen as “enhanced versions” of Java generics, so as I said earlier, Kotlin’s generics become a breeze once you know Java generics

I mentioned that in Java generics are immutable, and arrays are covariant, and on Kotlin, both generics and arrays are immutable, so that makes the type safer, so I say — Kotlin generics and Java generics enhanced

Before we get into the “enhancements” of other Kotlin generics, let’s take a look at how generic deformations in Java are implemented and represented in Kotlin

deformation Representation in Java Kotlin’s representation
covariance ? extends T out T
inverter ? super T in T
Unrestricted symbol ? *

You can see that Kotlin’s use of the out and in keywords is self-explanatory, with out read-only and in write-only, which is much easier to understand than Java

So much for the Java analogies, let’s move on to something different

Declaration-site variance

Variance with declaration is use-site variance. Let’s see what these two mean:

  • Declaration deformation: Define the deformation when the generic class is declared
  • Usage of deformation: Define deformation when using generic classes
// Declaration deformation: When the class is declared, the type parameter is specified as out T, so the generic type is covariant
interface SourceA<out T> {}

// If no deformation is specified in the declaration, the type of the generic type does not change
interface SourceB<T> {}
// When using SourceB as a parameter, we specify the type parameter as out String, which makes SourceB covariant
fun useSource(source: SourceB<out String>) {}
Copy the code

In Java, deformation can only occur at the point of use, so no local deformation is declared in Java

Ponder: Why does Kotlin want to make visible transformations? When we say that Kotlin generics are enhanced versions of Java generics, we must be addressing some scenarios that Java does not support

For example, if a generic class is determined to be read-only, it is convenient to use declarative deformation instead of specifying it every time

// This generic interface method can only read, but not write, so it is declared as out
interface Source<out T> {
    fun nextT(a): T
}

// It does not need to be specified every time it is used in all places
fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // This is ok because T is an out- argument
}
Copy the code

Similarly, a good example of Comparable is Comparable:

// Interface methods have only write capabilities, so declare in
interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

// No need to specify invert
fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 has the type Double, which is a subtype of Number
    // Therefore, we can assign x to variables of type Comparable 
      
    val y: Comparable<Double> = x / / OK!
}
Copy the code

Generic constraint

Further restrictions can be placed on type parameters of generics in Kotlin. The most common type of restriction is the upper bound corresponding to Java’s extends keyword

fun <T : Comparable<T>> sort(list: List<T>){}Copy the code

The type specified after the colon is upper bound: only subtypes Comparable

can replace T. Such as:

Sort (listOf(1, 2, 3)) // OK. Type sort(listOf(HashMap<Int, String>())) HashMap<Int, String> is not a subtype of Comparable<HashMap<Int, String>>Copy the code

The default upper bound (if not declared) is Any? . Only one upper bound can be specified in Angle brackets. If multiple upper bounds are required for the same type of parameter, we need a separate WHERe-clause:

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}
Copy the code

The type passed must satisfy all the conditions of the WHERE clause. In the example above, type T must implement both CharSequence and Comparable.