Kotlin generic base
Generics allow us to declare type parameters in code. The basic use of Kotlin generics is the same as in Java: you can declare them on classes and functions, and the usage is similar.
- A type that can be declared as a parameter or return value on a function that is a generic function
- When declared on a class, it can be used in any type declaration. The class is generic
class GenericsDemo<T>(t: T) {
val value = t
}
fun <T> invoke(t: T) : T {
return t
}
Copy the code
We can declare a generic method in a class that declares type parameters, but if the inner method declares the same type parameter name as the class, the type parameters declared on the class will be overridden. The following code does not return an error and prints a Hello string.
class GenericsDemo<T>() {
fun <T> invoke(t: T) : T {
return t
}
}
val demo = GenericsDemo<Int>()
println(demo.invoke("Hello"))
Copy the code
In addition, we know that we can override methods with the same name in a class, but this does not work in generics, and an error will be reported if the class has the following two methods.
class GenericsDemo<T>() {
// Generics come from classes
fun invoke(t: T) : T {
return t
}
// Generics come from the method itself
fun <S> invoke(s: S) : S {
return s
}
}
Copy the code
The error is due to the fact that both methods have the same signature, which means that the JVM looks at both methods with the same name and parameters.
Platform declaration clash: The following declarations have the same JVM signature (invoke(Ljava/lang/Object;) Ljava/lang/Object;) :
The reason the appeal parameter type override is the same as the overload signature is that the type parameters are erased after the.class file is compiled.
Generic erasure
Both Java and Kotlin’s generics are pseudo-generics. The type safety checks performed by generics are only performed by the compiler, and the type parameters are removed when they enter the JVM. No information about the type parameters is retained at runtime.
Since generics were introduced in JDK 1.5, generic erasures are used to remove runtime type parameters for compatibility with previous releases
When a generic erase occurs, any type parameter erased is replaced with Object, which is why the invoke() signature above is invoke(Ljava/lang/Object;). Ljava/lang/Object; .
If the error comes from IDEA, it might prompt the @jVMName annotation to handle the problem, which changes the name of the method when compiled into bytecode.
@JvmName("invoke1")
fun <S> invoke(s: S) : S {
return s
}
Copy the code
Using the IDEA > Tools > Kotlin > Decompile Kotlin to Java tool, you can see the decomcompiled Java code as follows.
public final class GenericsDemo {
public final Object invoke(Object t) {
return t;
}
@JvmName( name = "invoke1" )
public final Object invoke1(Object s) {
returns; }}Copy the code
Generic constraint
When declaring type parameters in Java, you can use the extends keyword to specify a generic upper bound, which is specified in Kotlin as follows:
class Demo<T : Number>() {... }Copy the code
If not specified in Kotlin, then there is a default upper bound, Any? We can specify only one upper bound in Angle brackets. If multiple upper bounds are required for type parameters, a separate WHERE clause can be used.
Also, if type parameters have more than one constraint, they all need to be placed in the ‘where’ clause.
class Demo<T> where T : CharSequence.T : Comparable<T>
Copy the code
Covariance and contravariance
Type variation refers to the correlation of subtype relations determined by complex types (composite types) according to the subtype relations of the constituent types
Order relation: subtype <= base type
- Covariance: order relationships of subtypes are maintained
- Contravariance: Reverses the order relation of subtypes
- Invariance: There is no relationship of subtypes
Generic classes are complex types composed of multiple types and are product types in algebraic data types (ADT). At compile time, generic classes represent different types when specifying different type parameters, such as List
and List
, which are not of the same type.
Java and Kotlin’s simple generics are immutable, that is, List
is not a subtype of List
, and the following operation will raise an error at compile time.
List<Integer> integerList = new ArrayList<>();
List<Number> numberList = integerCollection; // error
Copy the code
The reason for this error is a List that can hold objects of type Number, either Integer or Double, because these are subclasses of Number, If we can assign List
to List
, that means we can put an object of type Double inside List
, which will cause a ClassCastException, Therefore List
cannot be used as a superclass of List
.
Java usage changes
If we want Java generics to support type variation, we need to use wildcard type arguments:
? extends E
Defines an upper bound that represents the type parameterE
A subclass of? super E
Defines a lower bound that represents the type parameterE
The superclass
? Extends E is different from specifying E directly. List
specifies that a List contains a subtype of Number. Java does not know which subtype List is. The extends E type cannot be used as a method parameter type. (We cannot pass in a capture of? The type of extends Number), but Number can be the return value type.
List
is not a List
superclass because List
Can’t add elements using add, so you don’t have to worry about adding objects of other subtypes to the List
container. Then you don’t have to worry about creating a ClassCastException, so List
can be used as a superclass of List
.
List<Integer> integerList = new ArrayList<>();
// List
is a superclass that belongs to List
List<? extends Number> list1 = integerList;
list1.add(1) // error
Number number = list1.get(0);
Copy the code
As for? Super Integer, Integer can be cast up to any parent class, so it can be used as the parameter type of a method. But because you can’t determine the wildcard, right? Which parent class does it represent? There’s always a risk of going down, so it can’t take Integer as the type of the return value, but if we try to get capture of? Super Number returns an Object of type value, and you get an Object, because Object is the superclass of all classes.
This represents a List
allows you to add Integer objects but only get Object objects, so it can be used as a superclass of List
.
List<Number> numberList = new ArrayList<>();
// List
A superclass that belongs to List
List<? super Integer> list2 = numberList;
list2.add(1);
// Object is the superclass of all classes, which can be used as capture of? Super Number type
Object object = list2.get(0);
Copy the code
The way that Java supports type variation with wildcard types when using type parameters is called using char variants.
List<? extends Number>
Can be used as aList<Integer>
Super class, saidList<? extends Number>
是CovariantList<? super Integer>
Can be used as aList<Number>
The superclass is calledList<? super Integer>
是Contravariant
In Effective Java, Joshua Bloch calls objects that can only be read from them producers, and objects that can only be written to them consumers.
The following mnemonics are proposed: Producer Extends, Consumer Super PECS (Producer Extends, Consumer Super)
Kotlin’s declaration changes
interface Source<T> {
T nextT(a);
}
Copy the code
Kotlin’s team considered it safe to store Source
instance references in variables of type Source
The type parameters of the OUT annotation will only be returned from the members of the class, that is, as production, not consumption.
- Can be used for class members
out
location - Not available on class members
in
location
class OutDemo<out T>(t: T) {
// As a producer
fun get(a): T = t
// The method argument is' in 'position, i.e. producer position
// error: the type parameter T is' out 'and cannot appear in the' in 'position
fun set(t: T) {
this.t = t
}
}
Copy the code
The OUT modifier allows type parameters to be convariant
interface Source<out T> {
fun nextT(a): T
}
val source : Source<Any> = Source<String>() // success
Copy the code
The in modifier can make type arguments contravariant. In can only be consumed, not produced. Super E corresponds
class InDemo<in T>(t: T) {
// error: the type parameter T is' in 'and cannot appear in the' out 'position
fun get(a): T = t
// As a consumer
fun set(t: T) {
this.t = t
}
}
Copy the code
In addition, member variables provide getters and setters to make them externally readable/writable (with consumption or production capabilities), so generic members of type parameters decorated with out or in need to be declared private
Out and in are called type-variant annotations, and because they are provided at the type declaration, they are called declaration-in-type annotations.
Types of projection
Declaring a generic variant would suffice for most purposes, but there are classes whose type parameters we cannot limit to consume or return only, such as ArrayList, which needs to have both consumption and production.
Kotlin, in addition to providing a declaration of local variations, also preserves the use of local variations, called type projections. We can declare variables using out and in, which is different from the Java? Extends and E? Super E corresponds.
val intList = ArrayList<Int> ()var outList: ArrayList<out Number> = ArrayList()
var inList: ArrayList<in Int> = ArrayList()
outList = intList
inList = intList
Copy the code
In addition to being used as a modification, the projection also allows us to ensure that the method does not do bad things to the object that the parameter receives. Here is an example from Kotlin’s website:
Copying the data from one Array object to another, and marking the Array parameter type as OUT in the method parameter, prevents the elements in the form from being modified, ensuring the security of the data in the original Array object.
fun copy(from: Array<out Any>, to: Array<Any>) { …… }
Copy the code
Star projection
The star projection syntax allows us to safely use it even when we are unsure of generic parameters.
class Foo<T>
/ / star projection
val foo: Foo<*> = Foo<Number>()
Copy the code
Each concrete instance of the generic type will be a subtype of the star projection that defines the generic type.
- for
Foo<T>
Of the definition of,Foo<*>
Is equivalent toFoo<out Any? >
- for
Foo<out T>
.T
Is a covariant type parameter,Foo<*>
Is equivalent toFoo<out Any? >
.Because of the fact thatout
The projection is redundant and corresponds to the same variances of type parameters, so it can also be equivalent to Foo<Any? > - for
Foo<in T>
.T
Is an inverse type parameter,Foo<*>
Is equivalent toFoo<in Nothing>
或Foo<Nothing>
, this will not writeFoo<*>
Because theT
When unknown, there is no safe way to write - for
Foo<T : TUpper>
.T
It’s an upper boundTUpper
Type invariable type parameter,Foo<*>
It’s equivalent to reading the valueFoo<out TUpper>
For writing values, is equivalent toFoo<in Nothing>
- for
Foo<out T : TUpper>
.T
It’s an upper boundTUpper
Covariant type parameter of,Foo<*>
Is equivalent toFoo<out TUpper>
或Foo<TUpper>
If a generic type has more than one type parameter, each type parameter can be projected separately.
Specify type parameters
We know that when the JVM runs, generics are erased and all places where generics are used are replaced with Objects, so there is no way to use generics as a concrete type
However, inline functions in Kotlin can copy and replace the code of the function body with the corresponding call location, and Kotlin provides the reified keyword to externalize the type arguments of the inline function.
inline fun <reified T> nameOf(a): String = T::class.java.name
fun main(a) {
println(nameOf<Int> ())// java.lang.Integer
}
Copy the code
When we look at the decomcompiled code, we can see that the main() method calls nameOf() are replaced with code inside the nameOf() function, and the use of generics is replaced with actual types that match the above.
// Simplify the code, leaving the parts of concern
public static final void main(a) {
// The generic T is replaced with Integer
String var1 = Integer.class.getName();
System.out.println(var2);
}
public static final String nameOf(a) {
// The generic T is replaced with Object
String var1 = ((Class)Object.class).getName();
return (String)var1;
}
Copy the code
Specifying type parameters allows us to use generics as if they were a concrete type in our functions, enabling us to write more elegant code using type judgments, type conversions, and so on.
if (p isT) {... }// This is supported when type parameters are specified
// @suppress ("UNCHECKED_CAST") is not required to ignore conversion warnings
return p as T
Copy the code
reference
- Generics: in, out, where – Kotlin 中文 网 (kotlincn.net)
- Inline functions and externalized type arguments
- Generics: in, out, where | Kotlin (kotlinlang.org)
- Inline functions | Kotlin (kotlinlang.org)
- What is the reason why Java can’t implement true generics? | RednaxelaFX answer – zhihu (zhihu.com)