This post will focus on some of the issues with Kotlin generics, with Java generics in between.

1. Generic type parameters

1.1 the form

We use generics in the form of classes, excuses, and methods. Let’s look at two examples.

1.2 Declare generic classes

As in Java, we declare generic classes and generic interfaces by adding a pair of <> after the class name and placing type parameters inside <>.

Once declared, we can use type parameters inside our classes and interfaces just like any other type.

Let’s take a look at how the standard Java interface List is declared using Kotlin. Definition of the List interface in Kotlin.


public interface List<out E> : Collection<E> {
  
    override val size: Int

    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    public operator fun get(index: Int): E
    public fun indexOf(element: @UnsafeVariance E): Int
    public fun lastIndexOf(element: @UnsafeVariance E): Int
    public fun listIterator(): ListIterator<E>
    public fun listIterator(index: Int): ListIterator<E>
    public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

Copy the code

If your class inherits a generic class (or implements a generic interface), you must provide a type argument to the underlying type’s generics, which can be a concrete type or another type parameter.

class StringList : List<String>{
	override fun get(index:Int):String =...
}
Copy the code

A simple generic class definition:

class Pair<K, V>(key: K, value: V) {
    var key: K = key
    var value = value
}

Copy the code

1.3 Generic method Definition:

fun <T : Any> getClassName(clzObj: T): String {
    return clzObj.javaClass.simpleName
}
Copy the code

The generic T is declared before the method name.

The generic T is nullable by default, and we qualified T to inherit Any to prevent clzObj from being null.

1.4 Differences from Java generics

(1) Unlike Java, Kotlin always requires that type arguments be either explicitly declared or can be pushed out by the compiler.

val readers : MutableList<String> = mutableListOf()
val readers = mutableListOf<String> ()Copy the code

These two lines of code are equivalent.

(2) Kotlin does not support native types

Kotlin has generics from the start, so it does not support primitive types and type arguments must be defined.


2. Type parameter constraints

2.1 T upper bound specified

If a type is specified as an upper bound constraint on the generic type parameter, the corresponding generic argument type must be that type or its subtype when the generic type is initialized.

The form T extends Number in Java

The form in Kotlin: T: Number


fun <T : Number> List<T>.sum():T{
	/ /...
}
Copy the code

Once you specify an upper bound, you can use T as its upper bound type.

In the example above, we can use T as the Number type.

2.2 Specifying multiple constraints for a type parameter

fun <T> ensureTrailingPeriod(seq: T)
        where T : CharSequence, T : Appendable {
    / /...
}
Copy the code

The above example specifies that types that can be used as type arguments must implement the CharSequence and Appendable interfaces.

2.3 Make type parameters non-empty

In fact, type arguments that do not specify an upper bound will use Any? The default upper bound.

Take a look at this example:

class Processor<T> {
    fun process(value: T) { println(value? .hashCode()) } }Copy the code

In the process method, value is nullable, even though T does not use any? The tag.

If you want to specify that the type parameter is non-empty at all times, you can do so by specifying a constraint.

If you have no restrictions other than controllability, you can use Any instead of the default Any. As its upper bound.

class Processor<T : Any> {
    fun process(value: T) {
        println(value.hashCode())
    }
}
Copy the code

Note: the String? It’s not a subtype of Any, it’s Any, right? Subtype of; String is a subtype of Any.


3. Runtime generics

We know that generics on the JVM are generally implemented by type erasures, so they are also called pseudo-generics, meaning that type arguments are not saved at run time.

In fact, we could declare an inline function so that its type arguments are not erased, but this is not possible in Java.

3.1 Type Erasure

As with Java, Kotlin’s generics are erased at run time, meaning that the instance does not carry information about the type arguments used to create it.

For example, if you create a List and store a bunch of strings in it, you can only see a List at runtime, and you can’t tell what kind of elements the List was intended to contain.

It seems that type erasure is not safe, but when we code, the compiler knows the type of the type argument, making sure that you can only add elements of the appropriate type.

The benefit of type erasure is memory savings.

3.1.1 is

Because of type erasure, it is generally not possible to use the type of a type argument in an IS check. In Java, the instanceof operation cannot be performed on a deterministic generic.

Kotlin does not allow the use of generic types that do not specify type arguments. Then you might be wondering how to check if a value is a list, rather than a Set or other object, using special asterisk projection syntax to do this check.

fun <T : Any> process(value: T) {
    if (value is List<String>) {
        // error
    }

    if (value is List<*>) {
        // ok
    }

    var list = listOf<Int>(1.2.3)
    if (list is List<Int>) {
        // ok}}Copy the code

Kotlin’s compiler is smart enough to allow IS checking if the corresponding type information is known at compile time.

List<*> is the Java equivalent of List<? >, a generic type with an argument of unknown type. The above example simply checks if the value is a List and does not get any information about its element type.

3.1.2 the as/as?

In the as and as? We can still use the generic type in the transformation.

(1) If the class has the correct underlying type but the type arguments are wrong, the conversion will not fail because the type arguments are unknown at runtime (because they are erased), but a ClassCastException may follow.

(2) If the base type of this type is not correct, as? A null value is returned.


fun main(args: Array<String>) {
    var list: List<Int> = listOf(1.2.3)
    printSum(list)/ / 6

    var strList: List<String> = listOf("1"."2"."3")
    printSum(strList)// ClassCastException

    var intSet: Set<Int> = setOf(1.2.3)
    printSum(intSet) // IllegalStateException
}

fun printSum(c: Collection<*>) {
    val intList = c as? List<Int> ? :throw IllegalStateException("List is expected")
    println(intList.sum())
}

Copy the code

3.2 Implement type parameter functions

The type arguments of generic functions are also erased at run time. There is only one special case: inline functions, whose type parameters can be instantiated, meaning that at run time you can refer to the actual type arguments.


// compile error
fun <T> isA(value: Any) = value is T 

Copy the code

3.2.1 the inline

If you use an inline tag function, the compiler replaces each function call with the actual code of the function.

Lambda’s code is also inlined and no anonymous inner classes are created.

Another scenario where inline functions come into their own: their type parameters can be implemented.

3.2.2 Implement parameter functions

inline fun <reified T> isA(value: Any) = value is T

Copy the code

A practical example is filterIsInstance:


public inline fun <reified R> Iterable<*>.filterIsInstance(): List<@kotlin.internal.NoInfer R> {
    return filterIsInstanceTo(ArrayList<R>())
}

public inline fun <reified R, C : MutableCollection<in R>> Iterable<*>.filterIsInstanceTo(destination: C): C {
    for (element in this) if (element is R) destination.add(element)
    return destination
}

Copy the code

Simplify the Android startActivity method:

inline fun <reified T : Activity> Context.startActivity() {
    val intent = Intent(this,T::class.java)
    startActivity(intent)
}

Copy the code

3.2.2 Why does compaction only work for inline functions

How does this work? Why is it ok to write element is R in an inline function but not in a normal one?

As described earlier, the compiler inserts bytecode implementing the inline line number into the place where each call occurs. Each time you call an inline method with an instantiated type parameter, the compiler knows the exact type used as the type argument in that particular call, so the compiler can generate bytecode referencing the actual type.

Inline functions that implement type parameters cannot be called in Java, whereas normal inline functions can be called in Java.


4. Variations: Generics and subtyping

4.1 Subclasses and subtypes

Int is a subclass of Number.

Int is an Int, right? Type, which all correspond to the Int class.

A non-empty type is a subtype of its non-empty version, but they all correspond to the same class.

List is a class; List\List is a type.

It is important to understand subtypes and subtypes.

4.2 not to distort

A generic class, such as MutableList– if for any two types A and B, Mutable is neither A Mutable parent nor its parent, then A generic class is said to be unmutated in that type argument.

All classes in Java are immutable.

4.3 Covariant: Subtyping is retained

A covariant class is A generic class (we use the Poducer class as an example). If A is A subtype of B, then Producer is A subtype of Producer; We say that subtyping is preserved.

In Kotlin, to declare that a type parameter is covariant, prefix the name of the type parameter with the out keyword:

interface Producer<out T> {
    fun produce(): T
}

Copy the code

An immutable example:

open class Animal { open fun feed() { println("Animal is feeding.") } } class Herd<T : Animal> { val size: Int = 10 fun get(index: Int): Animal? { return null } } fun feedAllAnimal(animals: Herd<Animal>) { for (i in 0 until animals.size) { animals.get(i)? .feed() } } class Cat : Animal() { override fun feed() { println("Cat is feeding.") } } fun takeCareOfCats(cats: Herd<Cat>) { feedAllAnimal(cats)//comile error }Copy the code

Unfortunately, in the example above we cannot treat the cat herd as an animal herd.

A generic class is invariant on type parameters that do not use any wildcards.

So how can we make the Herd be cared for as a Herd? Simply modify the Herd as follows:

class Herd<out T : Animal> {
    val size: Int = 10
    fun get(index: Int): Animal? {
        return null
    }
}

Copy the code

in & out

The use of type parameters in the member declaration of a class can be divided into in and out positions.

If the function takes T as its return type, we say it’s in out position.

If T is used as a function parameter type, it is in position.

The out keyword in front of a class type argument requires that all methods that use T place T only in the out position, not in position.

Now let’s think about, can we declare T in MutableList covariant?

The answer is no, because you can either add T to a MutableList, or you can take T from a MutableList, so T appears in both out and in positions.

In the above analysis, we have seen the definition of the List interface, which is read-only in Kotlin.

public interface List<out E> : Collection<E>{
	//....
}

Copy the code

Let’s look at the definitions of List and MutableList:

public interface MutableList<E> : List<E>, MutableCollection<E>{
	override fun add(element: E): Boolean
	public fun removeAt(index: Int): E
}

Copy the code

The definition of the method validates our previous analysis that the type parameter E of the MutableList cannot be declared either out E or in E.

The constructor parameter is neither in nor out, and even if the type parameter is declared out, we can still use it in the constructor parameter declaration.

For the var attribute of a class, we cannot use out to modify the type parameter of the attribute. This is because setter methods for properties use type parameters in the IN position and getter methods use type parameters in the out position.

Note that the location rule only overrides the public\protected\internal API outside the class. Private method parameters are neither out nor in.


// compile error
class Herd<out T : Animal>(var leadAnimal: T) {
    
}

// ok
class Herd<out T : Animal>(private var leadAnimal: T) {
    
}

Copy the code

4.4 Reversing the subtyping relationship

The notion of contravariance can be thought of as a mirror image of covariation: for a contravariant class, the typing relationship is the opposite of the typing relationship of the class arguments of the type.

The contravariant class is A generic class (let’s take Consumer as an example). If B is A subtype of A, then Consumer is A subtype of Consumer. Types A and B switch positions, so we say that subtyping is reversed.

The keyword corresponding to inverse is IN.

4.5 Point variations: Specify variations where types occur

fun <T : R, R> copyTo(source: MutableList<out T>, destination: MutableList<in T>) {
    source.forEach { item -> destination.add(item) }
}

Copy the code

We say that source is not a normal MutableList, but a projected (limited) MutableList that can only call methods whose return type is a generic parameter.

MutableList in Kotlin and MutableList in Java <? Extends T> has one meaning.

MutableList in Kotlin and MutableList in Java <? Super T> means the same thing.

The projection of MutableList<*> is MutableList<out Any? >.

MyType<*> in Kotlin corresponds to MyType<? >.

5. To summarize

Kotlin’s generics and Java generics are very much the same, but in a different way, so we’ll compare them to learn more about them.

I wish you all a happy job.