https://www.kotlincn.net/docs/reference/generics.html 关于泛型的官方解释
网上好多关于Kotlin的发型 基本都是copy 上边链接里的内容 我下边找了一篇其他的讲解:
Generics in Kotlin
Last modified: November 23, 2018
1. Overview
In this article, we’ll be looking at the generic types in the Kotlin language.
They are very similar to those from the Java language, but the Kotlin language creators tried to make them a little bit more intuitive and understandable by introducing special keywords like out and in.
2. Creating Parameterized Classes
Let’s say that we want to create a parameterized class. We can easily do this in Kotlin language by using generic types:
1
2
3
4
5
6
|
We can create an instance of such a class by setting a parameterized type explicitly when using the constructor:
1
2
3
4
5
|
val
parameterizedClass
=
ParameterizedClass
"string-value"
)
val
res
=
parameterizedClass.getValue()
assertTrue(res is String)
|
Happily, Kotlin can infer the generic type from the parameter type so we can omit that when using the constructor:
1
2
3
4
5
|
val
parameterizedClass
=
ParameterizedClass(
"string-value"
)
val
res
=
parameterizedClass.getValue()
assertTrue(res is String)
|
3. Kotlin out and in Keywords
3.1. The out Keyword
Let’s say that we want to create a producer class that will be producing a result of some type T. Sometimes; we want to assign that produced value to a reference that is of a supertype of the type T.
To achieve that using Kotlin, we need to use the out keyword on the generic type. It means that we can assign this reference to any of its supertypes. The out value can be only be produced by the given class but not consumed:
1
2
3
4
5
|
class
ParameterizedProducer
private
val
value
:
T) {
fun get()
:
T {
return
value
}
}
|
We defined a ParameterizedProducer class that can produce a value of type T.
Next; we can assign an instance of the ParameterizedProducer class to the reference that is a supertype of it:
1
2
3
4
5
|
val
parameterizedProducer
=
ParameterizedProducer(
"string"
)
val
ref
:
ParameterizedProducer
=
parameterizedProducer
assertTrue(ref is ParameterizedProducer
|
If the type T in the ParamaterizedProducer class will not be the out type, the given statement will produce a compiler error.
3.2. The in Keyword
Sometimes, we have an opposite situation meaning that we have a reference of type T and we want to be able to assign it to the subtype of T.
We can use the in keyword on the generic type if we want to assign it to the reference of its subtype. The in keyword can be used only on the parameter type that is consumed, not produced:
1
2
3
4
5
|
class
ParameterizedConsumer
fun toString(value
:
T)
:
String {
return
value.toString()
}
}
|
We declare that a toString() method will only be consuming a value of type T.
Next, we can assign a reference of type Number to the reference of its subtype – Double:
1
2
3
4
5
|
val
parameterizedConsumer
=
ParameterizedConsumer
val
ref
:
ParameterizedConsumer
=
parameterizedConsumer
assertTrue(ref is ParameterizedConsumer
|
If the type T in the ParameterizedCounsumer will not be the in type, the given statement will produce a compiler error.
4. Type Projections
4.1. Copy an Array of Subtypes to an Array of Supertypes
Let’s say that we have an array of some type, and we want to copy the whole array into the array of Any type. It is a valid operation, but to allow the compiler to compile our code we need to annotate the input parameter with the out keyword.
This lets the compiler know that input argument can be of any type that is a subtype of the Any:
1
2
3
4
5
|
fun copy(from
:
Array
:
Array
assert(from.size
==
to.size)
for
(i in from.indices)
to[i]
=
from[i]
}
|
If the from parameter is not of the out Any type, we will not be able to pass an array of an Int type as an argument:
1
2
3
4
5
6
7
8
|
val
ints
:
Array
=
arrayOf(
1
,
2
,
3
)
val
any
:
Array
=
arrayOfNulls(
3
)
copy(ints, any)
assertEquals(any[
0
],
1
)
assertEquals(any[
1
],
2
)
assertEquals(any[
2
],
3
)
|
4.2. Adding Elements of a Subtype to an Array of its Supertype
Let’s say that we have the following situation – we have an array of Any type that is a supertype of Int and we want to add an Int element to this array. We need to use the in keyword as a type of the destination array to let the compiler know that we can copy the Int value to this array:
1
2
3
|
fun fill(dest
:
Array
:
Int) {
dest[
0
]
=
value
}
|
Then, we can copy a value of the Int type to the array of Any:
1
2
3
4
5
|
val objects: Array
1
)
fill(objects,
1
)
assertEquals(objects[
0
],
1
)
|
4.3. Star Projections
There are situations when we do not care about the specific type of the value. Let’s say that we just want to print all the elements of an array and it does not matter what the type of the elements in this array is.
To achieve that, we can use a star projection:
1
2
3
|
fun printArray(array
:
Array<*>) {
array.forEach { println(it) }
}
|
Then, we can pass an array of any type to the printArray() method:
1
2
|
val
array
=
arrayOf(
1
,
2
,
3
)
printArray(array)
|
When using the star projection reference type, we can read values from it, but we cannot write them because it will cause a compilation error.
5. Generic Constraints
Let’s say that we want to sort an array of elements, and each element type should implement a Comparable interface. We can use the generic constraints to specify that requirement:
1
2
3
|
fun
return
list.sorted()
}
|
In the given example, we defined that all elements T needed to implement the Comparable interface. Otherwise, if we will try to pass a list of elements that does not implement this interface, it will cause a compiler error.
We defined a sort function that takes as an argument a list of elements that implement Comparable, so we can call the sorted() method on it. Let’s look at the test case for that method:
1
2
3
4
5
|
val
listOfInts
=
listOf(
5
,
2
,
3
,
4
,
1
)
val
sorted
=
sort(listOfInts)
assertEquals(sorted, listOf(
1
,
2
,
3
,
4
,
5
))
|
We can easily pass a list of Ints because the Int type implements the Comparable interface.
6. Generics at Runtime
6.1. Type Erasure
As with Java, Kotlin’s generics are erased at runtime. That is, an instance of a generic class doesn’t preserve its type parameters at runtime.
For example, if we create a Set
Let’s create two Sets with two different type parameters:
1
2
|
val books: Set
"1984"
,
"Brave new world"
)
val primes: Set
2
,
3
,
11
)
|
At runtime, the type information for Set
So, how does Kotlin’s compiler prevent us from adding a Non-String into a Set
The answer is simple. The compiler is the one responsible for erasing the type information but before that, it actually knows the books variable contains String elements.
So, every time we get an element from it, the compiler would cast it to a String or when we’re gonna add an element into it, the compiler would type check the input.
6.2. Reified Type Parameters
Let’s have more fun with generics and create an extension function to filter Collection elements based on their type:
1
2
|
fun
Error: Cannot check
for
instance of erased type: T
|
The “it is T” part, for each collection element, checks if the element is an instance of type T, but since the type information has been erased at runtime, we can’t reflect on type parameters this way.
Or can we?
The type erasure rule is true in general, but there is one case where we can avoid this limitation: Inline functions. Type parameters of inline functions can be reified, so we can refer to those type parameters at runtime.
The body of inline functions is inlined. That is, the compiler substitutes the body directly into places where the function is called instead of the normal function invocation.
If we declare the previous function as inline and mark the type parameter as reified, then we can access to generic type information at runtime:
1
|
inline fun
|
The inline reification works like a charm:
1
2
3
|
>> val set = setOf(
"1984"
,
2
,
3
,
"Brave new world"
,
11
)
>> println(set.filterIsInstance
[
2
,
3
,
11
]
|
Let’s write another example. We all are familiar with those typical SLF4j Logger definitions:
1
2
3
4
5
|
class
User {
private
val log = LoggerFactory.getLogger(User::
class
.java)
// ...
}
|
Using reified inline functions, we can write more elegant and less syntax-horrifying Logger definitions:
1
|
inline fun
class
.java)
|
Then we can write:
1
2
3
4
5
|
class
User {
private
val log = logger
// ...
}
|
This gives us a cleaner option to implement logging, the Kotlin way.
6.3. Deep Dive into Inline Reification
So, what’s so special about inline functions so that type reification only works with them? As we know, Kotlin’s compiler copies the bytecode of inline functions into places where the function is called.
Since in each call site, the compiler knows the exact parameter type, it can replace the generic type parameter with the actual type references.
For example, when we write:
1
2
3
4
5
|
class
User {
private
val log = logger
// ...
}
|
When the compiler inlines the logger
7. Conclusion
In this article, we were looking at the Kotlin Generic types. We saw how to use the out and in keywords properly. We used type projections and defined a generic method that uses generic constraints.
The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven project, so it should be easy to import and run as it is.