Advanced features of generics

We learned the basics of Kotlin generics in Kotlin Generics and Delegates. The basic usage is essentially the same as the use of generics in Java, so it’s relatively easy to understand. In fact, Kotlin offers a number of unique features in generics that will allow you to play with Kotlin and implement some of the most incredible syntactic features that we can’t miss.

Instantiate the generics

Generic realization is a feature that is alien to most Java programmers, as it is completely absent from Java. If we want to fully understand generic realization, we need to first explain Java’s generic erasing mechanism.

Prior to JDK 1.5, Java had no generics capabilities, and data structures such as lists could store any type of data, requiring a manual transition down to retrieve the data, which was cumbersome and dangerous. For example, we store both string and integer data in the same List, but we cannot distinguish the data type when fetching the data. If we manually force them to be the same type, we will throw a cast exception.

So with JDK 1.5, Java finally introduced generics. This not only makes data structures such as lists easier to use, but also makes our code more secure.

In reality, however, Java’s generics capabilities are implemented through a type erasure mechanism. What does that mean? This means that the generic type constraint only exists at compile time. When running, the JVM will still run as it did before JDK 1.5. The JVM will not recognize the generic type we specified in the code. For example, suppose we create a List<String> collection. Although only String elements can be added to the collection at compile time, at run time the JVM has no idea what type of elements it is intended to contain, only that it is a List.

All JVM-based languages, including Kotlin, implement their generic functionality through a type erasure mechanism. This mechanism makes it impossible to use syntax like A is T or T::class.java because the actual type of T is erased at run time.

However, Kotlin provides the concept of an inline function, the code in the inline function will automatically be replaced at compile time to call it, so there’s no generic also erase, because in the compiled code can directly use the actual type instead of the generic inline function declarations, its working principle as shown in the figure below.

Eventually the code will be replaced with something like the one below.

As you can see, bar() is an inline function with a generic type, foo() calls bar(), and after the code is compiled, the code in bar() will get the actual type of the generic.

This means that generics in inline functions can be instantiated in Kotlin.

So how do I actually write this to make a generic real? First, the function must be inline, that is, the inline keyword is used to qualify the function. Second, the reified keyword must be added where the generic is declared to indicate that the generic is to be instantiated. The example code is as follows:

inline fun <reified T> getGenericType(a){}Copy the code

The generic T in the above function is a instantiated generic because it satisfies both the inlining function and reified keyword. So what can be achieved with the help of generic realism? As you can see from the name of the function, we are going to implement a function to get the actual type of the generic type. The code looks like this:

inline fun <reified T> getGenericType(a) = T::class.java
Copy the code

It’s a single line of code, but it does something completely impossible in Java: the getGenericType() function directly returns the actual type of the currently specified generic. A syntax like T.class is illegal in Java, whereas in Kotlin, a syntax like T::class.java can be used with the help of generic instantiation.

Now we can test the getGenericType() function with the following code:

fun main(a) {
    val result1 = getGenericType<String>()
    val result2 = getGenericType<Int>()
    println("result1 is $result1")
    println("result2 is $result2")}Copy the code

GetGenericType () specifies two different generics. Since getGenericType() returns the specific type of the specified generictype, we print the returned result.

Now run the main() function. The result looks like the figure below.

As you can see, if you specify the generic as String, you get the type java.lang.string; If you specify Int as a generic type, you get the type of java.lang.Integer.

Now that we’ve covered the basics of generic implementation, let’s take a look at what applications generic implementation can have in An Android project.

Application of generic instantiation

The generic instantiation function allows us to obtain the actual type of the generic in the generic function, which makes syntax like A is T, T::class.java possible. Using this feature flexibly can lead to some incredible syntactic constructions, which we’ll take a look at.

In addition to ContentProvider, the other three components of Android have one common feature: they are all used with intents. For example, to start an Activity, write:

val intent = Intent(context, TestActivity::class.java)
context.startActivity(intent)
Copy the code

Do you find TestActivity::class.java syntax annoying? Of course, this would be tolerable if there were no better alternatives, but Kotlin’s generic realization capabilities give us a better option.

Create a new reified. Kt file and write the following code in it:

inline fun <reified T> startActivity(context: Context) {
    val intent = Intent(context, T::class.java)
    context.startActivity(intent)
}
Copy the code

Here we define a startActivity() function that takes a Context argument and uses both inline and reified as a generic T. Here’s where the magic comes in. The second argument received by the Intent should be an Activity Class, but since T is now an instantiated generic type, we can simply pass in T::class.java. Finally, start the Activity by calling the startActivity() method of the Context.

Now, if we want to start TestActivity, we can simply write:

startActivity<TestActivity>(context)
Copy the code

Kotlin will be able to identify the actual type of the specified generic and start the corresponding Activity. So, do you feel like your code is streamlined all of a sudden? This is the magic of generic realization.

However, the current startActivity() function is problematic, because it is often possible to start an Activity with an Intent with parameters such as:

val intent = Intent(context, TestActivity::class.java)
intent.putExtra("param1"."data")
intent.putExtra("param2".123)
context.startActivity(intent)
Copy the code

And after that encapsulation, we can’t pass arguments.

And that’s not a hard problem to solve, just by using the higher order functions that we’ve learned before. Back in the reified. Kt file, add a new overloading of the startActivity() function as follows:

inline fun <reified T> startActivity(context: Context, block: Intent. () - >Unit) {
    val intent = Intent(context, T::class.java)
    intent.block()
    context.startActivity(intent)
}
Copy the code

As you can see, this time the startActivity() function has a function type parameter, and its function type is defined in the Intent class. After an instance of the Intent is created, this function is called with the Intent instance passed in. When calling startActivity(), you can pass the Intent arguments in the Lambda expression, as shown below:

startActivity<TestActivity>(context) {
    putExtra("param1"."data")
    putExtra("param2".123)}Copy the code

I have to say that the code to start an Activity is very comfortable to write, and generic implementations and higher-order functions make this syntax possible, thanks to Kotlin for providing so many great language features.

Okay, so that’s pretty much the end of the practical application of generics. Although we’ve been using the code to start an Activity, the code to start a Service is basically similar, and it’s easy for you to simplify its use with generic implementations and higher-order functions.

So let’s move on to the advanced features of generics.

Covariant of generics

The covariant and contravariant functions of generics are less commonly used, and I personally find it a little hard to understand. But Kotlin’s built-in API uses a lot of covariant and contravariant features, so it’s worth learning if you want to get a deeper understanding of the language.

Now, before we start looking at covariant and contravariant, we need to understand a convention. A method in a generic class or interface whose argument list is where the data is received can be called the in position, and whose return value is where the data is output, so it can be called the out position, as shown in the figure below.

And with that agreed premise, we can move on. First define the following three classes:

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)
Copy the code

Here we define a Person class that contains the fields name and age. Then I defined the Student and Teacher classes as subclasses of the Person class.

Now let me ask you a question: Is it illegal for a method to take an argument of type Person and we pass in an instance of Student? Obviously, because Student is a subclass of Person, and students are people, so this must be legal.

So let me escalate the problem: If a method takes a List<Person> argument and we pass in an instance of List<Student>, is it illegal? This seems to be true, but you can’t do this in Java because List<Student> can’t subclass List<Person>, or there might be a cast security risk.

Why is there a security risk with type conversions? Let’s use a specific example to illustrate. Here’s a custom SimpleData class that looks like this:

class SimpleData<T> {	
    private var data: T? = null
	
    fun set(t: T?). {
    	data = t
    }
    
    fun get(a): T? {
    	return data}}Copy the code

SimpleData is a generic class that encapsulates a generic data field. A call to set() can assign a value to the data field, and a call to get() can retrieve the value of the data field.

Then we assume that if the programming language allowed an instance of SimpleData<Student> to be passed to a method that takes a SimpleData<Person> argument, then the following code would be legal:

fun main(a) {
    val student = Student("Tom".19)
    val data = SimpleData<Student>()
    data.set(student)
    handleSimpleData(data) // In fact, this line of code will report an error, assuming it will compile
    val studentData = data.get()}fun handleSimpleData(data: SimpleData<Person>) {
    val teacher = Teacher("Jack".35)
    data.set(teacher)
}
Copy the code

See anything wrong with this code? In the main() method, we create an instance of Student, encapsulate it into SimpleData<Student>, and pass SimpleData<Student> as an argument to the handleSimpleData() method. But the handleSimpleData() method takes a SimpleData<Person> argument (assuming it can be compiled), so in the handleSimpleData() method, we can create an instance of Teacher, Use it to replace the original data in the SimpleData<Person> parameter. This must be legal, because Teacher is also a subclass of Person, so it’s safe to set an instance of Teacher in there.

But here’s the problem, back in the main() method, we call the get() method on SimpleData<Student> to get the Student data that it encapsulates internally, Now that SimpleData<Student> actually contains an instance of Teacher, a cast exception is inevitable.

Therefore, in order to eliminate this security risk, Java does not allow you to pass parameters in this way. In other words, even though Student is a subclass of Person, SimpleData<Student> is not a subclass of SimpleData<Person>.

However, looking back at the code, you can see that the main reason for the problem is that we set an instance of Teacher to SimpleData<Person> in the handleSimpleData() method. If SimpleData is read-only on a generic T, then there is no safety concern with conversions. Could SimpleData<Student> be a subclass of SimpleData<Person>?

At this point, we are finally going to introduce the definition of generic covariant. If we define A generic class MyClass<T>, where A is A subtype of B and MyClass<A> is A subtype of MyClass<B>, then MyClass is said to be covariant on the generic type T.

But how do you make MyClass<A> A subtype of MyClass<B>? As mentioned earlier, if a generic class is read-only on data of its generic type, there is no cast safety hazard. To do this, you need to make none of the methods in MyClassMyClass<T> class accept parameters of type T. In other words, T can only appear in the out position, but not in position.

Now modify the code for the SimpleData class to look like this:

class SimpleData<out T>(val data: T?) {
    fun get(a): T? {
        return data}}Copy the code

Here we modify the SimpleData class by prefixing the declaration of the generic T with the out keyword. This means that T can now only appear in the out position, but not in position, and it also means that SimpleData is covariant over the generic T.

Since the generic T cannot appear in the in position, we cannot use the set() method to assign the data argument, so we use the constructor instead. You might say, isn’t the generic T in the constructor also in position? True, but since we’re using the val keyword here, the generic T in the constructor is still read-only, so it’s legal and safe to do so. In addition, even if we use the var keyword, it is legal to write as long as we add the private modifier to it and ensure that the generic T is not modifiable externally.

After this modification, the following code compiles perfectly without any security concerns:

fun main(a) {
    val student = Student("Tom".19)
    val data = SimpleData<Student>(student)
    handleMyData(data)
    val studentData = data.get()}fun handleMyData(data: SimpleData<Person>) {
    val personData = data.get()}Copy the code

Since the SimpleData class is already covariant declared, SimpleData<Student> is naturally a subclass of SimpleData<Person>, so it’s safe to pass arguments to the handleMyData() method here.

And then in the handleMyData() method we’re going to get the data wrapped by SimpleData, and even though the generic type here is Person, we’re going to get an instance of Student, but since Person is a parent of Student, The upward transition is completely safe, so there is nothing wrong with this code.

Now, that’s all you need to know about covariation, but there’s one last example to review. As we mentioned earlier, if a method takes an argument of type List<Person> and passes in an instance of List<Student>, this is not allowed in Java. Notice my language here, you’re not allowed to do this in Java.

You guessed right, this is legal in Kotlin because Kotlin already has covariant declarations for many built-in apis by default, including collections of classes and interfaces. Remember what we learned in Chapter 2? The List in Kotlin is itself read-only. If you want to add data to the List, you need to use a MutableList. Since the List is read-only, which means it is naturally covariant, let’s take a look at the List lite source code:

public interface List<out E> : Collection<E> {
    override val size: Int
    override fun isEmpty(a): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(a): Iterator<E>
    public operator fun get(index: Int): E
}
Copy the code

List precedes the generic E with the out keyword, indicating that List is covariant over the generic E. In principle, after covariant is declared, the generic E can only appear in the out position, but you will find that in the contains() method, the generic E still appears in the in position.

Writing this in and of itself is illegal, because the presence of the generic E in the in position implies a cast safety hazard. However, the purpose of the contains() method is very clear. It only determines whether the current set contains the element passed in as an argument, and does not modify the contents of the current set, so it is essentially safe. In order for the compiler to understand that it is safe to do this, we add an @unsafevariance annotation in front of the generic E so that the compiler will allow the generic E to appear in the in position. But if you abuse this feature and cause a cast exception at run time, Kotlin is not responsible for that.

All right, so that’s it for covariant, and now we’re going to do contravariant.

Inverting generics

I think it’s a little bit easier to learn about contravariances once you understand the covariant, because they’re related.

But just by definition, contravariant and covariant are the exact opposite. If we define A generic class MyClass<T>, where A is A subtype of B and MyClass<B> is A subtype of MyClass<A>, then we can say that MyClass is contravariant on the generic type T. The difference between covariant and contravariant is shown in the figure below.

From an intuitive point of view, the contravariant rule seems strange. Originally A is A subtype of B, how can MyClass<B> in turn be A subtype of MyClass<A>? Don’t worry, let’s look at a specific example and you’ll get the idea.

Here we define a Transformer interface to perform some transformation operations, as follows:

interface Transformer<T> {
    fun transform(t: T): String
}
Copy the code

As you can see, the Transformer interface declares a transform() method that takes a parameter of type T and returns data of type String, which means that the parameter T will become a String after being transformed by the transform() method. The actual transformation logic is implemented by subclasses, and the Transformer interface is not concerned with this.

Now let’s try implementing the Transformer interface as follows:

fun main(a) {
    val trans = object : Transformer<Person> {
        override fun transform(t: Person): String {
            return "${t.name} ${t.age}"
        }
    }
    handleTransformer(trans) // This line of code will report an error
}

fun handleTransformer(trans: Transformer<Student>) {
    val student = Student("Tom".19)
    val result = trans.transform(student)
}
Copy the code

First we write an anonymous class implementation of Transformer<Person> in the main() method and transform() to transform the passed Person object into a string of “name + age” concatenation. The handleTransformer() method takes an argument of type Transformer<Student>, and creates a Student object in the handleTransformer() method. The transform() method of the argument is called to convert the Student object to a string.

This code is safe from a security perspective, because Student is a subclass of Person, and it is perfectly safe to use the anonymous implementation of Transformer<Person> to convert the Student object to a string. In fact, the handleTransformer() method is called with a syntax error for the simple reason that Transformer<Person> is not a subtype of Transformer<Student>.

So that’s where the contravariant comes in, and it’s designed to deal with that. Modify the code in the Transformer interface to look like this:

interface Transformer<in T> {
    fun transform(t: T): String
}
Copy the code

Here we prefixed the declaration of the generic T with the in keyword. This means that now the T can only appear in the in position and not in the out position, which also means Transformer is inverting the generic T.

Yes, with that little change, the code should compile and work, because Transformer<Person> is now a subtype of Transformer<Student>.

That’s about it for inverting, but if you want to think a little bit more about it, why can’t the generic T appear in the out position when inverting? To explain the problem, let’s assume that contravariant is allowed to have the generic T in the out position, and then look at the potential security implications.

Modify the code in Transformer to look like this:

interface Transformer<in T> {
    fun transform(name: String, age: Int): @UnsafeVariance T
}
Copy the code

As you can see, we changed the transform() method to take two parameters, name and age, and changed the return value type to the generic T. Since contravariant does not allow the generic T to appear in the out position, the @unsafevariance annotation is added here to allow the compiler to compile properly, which is the same technique used in the List source.

So, what kind of security risks may arise at this time? Take a look at the following code:

fun main(a) {
    val trans = object : Transformer<Person> {
        override fun transform(name: String, age: Int): Person {
            return Teacher(name, age)
        }
    }
    handleTransformer(trans)
}

fun handleTransformer(trans: Transformer<Student>) {
    val result = trans.transform("Tom".19)}Copy the code

The above code is a typical example of a cast exception caused by contravariant rule violation. In the anonymous class implementation in Transformer, we build a Teacher object using the name and age parameters passed in the transform() method and return the object directly. Since the return value of the transform() method is required to be a Person object, and Teacher is a subclass of Person, this must be legal.

But in the handleTransformer() method, we call Transformer’s transform() method, passing in name and age, expecting a Student object to return, In fact, however, the transform() method returns a Teacher object, so there must be a cast exception.

Since this code is compilable, we can run it and print the exception information as shown in the figure below.

As you can see, it tells us that the Teacher type cannot be converted to the Student type.

In other words, Kotlin has taken into account all the potential conversions security concerns when it provides covariant and inverter capabilities. As long as we strictly follow the syntax rules and make generics appear only in the out position when covariant and in position when contravariant, there will be no cast exception. The @unsafevariance annotation breaks this grammar rule, but it also carries an additional risk, so you need to know exactly what you’re doing when using the @unsafevariance annotation.

Finally, let’s take a look at the use of inverters in Kotlin’s built-in API, a typical example of which is the use of Comparable. Comparable is an interface for comparing the size of two objects. Its source code is defined as follows:

interface Comparable<in T> {
	operator fun compareTo(other: T): Int
}
Copy the code

As you can see, Comparable is invert on the generic T, and the compareTo() method is used to implement the specific comparison logic. So why do we want the Comparable interface to be inverting here? Imagine the following scenario. If we implemented the logic to compare the size of two Person objects using Comparable<Person>, then it must also be true to compare the size of two Student objects using that same logic, So it makes sense to make Comparable<Person> a subclass of Comparable<Student>, which is a very typical application of inverting.

All right, so much for covariant and contravariant