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 EDefines an upper bound that represents the type parameterEA subclass of
  • ? super EDefines a lower bound that represents the type parameterEThe 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>Covariant
  • List<? 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
, but in Java you must declare the Object of type Source
, which makes no sense, so the out and in modifiers are officially provided to explain the situation to the compiler.

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 membersoutlocation
  • Not available on class membersinlocation
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.

  • forFoo<T>Of the definition of,Foo<*>Is equivalent toFoo<out Any? >
  • forFoo<out T>.TIs a covariant type parameter,Foo<*>Is equivalent toFoo<out Any? >.Because of the fact thatoutThe projection is redundant and corresponds to the same variances of type parameters, so it can also be equivalent to Foo<Any? >
  • forFoo<in T>.TIs an inverse type parameter,Foo<*>Is equivalent toFoo<in Nothing>Foo<Nothing>, this will not writeFoo<*>Because theTWhen unknown, there is no safe way to write
  • forFoo<T : TUpper>.TIt’s an upper boundTUpperType invariable type parameter,Foo<*>It’s equivalent to reading the valueFoo<out TUpper>For writing values, is equivalent toFoo<in Nothing>
  • forFoo<out T : TUpper>.TIt’s an upper boundTUpperCovariant 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)