Write at the beginning: I plan to write a Kotlin series of tutorials, one is to make my own memory and understanding more profound, two is to share with the students who also want to learn Kotlin. The knowledge points in this series of articles will be written in order from the book “Kotlin In Action”. While displaying the knowledge points in the book, I will also add corresponding Java code for comparative learning and better understanding.
Kotlin Tutorial (1) Basic Kotlin Tutorial (2) Functions Kotlin Tutorial (3) Classes, Objects and Interfaces Kotlin Tutorial (4) Nullability Kotlin Tutorial (5) Types Kotlin Tutorial (6) Lambda programming Kotlin Tutorial (7) Operator overloading and Other conventions Higher-order functions Kotlin tutorial (9) Generics
Generic type parameters
Generics allow you to define a type that takes a type parameter. When instances of that type are created, the type parameter is replaced with a concrete type called a type argument. Such as:
List<String>
Map<String, Person>
Copy the code
As with types in general, the Kotlin compiler can often derive type arguments:
val authors = listOf("Dimtry"."Sevelana")
Copy the code
If you want to create an empty list so that there is no clue to the type argument, you have to specify it explicitly.
val readers: MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()
Copy the code
Unlike Java, Kotlin always requires that type arguments be either explicitly stated or can be deduced by the compiler. Since generics were only introduced to Java in version 1.5, it must be compatible with older versions, so it allows the use of generic types with no type parameters — so-called primitive types. Kotlin has generics from the start, so it does not support primitive types, and type arguments must be defined.
Generic functions and properties
If you want to write a function that uses a list, and you want it to work on any list, rather than a list of elements of a specific type, you need to write a generic function.
fun <T> List<T>.slice(indices: IntReange): List<T>
Copy the code
It is basically similar to the Java declaration, declared in front of the method name, can be used in the function.
You can also declare type parameters for class or interface methods, top-level functions, extension properties, and extension functions. For example, your extended attribute returns the next-to-last element of the list:
val <T> List<T>.penultimate: T
get() = this[size -2]
Copy the code
Generic non-extended attributes cannot be declared
Ordinary (non-extended) attributes cannot have type parameters and cannot store multiple values of different types in an attribute of a class, so it makes no sense to declare generic non-extended function functions.
Declare generic classes
Like Java, Kotlin declares generic classes and generic interfaces by following class names with Angle brackets and enclosing type parameters within Angle brackets. Once declared, type parameters can be used in the body of the class just like any other type.
interface List<T> {
operator fun get(index: Int): T
}
Copy the code
If your class inherits generics (or implements a generic interface), you need to provide a type argument to the underlying type’s generic parameter.
class StringList: List<String> {
override fun get(index: Int): String = ...
}
Copy the code
Type parameter constraint
Type parameter constraints can restrict types that are type arguments to (generic) classes and (generic) functions. If you specify a type as an upper bound constraint on the parameter of the generic type, the corresponding type argument must be that type or its subtype in the specific initialization of the generic type. You define the constraint by placing the colon after the type parameter name, followed by the type that is the upper bound on the type parameter:
fun <T : Number> List<T>.sum(): T
Copy the code
Equivalent to Java:
<T extends Number> T sum(List<T> list)
Copy the code
Once you specify an upper bound for the type parameter T, you can use the value of type T as the value of its upper bound:
fun <T : Number> oneHalf(value: T): Double {
returnValue.todouble () // Call the Number method}Copy the code
In rare cases where you need to specify more than one constraint on a type parameter, you need to use a different syntax:
fun <T> ensureTrailingPeriod(seq: T)
where T : CharSequence, T : Appendable {
if(! seq.endWith('. ') {// Call the CharSequence method seq.append('. '// call Appendable's method}}Copy the code
In this case, it is clear that the type as a type argument must implement both the CharSequence and Appendable interfaces.
Makes the type parameter non-empty
If you declare a generic class or function, any type argument, including nullable type arguments, can replace her type parameter. In fact, type arguments that do not specify an upper bound will use Any? The default upper bound:
class Processor<T> { fun process(value: T) { value? .hashCode() } }Copy the code
In the process function, the parameter value is nullable, even though T is not marked with a question mark.
If you want to ensure that the replacement type parameter is always a non-null type, you can do this by making a constraint. If you have no restrictions other than nullability, you can use Any instead of the default Any. As an upper bound.
class Processor<T : Any> {
fun process(value: T) {
value.hashCode()
}
}
Copy the code
Generics at run time: erase and implement type parameters
As you probably know, generics on the JVM are typically implemented by type erase-that is, the type arguments of a generic class instance are not retained at run time.
Generics at run time: type checking and conversion
As with Java, Kotlin’s generics are erased at run time. This means that a generic class instance does not carry information about the type arguments used to create it. For example, if you create a List
and put a bunch of strings into it, at run time you can only see it as a List, not what kind of elements the List was intended to contain. Along with erasing type information comes constraints. Because the type arguments are not stored, you cannot check them. For example, you can’t tell if a list is a list of strings or a list of other objects:
>>> if (value is List<String>)
ERROR: Canot check for instance of erased type
Copy the code
So how do you check if a value is a list, not a set or some other object? The special * projection syntax can be used to do such a check:
if (value is List<*>)
Copy the code
This represents a generic type with an argument of unknown type, similar to the List
.
Notice that in as and as? Normal generic types can still be used in conversions. But if the class has the right underlying type but the type arguments are wrong, the conversion will not fail because the type arguments are unknown at run time when the conversion occurs. As a result, such a transformation would result in the compiler issuing an “unchecked cast” warning. This is just a warning, you can still use this value.
fun print// Warning: Unchecked :List<*> to List<Int> val intList = cas? List<Int> ? : throw IllegalArgumentException("List is expected")
println(intList.sum())
}
>>> printSum(listOf(1, 2, 3))
6
Copy the code
The compilation is fine: the compiler simply issues a warning, which means the code is legal. If you call this function on a list or set of integers, everything happens as expected: in the first case the sum of the elements is printed, and in the second case IllegalArgumentException is thrown. But if you pass a value of the wrong type, such as List
, the runtime gets a ClassCastException.
Declare a function that takes an argument of the implemented type
As mentioned earlier, Kotlin generics are erased at run time, as are type arguments to generic functions. When calling a generic function, you cannot determine the type argument used in the function body:
>>> fun <T> isA(value: Any) = value is T
Error: Cannot check for instance of erased type: T
Copy the code
This is usually the case, with one exception to avoid this limitation: inline functions. Type parameters of inline functions can be implemented, meaning that you can reference the actual type arguments at run time. In the previous section, we learned that if a function is marked with the inline keyword, the compiler replaces every function call with the actual code implementation of the function. Using inline functions can also improve performance if the function uses lambda arguments: the code for lambda is also inlined, so no anonymous classes are created. Based on this implementation principle, it should and can be imagined that generics are already identified in the class file, depending on the context in which they are embedded. If you declare the isA function in the previous example inline and reified the type parameter, you can use it to check whether value is an instance of T.
inline fun <reified T> isA(value: Any) = value is T
>>> println(isA<String>("abc"))
true
>>> println(isA<String>(123))
false
Copy the code
One of the simplest examples where an implementable type parameter can come into play is the library function filterIsInstance. This function takes a collection, selects which instances of the specified class, and returns those selected instances.
>>> val items = listOf("one", 2, "three")
>>> println(items.filterIsInstance<String>())
[one, three]
Copy the code
By specifying
as the type argument to the function, you indicate that you are only interested in strings. So the return type of the function is List
. In this case, the type argument is known at run time and is used by the filterIsInstance function to check whether the value in the list is an instance of the class specified as the type argument. Here is a simplified version of the Kotlin library function filterIsInstance declaration:
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
val destination = mutableListOf<T>()
for (element in this) {
if (element is T) {
destination.add(element)
}
}
return destination
}
Copy the code
In the previous section, we mentioned that marking functions inline has a performance advantage only if the function has a parameter of type function and its corresponding argument lambda is inlined with the function. Now we mark the function inline in order to be able to use the argument.
Why does compaction only work for inline functions
The compiler inserts the bytecode that implements the inline function where each call occurs. Every time you call a function that takes an argument to the implemented type, the compiler knows the exact type used as the argument to the type in that particular call. Therefore, the compiler can generate bytecode references to concrete classes as type arguments. In fact, for filterIsInstance
, the generated code is equivalent to the following code:
for (element in this) {
if (element is String) {
destination.add(element)
}
}
Copy the code
Because the generated bytecode refers to a concrete class rather than a type parameter, it is not affected by type parameter erasations that occur at run time. Note that an inline function with a parameter of type reified cannot be called from Java code. Normal inline functions can be called in Java just like regular functions — they can be called but not inlined. Functions with implemented parameter types require extra processing to replace the value of the type parameter into the bytecode, so they must always be inline. That way they can’t be called in a normal way like Java.
Use the materialized type parameter instead of the class reference
If you’re an Android developer, displaying activities is one of the most common methods. Instead of passing the Activity Class as java.lang.Class, you can also use an instantiated type parameter:
inline fun <reified T : Activity> Context.startActivity() {
val intent = Intent(this, T::class.java)
startActivity(intent)
}
>>> startActivity
Copy the code
The ::class. Java syntax shows how to get the Kotlin class corresponding to java.lang. class. This is the exact equivalent of Service. Class in Java.
Constraints on type parameters
Although it is a convenient tool to implement type parameters, they have some limitations. Some factalization is inherent, while others are determined by existing implementations, and these restrictions may be relaxed in future versions of Kotlin. In particular, you can use the materialized type parameter as follows:
- Used in type checking and type conversions (
is
,! is
,as
,as?
) - Using the Kotlin reflection API (
::class
) - Get the corresponding
java.lang.Class
(::class.java
) - As a type argument to call another function
Don’t do the following things:
- Creates an instance of the class specified as a type parameter
- Calls the method of the companion object of the type parameter class
- Calls to functions with arguments of an actualized type use unactualized type parameters as type arguments
- Marks the type parameters of a class, attribute, or non-inline function as
reified
Variants: Generics and subtyping
The concept of variation describes how (generic) types that have the same base type and different type arguments are related to each other: for example, List
and List
.
Why is there variation: passing arguments to functions
Suppose you have a function that accepts List
as an argument. Is it safe to pass a List
variable to this function? Without a doubt, it is safe to pass a String to a function that expects Any, because String inherits Any. This is not so simple when String and Any become type arguments to the List interface.
fun printContents(list: List<Any>) {
println(list.joinToString())
}
>>> printContents(listOf("abc"."bac"))
abc, bac
Copy the code
That doesn’t seem like a problem, so let’s look at another example:
fun addAnswer(list: MutableList<Any>) {
list.add(42)
}
>>> val strings = mutableListOf("abc"."bac")
>>> addAnswer(strings)
Type mismatch. Required: MutableList<Any> Found: MutableList<String>
Copy the code
The only difference between this example and the previous one is that you change List
to MutableList
, so you can’t pass a List of generic strings to the function. Now you can answer the question of whether it is safe to pass a list of strings to a function that expects a list of Any objects. It is not safe for a function to add or replace elements in the list, as this raises the possibility of type inconsistencies. In Kotlin, you can easily control this by choosing the right interface depending on whether the list is mutable. If the function receives a read-only list, you can pass a list with a more specific element type. You can’t do this if the list is mutable.
Classes, types, and subtypes
In order to discuss the relationship between types, you need to be familiar with the term subtypes. Whenever you need A value of type A, you can use A value of type B (as A value of A), and type B is called A subtype of type A. For example, Int is a subtype of Number, but Int is not a subtype of String. This definition also indicates that any type can be considered a subtype of its own. The term supertype is the opposite of a subtype. If A is A subtype of B, then B is A supertype of A. Why is it important that one type is a subtype of another? The compiler performs this check every time it assigns a value to a variable or passes an argument to a function.
fun test(I: Int) {val n: Number = I // funf (s: String) {/*... */} f(I)Copy the code
A variable is allowed to store the value only if the value type is a subtype of the variable type. For example, the type Int of the initializer I of variable n is a subtype of the type Number of the variable, so the declaration of n is legal. An expression is only allowed to be passed to a function if its type is a subtype of the type of the function argument. The type Int of I in this example is not a subtype of the type String of the function argument, so the call to f will fail to compile. You might think that subtypes are subtypes, but why are they called subtypes in Kotlin? Because Kotlin has nullable types. A non-null type is a subtype of its nullable version, but they all correspond to the same class. You can always store values of non-null types in nullable variables, but not the other way around.
var s: String = "abc"val t: String? = s // The compiler passes s = t // the compiler failsCopy the code
Earlier, it was safe to pass a List
variable to a function that expects List
. Now we can reorganize this using subtyping terminology: Is List
a subtype of List
? You have seen why it is not safe to treat a MutableList
as a subtype of a MutableList
. Obviously, the return is also false: MutableList
is definitely not a subtype of MutableList
. A generic class (e.g., MutableList) is said to be invariant in terms of the type parameters if A MutableList
is neither A subtype nor A supertype of any two types A and B. All classes in Java are immutable (although which specific uses of classes can be marked as mutable, you’ll see later).
The typing rules for the List class are different. The List interface in Kotlin represents A read-only collection. If A is A subtype of B, then List is A subtype of List. Such classes or interfaces are said to be covariant.
Covariant: Preserve the subtyping relationship
A covariant class is A generic class (we use Producer
as an example) for which the following description holds: If A is A subtype of B, then Producer
is A subtype of Producer. We say that subtyping is preserved. In Kotlin, to declare that a class is covariant on a type parameter, prefix the name of the type parameter with the out keyword:
interface Producer<out T> {
fun produce(): T
}
Copy the code
Marking the type parameters of a class as covariant allows the values of the class to be passed as arguments to or returned from functions that do not exactly match the type parameters defined in the functions. For example, imagine a function that feeds a group of animals represented by the Herd class, whose type parameter determines the type of animals in the Herd.
open class Animal {
fun feed() {... } } class Herd<T : Animal> { val size: Int get() = ... operator fun get(i: Int): T {... } } fun feeAll(animals: Herd<Animal>) {for (i in 0 until animals.size) {
animals[i].feed()
}
}
Copy the code
Suppose the user of this code has a herd of cats to care for:
class Cat : Animal() {
fun cleanLitter() {... } } fun takeCareOfCats(cats: Herd<Cat>) {for(i in0 until cats.size) {cats[I]. CleanLitter () // feedAll(cats) // error: type mismatch}}Copy the code
If you try to pass the cat herd to the feedAll function, you will get type mismatch errors at compile time. Because the type parameter T in the Herd class does not use any variant modifiers, cats are not subclasses of herds. You can get around this problem using explicit type conversions, but this approach is verbose, error-prone, and almost never the right way to solve type mismatches. Because Herd has a List-like API and does not allow its callers to add and change animals in the Herd, it can be made covariant and modify the calling code accordingly.
class Herd<out T: Animal> {
...
}
Copy the code
You can’t make any class into a helper: it’s not safe. Making a class covariant on a type parameter limits the possibilities of using that type parameter in that class. To be type-safe, it can only be used in so-called out locations, meaning that the class can only produce values of type T, not consume them. The use of type parameters in class member declarations can be divided into in position and out position. Consider a class that declares a type parameter T and contains a function that uses T. If the function takes T as its return type, we say it’s in out position. In this case, the function produces a value of type T. If T is used as the type of a function argument, it is in position, and such a function consumes a value of type T.
interface Transformer<T> {
//inFun transform(t: t): t}Copy the code
The out keyword before the type argument of the class requires all methods that use T to place T only in the out position, not in position. This keyword constrains the possibility of using T, which guarantees the security of the corresponding subtype relationship.
To reiterate, the keyword out on the type parameter T has two meanings:
- Subtyping is preserved
- T can only be used in the out position
Now let’s look at the List
Interface. Kotlin’s List is read-only, so it only has a method get that returns elements of type T and doesn’t define any methods to store elements of type T in the List. Therefore, it is also covariant.
interface List<out T> : Collection<T> {
operator fun get(index: Int): T
}
Copy the code
Note that type parameters can be used not only as parameter types or return types directly, but also as type arguments of another type. For example, the List interface contains a subList method that returns List
:
interface List<out T> : Collection<T> {
fun subList(fromIndex: Int, toIndex: Int): List<T>
}
Copy the code
In this example, the T in the function subList is also used in the out position. Note that you cannot declare a MutableList
covariant on its type argument, because it contains both methods that accept a value of type T as an argument and methods that return the value (thus, T appears in and out positions).
interface MutableList<T>
: List<T>, MultableCollection<T> {
override fun add(element: T): Boolean
}
Copy the code
The compiler enforces this limitation. If this class is declared as a cooperator, the compiler will report an error: Type parameter T is declared as ‘out’ but occurs in’ in’ position. Notice that the constructor parameters are neither in nor out. Even if the type parameter is declared out, you can still use it in the constructor parameter declaration:
class Herd<out T: Animal>(vararg animals: T) {... }Copy the code
If an instance of a class is used as an instance of a more general type, the variation prevents that instance from being misused: potentially dangerous methods cannot be called. The constructor is not a method that can be called after the instance is created, so it is not potentially dangerous. Then, if you use the keywords val and var on constructor arguments, you declare both a getter and a setter (if the property is mutable). Thus, for read-only attributes, the type parameter is used in the out position, while mutable attributes use it in both the out and in positions:
class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) {... }Copy the code
In the example above, T cannot be marked out because the class contains the setter for the leadAnimal property, which uses T in the in position. Also note that location rules only cover the public, protected, and internal apis visible outside the class. The parameters of a private method are neither in nor out. A variation rule only prevents misuse of a class by external users, but does not affect the class’s own implementation:
class Herd<out T: Animal>(private var leadAnimal: T, vararg animals: T) {... }Copy the code
It is now safe to make Herd covariant on T, as the attribute leadAnimal becomes private.
Inverse: inverse rotor typing relationship
The notion of contravariance can be thought of as a mirror image of covariant: for a contravariance, its subtyping relationship is the opposite of that of the class used as a type argument. We started with the example of the Comparator interface, which defines a method to compare two given objects:
interface Comparator<in T> {
fun compare(e1: T, e2: T): Int {...}
}
Copy the code
A comparator defined for a value of a particular type can obviously compare values of any subtype of that type. For example, if you have a Comparator
, you can use it to compare values of Any concrete type.
interface Comparator<in T> {
fun compare(e1: T, e2: T): Int
}
fun main(args: Array<String>) {
val anyComparator = Comparator<Any> { e1, e2 -> e1.hashCode() - e2.hashCode() }
val strings = listOf("a"."b"."c")
strings.sortedWith(anyComparator)
}
Copy the code
The sortedWith function expects a Comparator
(a Comparator that can compare strings), and it is safe to pass it a Comparator that can compare more general types. If you want to perform a comparison on a particular type of object, you can use a comparator that can handle that type or its supertype. This shows that the Comparator
is a subtype of the Comparator
, where Any is the supertype of String. The subtyping relationships between different types are diametrically opposed to the subtyping relationships between the comparators of those types. You are now ready for the full contravariant definition. A class that inverts type parameters is a generic class (we use Consumer
as an example) for which the following description is true: If B is A subclass of A, then Consumer
is A subtype of Consumer. The type arguments A and B swap places, so we say that the subtyping is reversed.
The in keyword means that values of the corresponding type are passed in to and consumed by the methods of the class. Similar to the covariance case, the use of constraint type parameters results in specific subtyping relationships. The in keyword on the type parameter T means that the subtyping is reversed and that T can only be used in position.
Covariant, contravariant, and invariant classes
covariance | inverter | Not to distort |
---|---|---|
Producer<out T> |
Consumer<in T> |
MutableList<T> |
The subtyping of the class is preserved:Producer<Cat> isProducer<Animal> The subtype of |
Subtyping reversed:Consumer<Animal> isConsumer<Cat> The subtype of |
There is no subtyping |
T can only be in the out position | T can only be in position | T could be anywhere |
A class can covariant on one type parameter while inverting on another. The Function interface is a classic example. Here is a Function declaration for a single argument:
interface Function1<in P, out R> {
operator fun invoke(p: P): R
}
Copy the code
Kotlin’s expression (P) -> R is another, more readable form of Function
. You can see that the in keyword P (parameter type) is only used in the IN position, and the out keyword R (return type) is only used in the out position. This means that for the first type parameter of this function type, the subtyping is reversed, while for the second type parameter, the subtyping is preserved.
,>
fun enumerateCats(f: (Cat) -> Number) {... } fun Animal.getIndex(): Int = ... >>> enumerateCats(Animal::getIndex)Copy the code
This code is legal in Kotlin. Animal is a supertype of Cat, and Int is a subtype of Number.
Use point variants: Specify variants where the type occurs
It is convenient to be able to specify variant modifiers at class declaration time, because these modifiers are applied wherever the class is used. This is called a declarative point variant. If you are familiar with Java’s wildcard types (? Extends and? Super), you’ll realize that Java handles variants in a completely different way. In Java, each time you use a type that takes a type parameter, you can also specify whether that type parameter can be replaced with its subtype or supertype. This is called using point variants.
Kotlin’s declarative point variants vs. Java wildcards
Declaring a dot deformation leads to cleaner code because all users of this class don’t have to worry about it anymore, because in Java, library authors have to always use the wildcard: Function
to create an API that runs as expected by the user. If you look at the source code for the Java 8 standard library, you’ll find wildcards every time you use the Function interface. For example, here is the declaration of the stream.map method:
/* Java */
public interface Stream<T> {
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
}
Copy the code
Kotlin also supports the use of point variants, allowing a variant to be specified where a type parameter occurs, even if it cannot be declared as covariant or contravariant when the type is declared. You have seen many interfaces like MutableList, which are neither covariant nor contravariant in general because they both produce and consume the values of the types specified as their type parameters. But it’s not uncommon for variables of this type to be used as just one of the roles in a particular function: either producer or consumer. For example, the following simple function:
fun <T> copyData(source: MutableList<T>,
destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}
Copy the code
This function copies elements from one collection to another. Although both collections have invariant types, the source collection is only used for reading, and the target collection is only used for writing. In this case, the types of collection elements do not need to match exactly. For example, there is no problem copying a collection of strings into a collection that can contain any object. To make this function support lists of different types, you can introduce a second generic parameter.
fun <T : R, R> copyData(source: MutableList<T>,
destination: MutableList<R>) {
for (item in source) {
destination.add(item)
}
}
>>> val ints = mutableListOf(1, 2, 3)
>>> val anyItems = mutableListOf<Any>()
>>> copyData(ints, anyItems)
>>> println(anyItems)
[1, 2, 3]
Copy the code
You declare two generic parameters representing the element types in the source list and the target list. To be able to copy an element from one list to another, the source element type should be a subtype of the element in the target list (Int is a subtype of Any). But Kotlin offers a more elegant way of putting it. When the implementation of a function calls methods whose type arguments appear only in the out position (or only in position), you can take advantage of this by adding variant modifiers to the function definition for type arguments for a specific purpose.
fun <T> copyData(source: MutableList<out T>,
destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}
Copy the code
You can specify variation modifiers for any use of type parameters in a type declaration: parameter types, local variable types, function return types, and so on. What happens here is called a type projection: we say that the source is not a regular MutableList, but a projected (restricted) MutableList. Call only those methods whose return type is a generic type parameter, or, strictly speaking, use it only in the out position. The compiler disallows calls to methods that take type arguments (using type arguments in the in position) :
>>> val list: MutableList<out Number> = ...
>>> list.add(42)
Error: Out-projected type 'MutableList<out Number>' prohibits the use of 'fun add (element: E): Boolean'
Copy the code
Don’t be surprised if you can’t call some methods with a projection type. If you need to call those methods, you should use regular types instead of projections. This may require you to declare a second type parameter, which depends on the type to be projected.
Of course, the correct way to implement a copyData function is to use List
as the type of the source argument, because we only use methods declared in List, not MutableList, and the variants of the List type parameters are specified when we declare them. But this example is still important to illustrate the concept, especially to remember that most classes don’t have two separate interfaces like List and MutableList, a covariant read interface and a constant read/write interface. If a type parameter already has an out variant, it makes no sense to get its out projection. Like List
. It’s the same thing as List
, because List is declared as class List
. The compiler issues a warning indicating that such projections are unnecessary.
Similarly, you can use the IN modifier on the usage of type parameters to indicate that the corresponding value acts as a consumer in this particular place, and that type parameters can be replaced with any of their subtypes.
fun <T> copyData(source: MutableList<T>,
destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
Copy the code
Kotlin’s use of point variants corresponds directly to Java’s bounded wildcards. MutableList
in Kotlin and MutableList
has one meaning. The in projection MutableList
corresponds to the Java MutableList
.
Asterisk projection: use*
Substitute type parameter
Earlier in this chapter, when we talked about type checking and conversions, we mentioned a special asterisk projection syntax that can be used to indicate that you don’t know anything about generic arguments. For example, a List containing elements of unknown type is represented in this syntax as List<*>. Now let’s dive into the semantics of asterisk projections.
The first thing to notice is MutableList<*> and MutableList
Are you sure you MutableList < Any? This type of list can contain elements of any type. MutableList<*>, on the other hand, is a list of elements of a particular type, but you don’t know which type. Such a list is created as a list containing elements of a particular type, such as String, and the code that creates it is expected to contain only elements of that type. Because you don’t know which type, you can’t write anything to the list, because any value you write might violate the expectations of the calling code. But it is possible to read elements from the list, because you know that all the values stored in the list match Any of the Kotlin supertypes, right? :
fun main(args: Array<String>) { val list: MutableList<Any? > = mutableListOf('a', 1, "qwe")
val chars = mutableListOf('a'.'b'.'c')
val unknownElements: MutableList<*> = if (Random().nextBoolean()) list elseChars // unknownElements. Add (42) // The compiler disallows calling this method println(unknownElements. First ()) // Reading elements is safe} // output aCopy the code
Why does the compiler treat MutableList<*> as the type of out projection? In the context of this example, the MutableList<*> projection becomes MutableList
, when you don’t have Any element type information, read Any? Type elements are still safe, but it is not safe to write elements to a list. Kotlin’s MyType<*> is equivalent to the Java MyType
.
An asterisk projection is equivalent to
for an inverse type of parameter like Consumer
. In fact, you cannot call any method with T in the signature in this asterisk projection. If the type parameter is contravariant, it can only be represented as one consumer, and, as we discussed earlier, you don’t know exactly what it can consume. Therefore, it can’t consume anything.
When the information about a type argument is not important, you can use the asterisk projection syntax without using any method of referring to a type parameter in the signature, or simply reading data regardless of its specific type. For example, we could implement a printFirst function that takes a List<*> as an argument:
fun printFirst(list: List<*>) {
if (list.isNotEmpty()) {
println(list.first())
}
}
Copy the code