Brief description: Kotlin’s article on generics is also near the end, but it will be a difficult and important part of generics until later. I believe that there are many beginners to Kotlin’s generic type changes are half-understanding, for example, I was confused at the beginning of contact, there are too many concepts, and each concept is related to the back, as long as there is a place in front of the understanding behind the difficulty is more and more difficult to understand. Kotlin’s generics have some new concepts than generics in Java, such as subtyping relationships, contravariant, covariant, and star projection. In my opinion, there are several steps to learn Kotlin’s generics well:

  • First, understand each small concept and conclusion of generics in depth, preferably in your own words;
  • Second, verify your understanding and conclusions by analyzing the relevant source code in Kotlin.
  • Third, reinforce your understanding with practical examples.

Because generic type change involves more content, so it is divided into two parts, no more nonsense, please see the following map:

First, why do there exist type changes?

First, we need to clarify two noun concepts: base type and argument type. For example, for List

, List is the base type and String is the argument type

And then, we need to be clear, what exactly do we mean by metamorphosis?

It can be described briefly as reflecting a special type of correspondence rule. Isn’t that abstract? List

and List

have the same base type. String and Any have a parent-child relationship. Is there a correspondence between List

and List

? In fact, the kata we’re talking about is built around this scenario.



With that in mind, why do we need this type of relationship? For a comparison example, we need to pass parameters to a function.

fun main(args: Array<String>) {
    val stringList: List<String> = listOf("a"."b"."c"."d")
    val intList: List<Int> = listOf(1.2.3.4)
    printList(stringList)// Pass a List
      
        argument to the function, where List
       
         can be substituted for List
        
       
      
    printList(intList)// Pass a List
      
        argument to the function, where List
       
         can be substituted for List
        
       
      
}

fun printList(list: List<Any>) {
List
      
        = List
       
         = List
        
          = List
        
       
      
    list.forEach {
        println(it)
    }
}
Copy the code

The preceding operation is valid. The result is as follows

List<Any>
MutableList<Any>

fun main(args: Array<String>) {
    val stringList: MutableList<String> = mutableListOf("a"."b"."c"."d")
    val intList: MutableList<Int> = mutableListOf(1.2.3.4)
    printList(stringList)// This is actually not compiled
    printList(intList)// This is actually not compiled
}

fun printList(list: MutableList<Any>) {
    list.add(3.0f)// Start importing dangerous operations. dangerous! dangerous!
    list.forEach {
        println(it)
    }
}
Copy the code

Let’s think about what would happen if the above code were to compile, using proof by contradiction, which could lead to similar dangerous operations. A collection of ints or strings introduces other illegal data types, so there must be a problem and the compilation fails. Because we said that inside a function it only knows the type of a MutableList

and it doesn’t know what was passed to it from the outside, so it can only follow the type rule inside. So inside the function list.add(3.0f) line is compiled. Adding a Float to a MutableList

collection obviously makes sense.

Conclusion: List

, List

replace List

, MutableList

, MutableList

replace MutableList

. In fact, the type substitution in question is actually a type change, so you can understand why there is a type change, the type change to generic interface is more secure, if there is no type change, the above dangerous problem would occur.





So the other question is why do some types work and some don’t? It is safe to pass in more specific types of arguments from outside the collection if there are no operations inside the collection that modify its elements (read only). It is not safe to pass in more specific types of arguments if there are operations inside the collection that modify elements (write operations), so the compiler does not allow this. Using the above example, List

is actually a read-only collection (note: It and Java in the List is not a thing, pay attention to distinguish), inside it there is no add, remove operation method, not letter may have a look of its source, so it is the function of parameter can open bold receiving external parameters, because there is no modification elements operation is safe, so the first example is compile OK; For MutableList

, it is a readable and writable set in Kotlin, which is equivalent to the List in Java. Therefore, there are dangerous operation methods to modify, delete and add elements in it. So it has to do a strict check for external function parameters that are of type MutableList

.


In order to help you understand and remember, I drew a funny cartoon to help you understand. This cartoon is so important that the covariant, contravariant and invariant can be understood from it. And it will be analyzed over and over again

Finally in order to thoroughly analyze this problem can give you a look at the List

and MutableList

part of the source code

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

    override fun isEmpty(a): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(a): Iterator<E>

    // Bulk Operations
    override fun containsAll(elements: CollectionThe < @UnsafeVariance E>): Boolean. }Copy the code

public interface MutableList<E> : List<E>, MutableCollection<E> {
    // Modification Operations
    override fun add(element: E): Boolean

    override fun remove(element: E): Boolean

    // Bulk Modification Operations
    override fun addAll(elements: Collection<E>): Boolean. }Copy the code

A careful comparison of List

and MutableList

generic definitions is different. They correspond to covariant and invariant, respectively. We’ll talk more about what is covariant and what is contravariant and what is invariant later.

Second, class, type, subclass, subtype, supertype concept sorting

You might be a little confused by the title, aren’t classes and types the same thing? I usually use them as one thing. In fact, it is not the same, here we need to go to the concept of one by one to understand, in order to better understand the type of variation relationship. So let’s see how they’re different.

We can classify Kotlin’s classes into two broad categories: generic classes and non-generic classes

  • A generic class

Non-generic classes are the most common classes in development. When a generic class defines a variable, its class is actually the type of the variable. For example: var MSG: String Here we can say that the String class and the MSG variable are of the same type. But there’s a special type in Kotlin and that’s nullable, which we can define as var MSG: String? Where the String class and MSG variable String, right? The types are different. So in Kotlin a class usually has at least two types. So classes and types are not the same thing.

  • A generic class

Generics are more complex than non-generic classes, in fact, one generic class can correspond to an infinite number of types. It’s easy to see why. As we know from the previous article, we define generic parameters when we define a generic class. To get a valid generic type, we need to replace the type parameters in the definition with the concrete type arguments in the external use place. We know in Kotlin that List is a class, it’s not a type. It can be derived from an infinite number of generic types such as List

, List

, List >, List

>




  • Subtypes, subtypes, and supertypes

A subclass is a derived class that inherits its parent class (also known as the base class). For example, class Student: Person(), where Student is generally called a subclass of Person.

Subtypes are different. We know from the above class and type differences that a class can have many types, so subtypes are not just as strict as the inheritance relationship of subtypes. The general rule for subtype definition is that whenever A value of type A is needed, it can be replaced with A value of type B, and then type B can be said to be A subtype of type A or A supertype of type B. It is obvious that rules for subtypes are looser than rules for subtypes. So we can analyze the following examples:

Note that a type is also a subtype of its own, so it is obvious that String must be replaceable anywhere a value of String appears. Subclass relationships are also subtype relationships in general. Values like String certainly do not replace values of Int, so they do not have a subtype relationship

For example, all non-null types of a class are subtypes of the nullable type of that class, but not the other way around, like String non-null is String, right? Nullable subtype, obviously, any String? Wherever a nullable type occurs, it can be replaced with a value of the String non-null type. In fact, I can experience these in the development process, for example, careful students will find that in the Kotlin development process, if a function receives a nullable type parameter, it is legal to pass in a non-nullable type argument at the call place. But if a function accepts arguments of a non-null type, passing in arguments of nullable type will prompt you that there may be a null pointer problem and you need to make a non-null check. Because we know that non-null types are safer than nullable types. Here’s a picture to understand:

Third, what is the subtyping relation?

I’m sure you can guess what a subtyping relationship is by now, right? It’s actually what we talked about above.

Subtyping relationships:

To recap: If A value of type A occurs anywhere and anytime and can be replaced by A value of type B, which is A subtype of type A, then the mapping substitution between type B and type A is A subtyping relationship

Answer the first question

Now we can also use Kotlin’s more technical term of subtyping to explain the original problem of why function arguments of type List

,List

can be passed to function parameters of type List

, Function parameters of type MutableList

cannot be passed to function parameters of type MutableList

.




Since List

,List

is a subtype of List

, Any List

values can be replaced with List

,List

values. And the MutableList

, the MutableList

type is not a subtype of the MutableList

type or a supertype of the MutableList

type, so of course you can’t replace Any of those.









Draw a detail point from the above answer

List

is a subtype of List

. List

is a subtype of List

. List

is a subtype of List

. It’s important that we think in terms of types and subtypes in generics, not just class and subclass inheritance, because List

or List

,List

is also a subtype of List

. This relationship is called the preserved subtyping relationship, which is called covariant. I’ll focus on that in the next part.


?>






Four, conclusion

This article can be said to be the basis of a concept understanding of the next article. Many advanced concepts and principles in the next article are extended in this article. It is suggested to digest these concepts well.

  • Make sure you understand what a subtype is and how it differs from a subtype. In fact, the basis of Kotlin’s generics is the subtyping relationship, which we usually analyze in terms of types and subtypes, rather than simply class and subclass inheritance.

  • 2, there is no thinking about why we want to make such a set of type change relationship ah, in fact, think carefully in order to generics class operation and use more secure, to avoid the introduction of some dangerous hidden dangers, resulting in generic unsafe, specific can see an ugly cartoon drawn in front of this article. So also have to admire the design of this set of rules language developer thought.

  • 3, finally, the next article is the advanced concept of generics, in fact, do not be afraid, as long as the concept of this article understand clearly behind will be very simple.

Welcome to Kotlin’s series of articles:

Original series:

  • Kotlin’s trick of Reified Type Parameter (Part 2)
  • Everything you need to know about the Kotlin property broker
  • Source code parsing for Kotlin Sequences
  • Complete analysis of Sets and functional apis in Kotlin – Part 1
  • Complete parsing of lambdas compiled into bytecode in Kotlin syntax
  • On the complete resolution of Lambda expressions in Kotlin’s Grammar
  • On extension functions in Kotlin’s Grammar
  • A brief introduction to Kotlin’s Grammar article on top-level functions, infix calls, and destruct declarations
  • How to Make functions call Better
  • On variables and Constants in Kotlin’s Grammar
  • Elementary Grammar in Kotlin’s Grammar Essay

Translation series:

  • Kotlin’s trick of Reified Type Parameter
  • When should type parameter constraints be used in Kotlin generics?
  • An easy way to remember Kotlin’s parameters and arguments
  • Should Kotlin define functions or attributes?
  • How to remove all of them from your Kotlin code! (Non-empty assertion)
  • Master Kotlin’s standard library functions: run, with, let, also, and apply
  • All you need to know about Kotlin type aliases
  • Should Sequences or Lists be used in Kotlin?
  • Kotlin’s turtle (List) rabbit (Sequence) race
  • The Effective Kotlin series considers using static factory methods instead of constructors
  • The Effective Kotlin series considers using a builder when encountering multiple constructor parameters

Actual combat series:

  • Use Kotlin to compress images with ImageSlimming.
  • Use Kotlin to create a picture compression plugin.
  • Use Kotlin to compress images.
  • Simple application of custom View picture fillet in Kotlin practice article

Welcome to the Kotlin Developer Association, where the latest Kotlin technical articles are published, and a weekly Kotlin foreign technical article is translated from time to time. If you like Kotlin, welcome to join us ~~~