This article is simultaneously published on my wechat official account. You can follow it by scanning the QR code at the bottom of the article or searching Nanchen on wechat

primers

Nan Chen has not written an article for a long time. In fact, he has been thinking about many problems, how to write valuable words for everyone. After reviewing the styles of several new influencer bloggers, I think it’s time for me to start a new chapter. Yes, that’s literacy.

Today, we bring you our first literacy article: Kotlin’s Generics.

I believe there are always a lot of students, always complaining about generic no matter how to learn, just stay in a simple use of the level, so has been suffering for this.

Kotlin, as a language that can interact with Java, naturally supports generics, but Kotlin’s new keywords in and out have always confused some people, because Java’s generics fundamentals are not solid enough.

Many students always have these questions:

  • What is the difference between Kotlin generics and Java generics?
  • What exactly is the point of Java generics?
  • What exactly does Java type erasure mean?
  • What is the difference between upper bound, lower bound, and wildcard characters of Java generics? Can they implement multiple restrictions?
  • The Java<? extends T>,<? super T>,<? >What does that correspond to? What are the usage scenarios?
  • The Kotlinin,out,*,whereWhat’s the magic?
  • What is a generic approach?

Today, with an article for you to remove the above doubts.

Generics: The edge of type safety

As you know, generics were not a concept in Java prior to 1.5. Back then, a List was just a collection that could hold everything. So it’s inevitable to write code like this:

List list = new ArrayList();
list.add(1);
list.add("nanchen2251");
String str = (String) list.get(0);
Copy the code

The above code compiles without any problems, but it does run with the usual ClassCastException:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Copy the code

The experience is terrible, and what we really need is to catch bugs as they compile, not release them into production.

If we added generics to the code above, we would see obvious errors at compile time.

List<String> list = new ArrayList<>();
list.add(1);
👆 error Required type:String but Provided:int
list.add("nanchen2251");
String str = list.get(0);
Copy the code

Obviously, with the advent of generics, we can make the type safer. We don’t need to write StringList, StringMap when we use List, Map, etc. We just need to specify the parameter type when we declare the List.

In general, generics have the following advantages:

  • Type checking, which helps developers detect errors at compile time;
  • More semantic, let’s say we declare oneLIst<String>We can tell directly what is stored in itStringObject;
  • Automatic type conversion, no need to do strong conversion operation when obtaining data;
  • Be able to write more generic code.

Type erasure

Some of you might have thought, since generics are related to types, can you also use polymorphism of types?

We know that a subtype can be assigned to a parent type, as in:

Object obj = "nanchen2251";
// 👆 this is polymorphic
Copy the code

Object, as the parent of String, can naturally accept the assignment of String objects.

But when we write down this code:

List<String> list = new ArrayList<String>();
List<Object> objects = list;  
// 👆 polymorphism Provided: List Provided: List
       
Copy the code

This assignment error occurs because Java’s generics have an inherently “Invariance” Invariance, which states that List

and List
are not the same type, i.e., The subclass’s generic List

is not a subclass of the generic List
.

Since Java generics are themselves “pseudo-generics”, Java cannot use Object references on the underlying implementation of generics in order to be compatible with pre-1.5 versions, so the generics we declare will be “type erased” at compile time, with generic types being replaced by Object types. Such as:

class Demo<T> {
    void func(T t){
        // ...}}Copy the code

Will compile to:

class Demo {
    void func(Object t){
        // ...     }}Copy the code

You may be wondering why our generics were changed to Object after type erasure at compile time, so we don’t need to strong-cast when we use them. Such as:

List<String> list = new ArrayList<>();
list.add("nanchen2251");
String str = list.get(0);
// 👆 there is no requirement to force list.get(0) to String
Copy the code

This is because the compiler will perform a type check in advance according to the generic type we declare, and then erase it to Object. However, the bytecode also stores the type information of our generic type, and when we use the generic type, the erased Object will be automatically cast. So list.get(0) is itself a strong String.

This technique looks good, but there is a downside. At runtime, you are not sure what type the Object is, although you can get the type of the Object by checking the checkcast inserted by the compiler. But you can’t really use T as a type: for example, this statement is illegal in Java.

T a = new T();
// 👆 error: Type parameter 'T' cannot be instantiated directly
Copy the code

Similarly, since they’re all erased as objects, you can’t make some kind of distinction based on type.

Such as instanceof:

if("nanchen2251" instanceof T.class){
                           // 👆 error: Identifier Expected Unexpected token
}
Copy the code

Such as overloading:

void func(T t){
// 👆 错 : 'func(T)' clashes with 'func(E)'; both methods have same erasure
}
void func(E e){}Copy the code

Also, since primitive data types are not OOP and therefore cannot be erased as Object, Java generics cannot be used for primitive types:

List<int> list;
👆 错 čŊŊ : Type argument cannot be of primitive Type
Copy the code

Oop: Object Oriented Programming

This brings us to the third question above: What exactly does Java type erasure mean?

The first thing you need to understand is that the type of an Object is never erased. If you use an Object to refer to an Apple Object, for example, you can still get its type. For example, RTTI.

RTTI: Run Time Type Identification

Object object = new Apple();
System.out.println(object.getClass().getName());
// 👆 will print Apple
Copy the code

Even if it’s inside a generic.

class FruitShop<T>{
    private T t;

    public void set(T t){
        this.t = t;
    }
    
    public  void showFruitName(a){
        System.out.println(t.getClass().getName());
    }
}
FruitShop<Apple> appleShop = new FruitShop<Apple>();
appleShop.set(new Apple());
appleShop.showFruitName();
// 👆 will print Apple too
Copy the code

Why? Because a reference is just a label that accesses an object, and the object is always on the heap.

So don’t take it out of context that type erasure means erasing the types of the objects in the container, and by type erasure, I mean the container classesFruitShop<Apple>forAppleThe type declaration is erased after type checking at compile time and becomes andFruitShop<Object>Equivalent effect, you could sayFruitShop<Apple> 和 FruitShop<Banana>Be clean for andFruitShop<Object>Equivalent, not refers to inside the object itself of the type is erased!

So, is there type erasure in Kotlin?

Both C# and Java did not support generics at first. Java only added generics in 1.5. In order for a language that does not support generics to support generics, there are only two ways to go:

  • The previous non-generic container remains the same, and then a set of generic types is added in parallel.
  • Extend existing non-generic containers directly to generics without adding any new generic versions.

Java had to choose the second option because of the amount of code available in one sentence prior to 1.5, while C# wisely chose the first.

Kotlin was originally written in Java 1.6, so there were no problems with generics. Does Kotlin’s implementation of generics have type erasures?

Of course. As is already clear, Kotlin is written in Java 1.6, and Kotlin and Java have strong interoperability, as well as type erasers.

But…

You’ll still find something interesting:

Val list = ArrayList() // 👆 提 错 : Not enough information to infer type variable ECopy the code

In Java, it’s fine not to specify generic types, but Kotlin doesn’t work that way. This is easy to think about, since there was certainly no such code before Java 1.5, and generics were not designed to accommodate the default Kotlin Any.

The upper bound wildcard of the generic

As mentioned earlier: Because Java’s generics are inherently “invariable Invariance”, even though the Fruit class is the parent of the Apple class, Java argues that List

is not the same as List

, which is to say, The subclass’s generic List

is not a subclass of the generic List

.



So such code is not run.

List<Apple> apples = new ArrayList<Apple>();
List<Fruit> fruits = apples;  
Required type:List
      
        Provided: List
       
      
Copy the code

What if we want to break through that barrier? Use upper bound wildcards? Extends.

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
    // 👆 use the upper bound wildcard, compile no longer error
Copy the code

“Upper bound wildcards” allow Java generics to be “Covariance”, where Covariance allows the above assignments to be legal.

In an inheritance tree, a subclass inherits from a parent class, which can be considered above and below. Extends limits the parent type of a generic type, so it is called an upper bound.

It has two meanings:

  • Among them?It’s a wildcard, which means thisListThe generic type of is an unknown type.
  • extendsIt limits the upper bound of the unknown type, that is, the generic type must satisfy thisextendsThe restrictions are here and definedclass įš„ extendsThe keywords are a little different:
    • Its scope extends not only to all direct and indirect subclasses, but also to the superclass itself defined by the upper bound, i.eFruit.
    • It also hasimplementsThe upper bound here could also beinterface.

Does this breach make sense?

Some do.

So let’s say we have an interface Fruit:

interface Fruit {
    float getWeight(a);
}
Copy the code

There are two Fruit classes that implement the Fruit interface:

class Banana implements Fruit {
    @Override
    public float getWeight(a) {
        return 0.5 f; }}class Apple implements Fruit {
    @Override
    public float getWeight(a) {
        return 1f; }}Copy the code

Suppose we have a need to weigh fruit:

List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
float totalWeight = getTotalWeight(apples); 
                                // 👆 提 įĪš 错 : Required type: List
      
        Provided: List
       
      

private float getTotalWeight(List<Fruit> fruitList) {
        float totalWeight = 0;
        for (Fruit fruit : fruitList) {
            totalWeight += fruit.getWeight();
        }
        return totalWeight;
    }
Copy the code

This is a very normal requirement. The scale can weigh all kinds of fruit, but it can also just weigh apples. You can’t stop weighing me just because I buy apples. So add the above code to the upper bound wildcard.

List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
float totalWeight = getTotalWeight(apples); 
                                // 👆 no longer returns an error
                                // 👇 added upper bound wildcards? extends
private float getTotalWeight(List<? extends Fruit> fruitList) {
        float totalWeight = 0;
        for (Fruit fruit : fruitList) {
            totalWeight += fruit.getWeight();
        }
        return totalWeight;
    }
Copy the code

However, the above use? Extends extends wildcards break one limit but impose another: they can be output but not input.

What does that mean?

Such as:

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
Fruit fruit = fruits.get(0);
fruits.add(new Apple());
            // 👆 : Required type: capture of? extends Fruit Provided: Apple
Copy the code

Declares the upper wildcard generic collections, is no longer allowed to add new objects, not Apple, Fruit. More broadly: not just collections, but also writing a generic as input.

interface Shop<T> {
    void showFruitName(T t);
    T getFruit(a);
}

Shop<? extends Fruit> apples = new Shop<Apple>(){
    @Override
    public void showFruitName(Apple apple) {}@Override
    public Apple getFruit(a) {
        return null; }}; apples.getFruit(); apples.showFruitName(new Apple());
                     // 👆 : Required type: capture of? extends Fruit Provided: Apple
Copy the code

The lower bound wildcard of the generic

Generics have upper bounded wildcards, but do they have lower bounded wildcards?

Some do.

And the upper bound wildcard? Extends corresponds to a lower bound wildcard, right? super

Lower bound wildcard? Super all cases and? Extends upper wildcard is just the opposite:

  • The wildcard?The generic type representing List is oneUnknown type.
  • superIt limits the lower bound of the unknown type, that is, the generic type must satisfy the super constraint
    • It covers not only all direct and indirect child classes, but also the subclasses themselves defined by the lower bounds.
    • superAlso supportsinterface.

The new restriction imposed on it is that it can only be input but not output.

Shop<? super Apple> apples = new Shop<Fruit>(){
    @Override
    public void showFruitName(Fruit apple) {}@Override
    public Fruit getFruit(a) {
        return null; }}; apples.showFruitName(new Apple());
Apple apple = apples.getFruit();
 👆 error: Required type: Apple Provided: Capture of? super Apple
Copy the code

Explain. First of all? Represents an unknown type. The compiler is not sure of its type.

We don’t know its type, but in Java any Object is a subclass of Object, so we can only assign objects from apples. GetFruit () to Object. Since the type is unknown, assigning directly to an Apple object is definitely irresponsible, requiring a layer of casts that can go wrong.

The Apple object must be a subtype of this unknown type. Due to the nature of polymorphism, it is legal to enter the Button object through the showFruitName.

In summary, Java generics themselves do not support covariant and contravariant generics:

  • You can use generic wildcards? extendsMake generics covariant, but “read but not modify”, where modification only means adding elements to the generic collection, ifremove(int index)As well asclearOf course you can.
  • You can use generic wildcards? superMake generics contravariant, but “modify but not read”, by “unread” I mean not read from generic types, if you followObjectRead out again strong turn of course is also ok.

Once you understand generics in Java, it’s easier to understand generics in Kotlin.

Kotlin’s out and in

Like Java generics, generics in Kolin themselves are immutable.

But in a different way:

  • Use keywordsoutTo support covariant, equivalent to the upper bound wildcard in Java? extends.
  • Use keywordsinTo support inverse, equivalent to the lower bound wildcard in Java? super.
val appleShop: Shop<out Fruit>
val fruitShop: Shop<in Apple>
Copy the code

They are exactly equivalent to:

Shop<? extends Fruit> appleShop;
Shop<? super Apple> fruitShop;
Copy the code

I wrote it a different way, but it does exactly the same thing. “Out” means, “I”, this variable or parameter is only used for output, not input, you can only read me, you can’t write me; In, on the other hand, means it’s only used for input, not output, you can only write me, you can’t read me.

Upper and lower bound constraints on generics

This is all about limiting generics as they are used. We call these “upper bound wildcards” and “lower bound wildcards”. Can we set this limit when we design the function?

Yes, yes, yes.

Such as:

open class Animal
class PetShop<T : Animal?>(val t: T)
Copy the code

Equivalent to Java:

class PetShop<T extends Animal> {
    private T t;

    PetShop(T t) {
        this.t = t; }}Copy the code

Thus, when designing the PetShop class PetShop, we set an upper bound on the supported generics, which must be a subclass of Animal. So we use words like:

class Cat : Animal(a)val catShop = PetShop(Cat())
val appleShop = PetShop(Apple())
                      // 👆 error: Type mismatch. Required: Animal? Found: Apple
Copy the code

Obviously, Apple is not a subclass of Animal and certainly does not satisfy the upper bound of the PetShop generic type.

That… Can I set more than one upper bound constraint?

Sure. In Java, the way to declare multiple constraints on a generic parameter is to use & :

class PetShop<T extends Animal & Serializable> {
                      // 👆 implements two upper bounds via &, which must be a subclass or implementation of Animal and Serializable
    private T t;

    PetShop(T t) {
        this.t = t; }}Copy the code

Instead of &, Kotlin added the where keyword:

open class Animal
class PetShop<T>(val t: T) whereT : Animal? , T : SerializableCopy the code

In this way, multiple upper bound constraints are realized.

Kotlin’s wildcard *

What we’ve said about generic types is that we need to know what type a parameter is. If we’re not interested in the type of a generic parameter, is there a way to handle this?

Some do.

In Kotlin, you can substitute the wildcard * for generic arguments. Such as:

val list: MutableList<*> = mutableListOf(1."nanchen2251")
list.add("nanchen2251")
      // 👆 error: Type mismatch. Required: Nothing Found: String
Copy the code

This error is truly bizarre. The wildcard represents the generic parameter type of MutableList. A String was added to the initialization, but a compilation error occurred when adding a new String.

If the code looks like this:

val list: MutableList<Any> = mutableListOf(1."nanchen2251")
list.add("nanchen2251")
      // 👆 no longer returns an error
Copy the code

It seems that the so-called wildcard as a generic parameter is not equivalent to Any as a generic parameter. MutableList<*> and MutableList

are not the same list. The type of the MutableList<*> is definite, while the type of the MutableList

is not. The compiler does not know what type it is. So it is not allowed to add elements because it would make the type unsafe.

But if you’re careful, you’ll notice that this is very similar to the generic covariant. The wildcard * is just a syntactic sugar that is covariant behind it. So: MutableList<*> is equivalent to MutableList

, using wildcards has the same properties as covariants.

In Java, wildcards have the same meaning, but with? As a wildmatch.

List<? > list =new ArrayList<Apple>(); 
Copy the code

Wildcards in Java? Which is the same thing as? Extends the Object.

Multiple generic parameter declarations

Can you declare multiple generics?

Yes, yes, yes.

Isn’t HashMap a classic example?

class HashMap<K,V>
Copy the code

Multiple generics can be split by multiple declarations, two above, actually multiple can be done.

class HashMap<K: Animal, V, T, M, Z : Serializable>
Copy the code

Generic method

If you declare generic types on a class, can you declare them on a method?

Yes, yes, yes.

If you’re an Android developer, isn’t findViewById of a View the best example?

public final <T extends View> T findViewById(@IdRes int id) {
    if (id == NO_ID) {
        return null;
    }
    return findViewTraversal(id);
}
Copy the code

Obviously, View has no generic parameter types, but findViewById is a typical generic method, and the generic declaration is on the method.

It’s also very easy to rewrite this as Kotlin:

fun 
        findViewById(@IdRes id: Int): T? {
    return if (id == View.NO_ID) {
        null
    } else findViewTraversal(id)
}
Copy the code

The Kotlin reified

As mentioned earlier, any operation that needs to know the exact type of a generic at run time is useless because of type erasings in Java generics. For example, you can’t check whether an object is an instance of the generic type T:

<T> void printIfTypeMatch(Object item) {
    if (item instanceof T) { // 👈 IDE will raise error, illegal generic type for instanceof}}Copy the code

The same goes for Kotlin:

fun <T> printIfTypeMatch(item: Any) {
    if (item is T) { 👈 IDE prompts an error, Cannot check for instance of Erased Type: T
        println(item)
    }
}
Copy the code

The usual solution to this problem in Java is to pass an additional argument of type Class

and then check with the Class#isInstance method:

👇 < T >void check(Object item, Class<T> type) {
    if(type.isinstance (item)) {👆}}Copy the code

Kotlin can also do this, but there is a more convenient way to do this: use the keyword reified with inline:

  👇          👇
inline fun <reified T> printIfTypeMatch(item: Any) {
    if (item is T) { // 👈 there will be no errors}}Copy the code

The above Gson parsing is widely used, such as this extension method in our project:

inline fun <reified T>String? .toObject(type: Type? = null): T? {
    return if(type ! =null) {
        GsonFactory.GSON.fromJson(this, type)
    } else {
        GsonFactory.GSON.fromJson(this, T::class.java)
    }
}
Copy the code

conclusion

Kotlin generics and Java generics have spent a lot of time in this article. Now let’s go back to the first few questions. Do you have the score? If you still feel half-assed, you might as well read it several times.

There is a lot of reference to Kotlin’s generics article on code

Some of them were even taken directly, mainly because they didn’t want to duplicate the wheel. If there are omissions in this article, feel free to leave a comment in the comments section.

Do not finish the open source, write not finish hypocritical. Please scan the qr code below or search “Nanchen” on the official account to follow my wechat official account. At present, I mainly operate Android and try my best to improve for you. If you like, give me a “like” and share