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 isT
And all theT
Can be assigned to a generic object of a subtype of? extend T
The generic type of - Lower bound wildcard
? super T
: qualified for type parametersThe lower limit, the type parameter isT
And all theT
Can be assigned to a generic object of the parent type? super T
The 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(List
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.