The need for generics

[1.1] Before generics

Before we explain why generics exist, let’s look at a piece of code. Right

List AList = new ArrayList(); A.dd (new B()); A A = (A) a.get (0);Copy the code

This code is now rarely seen. But in fact, prior to Java1.5, this was very often written code, and very error-prone code. In the above code, we declare a List of unknown types to store. Although we use the variable name “AList” to indicate that the List is stored, take A collection of type A. But we can still store objects of type B. And when we get it out, we have to do a type cast. This raises two questions:

  1. We can’t limit the type of input at the time of storage. Possible saving to other types causes CastClassException.
  2. When the set element comes out, we know it’s of type A, but we still have to do A strong cast every time.

The fundamental reason for this problem is that ArrayList() is implemented using Object[] at the bottom. This is intended to make ArrayList more generic, applicable to all types.

[1.2] With generics

After understanding the requirements and pain points above, we can naturally think of generics. It can parameterize types. After the introduction of generics. We can write the above code like this:

List<A> AList = new ArrayList(); // Failed to compile. A.add(new B()); A = a.get (0);Copy the code

As you can see, with the introduction of generics, type checking can be done at compile time. But the underlying implementation of ArrayList uses Object[], so why not use type coercion? We can look at the arraylist.get () method:

ArrayList.java

transient Object[] elementData;
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

E elementData(int index) {
    return(E) elementData[index]; }}Copy the code

[1.3] Summary of the need for generics

Here, we summarize the benefits of introducing generics:

  1. Type safety: The compiler can report type error access at compile time.
  2. Parameterization of types allows you to write more generic code.
  3. Simplify code.
  4. Type conversion can be performed automatically, and data can be obtained without type coercion

Implementation of generics: type erasure

[2.1] The actual implementation of generics

In fact, we talked a little bit about the implementation behind generics above. To get a better sense of how he implements generics through type erasers, let’s take a look at bytecode:

// ArrayList list = new ArrayList(); // add A generic ArrayList<A> AList = new ArrayList();Copy the code

Byte code:

//ArrayList list = new ArrayList();
0:  new               # 2
3:  dup
4:  invokespecial     # 3
7:  astore+1
//ArrayList<A> AList = new ArrayList();
8:  new               # 2
11: dup          
12: invokespecial     # 3
15: astore_2
Copy the code

As you can see, the bytecode of an ArrayList is the same with or without generics. So how does it guarantee the properties we talked about in generics? Type checking can be done by compiler checking. Automatic type conversion, as we have seen above, is achieved through strong conversions within generic classes.

[2.2] Why use type erasure to implement generics?

Now that we know how generics are implemented, let’s turn around and think about why Java uses type erasers to implement generics. The answer is backward compatibility. We know that backward compatibility is a key feature of Java, and before Java1.5, before generics, there was a lot of code like this:

ArrayList list = new ArrayList();
Copy the code

The type erasing method implements generics, and we can see that the compiled bytecode is the same as before 1.5, which is completely compatible. Some features of generics are then implemented through compilers and modifications to existing collection framework classes. Kotlin claims to be fully Java compatible, so of course Kotlin’s generic implementation is the same as Java.

[2.3] Generic type fetching

From the above we know that in order to improve the generality of the code, we use generics to parameterize the types, erasing differences between types. But in the process of coding, we often need to get the object type at runtime, and after type erasure, the generic class has lost the information of the type parameter, so there is no way to get the type parameter at runtime. Maybe we can get it manually. The specific code is as follows:

open class A<T>(val data: T, val clazz: Class<T>) {

    fun getType() {
        println(clazz)
    }

}

Copy the code

Summary: Getting generic type parameters in this way is a bit tricky, and it can’t get a generic type. Such as:

Class clazz = ArrayList<String>.classCopy the code

Is there a way to get a generic type? Yes:

[2.3.1] Use anonymous inner classes to get generic types
val listA = new ArryaList<A>() val listA2 = object : ArrayList < A > () {} println (listA. JavaClass. GenericSuperclass) println (lstA2. JavaClass. GenericSuperclass) / / print:  java.util.AbstractList<E> java.util.ArrayList<java.lang.String>Copy the code

Summary: We found that the second way we can get what type list is. The second one declares an anonymous inner class. But why does an anonymous inner class get the type of an LIS generic parameter? Type erasure does not actually erase all type information, but puts the type information in the constant pool of the appropriate class.

So we can try to design generic classes that get all types of information.

open class GenericsToken<T> {
    var type: Type = Any::class.java
    init {
        val superClass = this.javaClass.gnericSuperclass
        type = superClass as ParameterizedType).getActualTypeArguments()[0]
    }
}


fun test() { val gt = object : GenericsToken<Map<String, String>>(){} println(gt.type)} java.util.map <java.lang.String,? extends java.lang.String>Copy the code

Summary: When an anonymous inner class is initialized, it binds information about the parent class or interface of the parent class, so that we can get the desired generic type by getting information about the generic type of the parent class or interface of the parent. In fact, the commonly used Gson framework is also obtained in this way.

val json = new Json("...")
val type = object : TypeToken<List<String>>(){}.type
val stringList = Gson().fromJson<List<String>>(json.type)
Copy the code
[2.3.2] Get a generic type using Kotlin’s reified keyword

We know that Kotlin’s inline functions are inserted at compile time, and the compiler inserts the bytecode of the inline function directly into the call, so parameter types are also inserted into the bytecode. Getting the parameter type of a generic in an inline function is as simple as adding the reified keyword.

inline fun <reified T> getType(): T {
    return T::class.java
}
Copy the code

Type constraint.

When we talked about generics earlier, one of the features we talked about was type safety, which means that generics themselves are type binding. So what do I mean by type constraints here? It’s a constraint on generics. In Java we see the following code:

class Test<T extends B> {
...    
}
Copy the code

It is constrained that the generic must be a subclass of B by adding extends B after T. So in Kotlin, inheritance is represented by:, so Kotlin’s generic constraints are as follows:

class Test<T: B>{
    
}
Copy the code

But what if we need multiple constraints? You can use the WHERE keyword in Kotlin to implement this requirement as follows:

class Test<T> where T: A, T: B{

}
Copy the code

Using the WHERE keyword, we can constrain that the generic T must be A subclass of A and B.

4. Deformation of generics: covariant and contravariant

[4.1] Covariant

Handout: If type A is A subtype of type B, then Generic is also A subtype of Generic, which is covariant. In Kotlin, we implement this relationship by adding the out keyword to the generic parameter of a generic class or method. As follows:

Open class Flower class WhiteFlower:Flower(){}
class ReaFlower: Flower(){// user interface Product<out T> {fun produce(): T} class WhiteFlowerProduct<WhiteFlower> {override fun produce(): WhiteFlower {returnWhileFlower(); Val <Flower> = WhiteFLowerProduct()Copy the code

Summary: You can see that WhiteFLowerProduct() can be assigned to a Product variable because the covariant relationship is specified through out. And we see that generic types are produced as return types. So what if we add an object of generic type? As follows:

Interface Product<out T> {fun produce(): T } class WhiteFlowerProduct<WhiteFlower> {override fun produce(): WhiteFlower {return WhileFlower();
    }
    
    override fun add(flower: WhiteFlower){
       returnWhileFlower(); }}Copy the code

Type parameter T is declare as ‘out’ but occurs in’ in’ position in Type T. The type T declared as out cannot appear in the input position. The ‘out’ keyword also tells us that the generic it modifies can only be output as a producer, not input as a consumer. So ‘out’ -modified generics are often used as method returns. That’s the limitation of covariation. So what is the covariance that can’t be entered. We can use contradiction to understand: if you could add, what would happen?

Val flowerProduct: Product<Flower> = WhiteFLowerProduct() flowerProduct.add(ReaFlower())Copy the code

In Java, the corresponding generic covariant is defined as:
But this inconvenient generic covariant definition has been modified in Kotlin with the out keyword to better reflect its covariant read-only and unwritable nature.

[4.2] Inverter

Definition: If type A is A subtype of type B and in turn Generic<B> is A subtype of Generic<A>, we call this relationship contravariant. In Kotlin, we declare contravariant generics with the ‘in’ keyword. Here’s an example:

val numberComparator = Comparator<Number> { n1, N2 -> n1.toDouble.compareTo(n2.todouble ())} val daoubleList = mutableListOf(2.0, 3.0) SortWith (numberComparator) val intList = mutableListof(1, 2) We're still using the Number Comparator intList.sortWith(numberComparator) // and you can see that we're using it for the generic TinThe keyword. public fun <T> MutableList<T>.sortWith(comparator: Comparator<in T>): Unit {
    if (size > 1) java.util.Collections.sort(this, comparator)
}
Copy the code

Double and Int are subclasses of Number. With the in modifier, Comparator becomes a subclass of Comparator and Comparator. So you can assign a Comparator to a Comparator and a Comparator. Therefore, there is no need to define different DoubleComparator, IntComparatort and so on according to different data types. Again, it’s known by its name ‘in’. In-modified generics can only be used as input types, not return types. In Java it corresponds to <? Super T >.

[4.3] Summary

is covariance inverter The same
Kotlin Implementation: only as a consumer, can only read not write The implementation mode can only be added, and the access is limited Implementation: readable and writable
Java Implementation: <? Extends T> acts only as a consumer, reading but not writing Implementation mode <? Super T> can only be added and access is restricted Implementation: readable and writable

thanks

When I read “Kotlin Core Programming” in the early days, I always thought Kotlin’s generics were well explained in the book, so I always wanted to write a blog about it, which can be regarded as reading notes. The article has not the place, welcome the message points out. Thank you very much!