The following article is from Chapter 3-Item24-consider variance for Generic Types in Effective Kotlin: Best Practices

For more on generics, see my other article, “Understanding Generics in Java/Kotlin.”

Item 24: Focus on type variations of generics

Explanatory list of nouns

English Chinese explain
type parameter Type parameters Arguments in Angle brackets in a generic, for exampleList<T>In theT
variance modifiers Type variable modifier inout
A subclass of generic Generic types whose type parameters are subclasses are referred to simply as “subclass generics” for ease of description.
The generic superclass A generic type whose type parameter is a parent. For ease of description, it is referred to simply as “parent generics”.
function type Function types Like:(T) -> U

Translator’s note: There are a lot of proper names in this paper. For the convenience of understanding and remembering, here is a comparison table of nouns

Suppose we have the following generic class:

class Cup<T>
Copy the code

The type parameter T of the generic class above does not specify any type modifiers (in or out), so it is untyped by default. Unmorphing means that there is no inheritance relationship between subclass generics and parent generics, such as: no inheritance relationship between Cup

and Cup

, Cup

and Cup< protein plant.


fun main(a) {
  val anys: Cup<Any> = Cup<Int> ()// Compile error, type mismatch
  val nothings: Cup<Nothing> = Cup<Int> ()// Error compiling
}
Copy the code

If we want them to inherit, we need to use the type-variant modifiers out and in, where out makes generics covariant and in makes generics contravariant:

class Cup<out T>
open class Dog
class Puppy: Dog(a)fun main(args: Array<String>) {
  val b: Cup<Dog> = Cup<Puppy>() // After covariant, subclass generics are subclasses of parent generics, which can be assigned to the parent
}
Copy the code
class Cup<in T>
open class Dog
class Puppy: Dog(a)fun main(args: Array<String>) {
  val b: Cup<Puppy> = Cup<Dog>() // The parent generic is a subclass of the subclass generic, and the subclass can be assigned to the parent
}
Copy the code

The following diagram illustrates the relationship between these variants:

Function types

In Kotlin, function types are also type-variant, for example:

fun printProcessedNumber(transition: (Int) - >Any) {
  print(transition(42))}Copy the code

This method takes arguments of a function type and can accept all of the following types :(Int) -> Number,(Number) -> Any, (Number) -> Number,(Any) -> Number,(Number) -> Int, and so on.

These types of inheritance relationships look like this:

From this inheritance relationship, we can see that, from the top down, parameter types move toward the higher type in the inheritance hierarchy (parent direction), while return types move toward the lower type (subclass direction)

This is not a coincidence, precisely because in Kotlin, the parameter types of all function types are contravariant and the return types of function types are covariant:

This is not the only type in Kotlin that supports variant types. A more common type that supports covariant is List (declared with the out modifier), while MutableList is non-variant.

The security of type variable modifiers

In Java, arrays are covariant, and many sources say this is so that when arrays are used as arguments, you can implement methods like sort that support the same sort logic for different types of arrays. However, array covariance presents a big problem:

Integer[] numbers = {1.4.2.1};
Object[] objects = numbers; // No problems with compiling
objects[2] = 'B'; // The compilation is fine, but ArrayStoreException is thrown at runtime
Copy the code

In Kotlin, arrays are immutable, so these problems don’t exist.

When we assign a subclass to a parent class, there is an implicit upward transition:

open class Dog
class Puppy: Dog(a)class Hound: Dog(a)fun takeDog(dog: Dot) {}

takeDog(Dog())
takeDog(Puppy())
takeDog(Hound())
Copy the code

The above code does not deal with covariance. If we put covariant type parameters in the input position, we can pass in any type to the input parameter due to covariance and upcasting, which is obviously dangerous:

class Box<out T> {
  private var value: T? = null
  
  // 1. This will compile an error. Let's suppose we allow this, and see what happens
  fun set(value: T) {
    this.value = value
  }
  
  fun get(a): T = value ? : error("value not set")}val puppyBox = Box<Puppy>() // I am a box used to put puppy
val dogBox: Box<Dog> = puppyBox // puppyBox is transformed upward into dogBox, but I am still the box used to put puppy
dogBox.set(Hound()) Hound is a subclass of Dog, so it can also be passed in normally. However, I originally put a puppy in it, but now you put a Hound in me.

// For an even more outrageous example
val dogHouse = Box<Dog>() // I am a dog house
val box: Box<Any> = dogHouse // Upward transition to Box
      
       , but I'm still a dog house
      
box.set("some string") // String is a subclass of Any and can be passed in, but I am a dog dog, you threw a String in me!
box.set(42) // Int is also a subclass of Any, can be passed, outrageous, I am a dog, you give me an Int
Copy the code

Therefore, to avoid this, Kotlin disallows this behavior at compile time: Kotlin disallows covariant type arguments in public input positions:

class Box<out T> {
  var value: T? = null // Error compiling
  
  fun set(value: T) { // Error compiling
    this.value = value 
  }
}
Copy the code

This works if the access modifier is changed to private:

class Box<out T> {
  private var value: T? = null 
  
  private fun set(value: T) { 
    this.value = value 
  }
}
Copy the code

Covariant type parameters are usually only used to expose read methods as consumers. A good example is List

in Kotlin. In Kotlin, List provides only readable methods, so List is declared covariant (using out).

Contravariant type parameters also have problems if placed in public output positions:

open calss Car
interface Boat
class Amphibious: Car(), Boat 

class Box<in T>(
  // 1. This will compile an error. Let's assume this is allowed and see what happens
  val value: T 
)

val garage: Box<Car> = Box(Car()) // I am a garage
val amphibiousSpot: Box<Amphibious> = garage // Since it supports contravariant, this can be assigned to the subclass generic
val boat: Boat = garage.value // If 1 is supported, this assignment is also supported, but I am clearly a garage, I can not provide boats!

// An even more outrageous example
val noSpot: Box<Nothing> = Box<Car>(Card()) // I am a garage, but FIRST I transform into a nothingness
val noting: Noting = noSpot.value // I can't provide a nothing!
Copy the code

Therefore, to avoid this, Kotlin disallows this behavior at compile time: Kotlin disallows the use of contravariant type arguments in public output locations:

class Box<in T> {
  var value: T? = null // Error compiling
  
  fun get(a): T = value ? : error("value not set") // Error compiling
}
Copy the code

Similarly, change it to private and the code will not be repeated

This is consistent with PECS in Java:

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)

Type changes the position of the modifier

Type modifiers can operate in two positions: declaring and using type modifiers. Declaring local variations applies everywhere the generic is used, and using local variations gives us more flexibility in controlling which variations we need.

For example, for some generics that only have readable methods, we can use declarative modifier, such as List

, but for examples like MutableList that can be both written and read, it is more suitable to use the modifier to define which type modifier we need.

In Java, only local variants are used

conclusion

Kotlin has powerful generic types and supports the use of declared and local variants

  • The default type parameters are not typed
  • outModifiers can covariant type parameters
  • inModifiers invert type parameters

In the Kotlin

  • ListSetIs covariant,MutableList.MutableSet.MutableMapIt is unchangeable
  • The parameter types of function types are contravariant, and the return types of function types are covariant
  • Covariant type parameters are read-only and not writable
  • The inverter type parameter is only write but not read