Description: This is the last article on generics, and the last article on generics. By the way, I was in Beijing last week for JetBrains 2018 Developer Day, mainly for the Kotlin session. Personally, I feel that I have gained a lot. BennyHuo and Yanwei really delivered a lot of good things in their wonderful speeches. Of course, Hali evangelist brought the new features of Kotlin version 1.3, and Teacher Zhong Hui, the head of Google’s Technical promotion in China, brought the application of Coroutines in Android development. Therefore, the following articles are prepared for subsequent release:
- 1. What are the new 1.3 features in Kotlin?
- 2. Coroutine in Kotlin for Android
- 3. Initial experience of Ktor Asynchronous framework (Ktor Preschool)
- 4, The use of data class in Kotlin (Benny boss in the conference very clear, also very comprehensive. I will focus on the pits I have stepped on before, especially those used for back-end development.
So today’s article is mainly about giving two tails to the last one and how generic variants can be applied to real development. And I’m going to use the last post on how to choose the corresponding variant step by step to determine whether we should use covariant, contravariant, or invariant, with a practical example. This article is relatively simple and mainly includes the following four points:
- 1. Kotlin declares point variants in contrast to the use of point variants in Java
- 2, How to use Kotlin use point variants
- Star projection in Kotlin generics
- 4. Use generic variant implementations for Boolean extensions in real development
Kotlin declares point variants in contrast to the use of point variants in Java
1. The difference between declaring a point variant and defining a point variant
First, let’s explain what we mean by declaring point variants and using point variants. Declaring point variants, as the name implies, specifies the type of variant (covariant, contravariant, invariant) when we declare a generic class. In Kotlin, we mean declaring a generic class by adding in or out to its parameter. Using dot variants means specifying the type variation relationship every time you use the generic class. If you’re familiar with Java medium variants, Java uses dot variants.
2. Comparison of the advantages of both
Declaration point variation:
- Has a clear advantage is just in a generic class declaration defines a variable corresponding relationship is ok, so no matter in any place to use it without the specified variable corresponding relation, and use some variant is the place where every use has to repeat the definition of special trouble (or find a place of Kotlin is better than that of Java).
Using point variants:
- In fact, there are use scenarios for using point variants, which can be used more flexibly; So Kotlin hasn’t completely dismissed this syntax point, and here’s how it can be used.
3. Use comparison
I just said that using point variants is particularly troublesome. Let’s see how troublesome it is. So this is represented by Java, and we all know that in Java we use type variants, do we use? The wildcard extends (super/extends) to do this, for example: Function
, where? Extends E corresponds to covariant, and? Super T corresponds to contravariant. Here is the source of the flatMap function in the Stream API as an example
@FunctionalInterface
public interface Function<T.R> {// The declaration does not specify the type variant relation. }Function
>
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
Copy the code
How convenient is it to declare point variants? Let’s take Kotlin as an example. Kotlin uses in and out to implement the corresponding rules for type variants. The source code of the flapMap function in the Sequences API is used as an example
public interface Sequence<out T> {// Out covariant is declared at the Sequence definition
/** * Returns an [Iterator] that returns the values from the sequence. * * Throws an exception if the sequence is constrained to be iterated once and `iterator` is invoked the second time. */
public operator fun iterator(a): Iterator<T>
}
public fun <T, R> Sequence<T>.flatMap(transform: (T) -> Sequence<R>): Sequence<R> {The generic argument R in the flatMap function Sequence does not need to specify the variant type again, since Sequence declares covariant types
return FlatteningSequence(this, transform, { it.iterator() })
}
Copy the code
From the above source comparison, it is clear that declaring point variants in Kotlin is much easier than using point variants in Java. However, the use of point variants is not all bad, and there are some scenarios for using them in Kotlin. We’re going to find out
How to use Kotlin use point variants
In fact, there are some scenarios for using point variants in Kotlin. Imagine a real-world scenario where, although a generic class is immutable, that is, it has read-write operations, sometimes in a function we only use read-only or write-only operations. In this case, using point variants it can make an invariable reduced range of variants degenerate into covariant or contravariant. Is not suddenly meng force, with the source code to speak, you will understand, together with a source code example.
The MutableCollection
in Kotlin is invariant
public interface MutableCollection<E> : Collection<E>, MutableIterable<E> {// Without in and out modifications, the specification is unchanged
override fun iterator(a): MutableIterator<E>
public fun add(element: E): Boolean
public fun remove(element: E): Boolean
public fun addAll(elements: Collection<E>): Boolean
public fun removeAll(elements: Collection<E>): Boolean
public fun retainAll(elements: Collection<E>): Boolean
public fun clear(a): Unit
}
Copy the code
Then we move on to the source definitions of the filter and filterTo functions
public inline fun <T>可迭代<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
>
,>
,>
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
Copy the code
The function above is found to be consistent with the MutableCollection. Destination writes inside the filterTo function, iterating over the elements in Iterable and adding them to the destination set. Although MutableCollection is invariant, only write operations are involved inside the function, so it is completely possible to specify it as a contravariant type using point variants. The degeneration from invariant to contravariant type obviously does not affect the safety of generics, so the processing here is completely legal. If you look at other collection manipulation apis, there are many places that use this approach.
So, in terms of invariant degenerate to contravariant, here’s another example of invariant degenerate to covariant.
// We can see that the source set generic type declaration is out covariant.
fun <T> copyList(source: MutableList<out T>, destination: MutableList<T>): MutableList<T>{
for (element in source) destination.add(element)
}
Copy the code
MutableList
is an out covariant type of the source generic type. I don’t think so. Given the contravariant example, you can probably guess why. The simple reason is that in a copyList function, the source set does not involve writes but only reads, so you can use point variants to degrade the invariant MutableList to covariant versions without obviously introducing generic safety issues.
So after the above example and the previous example about how to use contravariant, covariant, invariant. As I said before, don’t memorize the rules, the key is whether the read and write operations in the usage scenario introduce generic type safety. If you have a clear read/write scenario, you can follow the above examples to flexibly use generic type variants, you can write more perfect programs.
Star projection in Kotlin generics
1. Definition of star projection
A star projection is a special asterisk projection that is commonly used to indicate that nothing is known about a generic argument, in other words that it represents a particular type, but only that the type is unknown or cannot be determined.
2,MutableList<*>
andMutableList<Any? >
The difference between
The first thing we need to notice and be clear about is MutableList<*> and MutableList
is different. MutableList<*> contains a list of names of a particular type; While MutableList < Any? > is a collection that contains any type. A collection of specific types is just not sure which type it is. Any type means that it contains multiple types. The difference is that once a collection type is determined, the collection can only contain one type. Any type can contain multiple types.
3,MutableList<*>
Actually an Out covariant projection
MutableList<*> projection into MutableList
type
We know that MutableList<*> contains only a set of a certain type. It could be String, Int, or one of the other types. We can imagine that we need to disable write operations on this set. Write operations can introduce a mismatched type into the collection, which is a dangerous thing. On the other hand, if the collection has a read-only operation, it is always safe to read the data element type without knowing it. If there’s only a read then it’s covariant, and covariant has a reserved subtyping relationship, that is, the read element type is an indeterminate subtype, so you can imagine that it just replaces Any, right? Type, because Any? So MutableList<*> is actually MutableList
subtype.
Use the generic variant implementation for Boolean extensions in real development
About the implementation of Boolean extension, mainly from watching some code written by BennyHuo found that the original can be so convenient to write if-else, so I went to see its implementation may be many people know its implementation, And the reason why I’m doing this is because this is a really good example of Kotlin generics covariant in action.
1. Why develop a Boolean extension
If all numbers are odd, return “odd set”. If not, return “not odd set”.
I’m going to ask you if you’ve written something like this
/ / Java version
public void isOddList(a){
int count = 0;
for(int i = 0; i < numberList.size(); i++){
if(numberList[i] % 2= =1){ count++; }}if(count == numberList.size()){
System.out.println("Odd set");
return;
}
System.out.println("Not an odd set.");
}
Copy the code
/ / kotlin version of the writing
fun isOddList(a) = println(if(numberList.filter{ it % 2= =1}.count().equals(numberList.size)){"Odd set"} else {"Not an odd set."})
Copy the code
//Boolean Extended version
fun isOddList(a) = println(numberList
.filter{ it % 2= =1 }
.count()
.equals(numberList.size)
.yes{"Odd set"}
.otherwise{"Not an odd set."})// Do you find that the Boolean extension is more silky
Copy the code
By contrast, Kotlin’s if-else expression returns a value, but the if-else structure breaks the chain call, but if you use the Boolean extension, you can make the chain call smoother all the way through.
2. Boolean Extends usage scenarios
The Boolean extension can be used in two scenarios:
- Used with functional apis, the Boolean extension is recommended for if-else judgments because it does not break the chain call structure like the if-else structure.
- Another scenario is that if has a large number of criteria, and if wrapping an if code around it is too bloated, using Boolean will make the code more concise.
3, Boolean code implementation
By observing the use of the Boolean extension above, we first need to clarify a few points:
- Number one: We know
Yes, otherwise
There must be an intermediate type that acts as a bridge between two functions. - Second point: The yes, otherwise function is scoped with a return value, as in the example above, which can return string data directly.
- Third point: Both the yes and Oterwise functions are lamBA expressions, and the lambda expression returns the value of the final expression
- Fourth point: The yes function is called on a Boolean type, so you need an implementation extension function based on a Boolean type
So, based on the above points, we can write a simple version of this extension (return values are not supported for now).
// As an intermediate type, implement chaining
sealed class BooleanExt
object Otherwise : BooleanExt()
object TransferData : BooleanExt()
fun Boolean.yes(block: () -> Unit): BooleanExt = when {
this -> {
block.invoke()
TransferData// Since the return value is BooleanExt, we also need to return a BooleanExt object or a subclass of it, so we define TransferData Object to inherit BooleanExt
}
else- > {Otherwise Object inherits BooleanExt; Otherwise object inherits BooleanExt
Otherwise
}
}
// We need to write an extension of the BooleanExt class to link up otherwise method operations
fun BooleanExt.otherwise(block: () -> Unit) = when (this) {
is Otherwise -> block.invoke()// If an Otherwise subclass executes block
else -> Unit// If no, return a Unit
}
fun main(args: Array<String>) {
val numberList: List<Int> = listOf(1.2.3)
// Use the defined extension
(numberList.size == 3).yes {
println("true")
}.otherwise {
println("false")}}Copy the code
The simple version above basically puts the extension shelf together, but the only functionality that doesn’t implement the return value, plus the return value functionality, is the final version of the Boolean extension.
TransferData cannot use the object expression type, because it needs to use the constructor to pass in parameters of the generic type. So TransferData should be replaced by a normal class.
To define covariant, contravariant, or invariant, we can refer to the process selection diagram and comparison table in the previous article
From the basic structure, untyped relationships (reserved, reversed), typeless variant points (covariant point out, contravariant point in), roles (producer output, consumer input), the location of type parameters (covariant is modifying read-only attributes and function return value types; Contravariant is to modify variable attributes and function parameter types), performance characteristics (read only, write, read and write) and so on
covariance | inverter | The same | |
---|---|---|---|
The basic structure | Producer<out E> |
Consumer<in T> |
MutableList<T> |
Subtyping relationships | Preserve the subtyping relationship | Inverse rotor typing relation | No subtyping relationship |
There is no type change point | Covariance points out | Inverter points in | No change point |
The location where the type parameter exists | Modifies read-only attribute types and function return value types | Modifies variable attribute types and function parameter types | Either way, no constraints |
role | The producer output is a generic parameter type | The consumer input is the generic parameter type | Both producer and consumer |
Performance characteristics | Internal operation read only | Internal operations only write | Internal operations can be read and written |
- The first step is to determine the location and characteristics of the type parameter
sealed class BooleanExt<T>
objectOtherwise : BooleanExt<Any? > ()class TransferData<T>(val data: T) : BooleanExt<T>()/ / val modify data
inline fun <T> Boolean.yes(block: () -> T): BooleanExt<T> = when {//T is in the return position of the function
this -> {
TransferData(block.invoke())
}
else -> Otherwise// Note: this is not compiled
}
inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T = when (this) {//T is in the return position of the function
is Otherwise ->
block()
is TransferData ->
this.data
}
Copy the code
With the code above we can basically determine covariant or invariant,
- Step 2: Determine whether there is a subtyping relationship
Since the yes function else branch returns Otherwise and the compilation does not pass, it is obvious that this is not immutable, since the above code was written immutable. So it’s basically covariant.
Sealed class BooleanExt
sealed class BooleanExt
The reason for this error is that the yes function requires that a BooleanExt
type be returned, and Otherwise a BooleanExt
sealed class BooleanExt<out T>// define covariant
object Otherwise : BooleanExt<Nothing> ()//Nothing is a subtype of all types. The covariant class inheritance is the same as the generic parameter type inheritance
class TransferData<T>(val data: T) : BooleanExt<T>()// Data only involves read-only operations
// Declare an inline function
inline fun <T> Boolean.yes(block: () -> T): BooleanExt<T> = when {
this -> {
TransferData(block.invoke())
}
else -> Otherwise
}
inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T = when (this) {
is Otherwise ->
block()
is TransferData ->
this.data
}
Copy the code
Five, the conclusion
So that’s the end of all of Kotlin’s articles on generics, of course generics are very important and go into the real world of development, especially developing frameworks, As you can see, the above Boolean implementation follows the rules from the previous article on how to overcome the difficulties of Kotlin’s generic type variants (part 2) by deciding which type to use and doing a little analysis. In general, it’s very handy to have that map as a guide. In fact, when it comes to generics, you still need to understand the rules, not memorize them, so that you can use them more flexibly. Finally, thanks to bennyHuo for the Boolean extension implementation.
Welcome to Kotlin’s series of articles:
Original series:
- How to overcome the difficulties of Generic typing in Kotlin (Part 2)
- How to overcome the difficulties of Generic typing in Kotlin (Part 1)
- 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 ~~~