Kotlin
Generics
@kotlin | Developed by JetBrains
What? Why?
fun quickSort(collection: CollectionOfInts) { ... }
quickSort(listOf(1, 2, 3)) // OK
quickSort(listOf(1.0, 2.0, 3.0)) // NOT OK
fun quickSort(collection: CollectionOfDoubles) { ... } // overload (we’ll get back to this a bit later)
quickSort(listOf(1.0, 2.0, 3.0)) // OK
quickSort(listOf(1, 2, 3)) // OK
Kotlin Number inheritors: Int, Double, Byte, Float, Long, Short
Do we need 4 more implementations of quickSort?
How?
Does the quickSort algorithm actually care what is it sorting? No, as long as it can compare
two values against each other.
fun <T : Comparable<T>> quickSort(collection: Collection<T>): Collection<T> { ... }
quickSort(listOf(1.0, 2.0, 3.0)) // OK
quickSort(listOf(1, 2, 3)) // OK
quickSort(listOf("one", "two", "three")) // OK
How?
Generics allow you to write code that can work with any type or with types that should
satisfy some rules (constraints) but are not limited in any other ways: type parameters.
class Holder<T>(val value: T) { ... }
val intHolder = Holder<Int>(23)
val cupHolder = Holder("cup") // Generic parameter type can be inferred
Constraints
Sometimes we do not want to work with an arbitrary type and expect it to provide us with
some functionality. In such cases type constraints in the form of upper bounds are used:
upper bounds.
class Pilot<T : Movable>(val vehicle: T) {
fun go() { vehicle.move() }
}
val ryanGosling = Pilot<Car>(Car("Chevy", "Malibu"))
val sullySullenberger = Pilot<Plane>(Plane("Airbus", "A320"))
Constraints continued
There can be several parameter types, and generic classes can participate in inheritance.
public interface MutableMap<K, V> : Map<K, V> { ... }
There can also be several constraints (which means the type parameter has to implement
several interfaces):
fun <T, S> moveInAnAwesomeWayAndCompare(a: T, b: S) where T : Comparable<T>, S :
Comparable<T>, T : Awesome, T : Movable { ... }
Star-projection
When you do not care about the parameter type, you can use star-projection * (Any? /
Nothing).
fun printKeys(map: MutableMap<*, *>) { ... }
Let's go back
open class A
open class B : A()
class C : B()
Nothing <: C <: B <: A <: Any
This means that the Any class is the superclass for all the classes and at the same time
Nothing is a subtype of any type
What is next?
Consider a basic example:
interface Holder<T> {
fun push(newValue: T) // consumes an element
fun pop(): T // produces an element
fun size(): Int // does not interact with T
}
What is next?
interface Holder<T> {
fun push(newValue: T) // consumes an element
fun pop(): T // produces an element
fun size(): Int // does not interact with T
}
In Kotlin there are type projections:
G<T> // invariant, can consume and produce elements
G<in T> // contravariant, can only consume elements
G<out T> // covariant, can only produce elements
G<*> // star-projection, does not interact with T
Several examples
G<T> // invariant, can consume and produce elements
interface Holder<T> {
fun push(newValue: T)// consumes an element: OK
fun pop(): T // produces an element: OK
fun size(): Int // does not interact with T: OK
}
Several examples
G<in T> // contravariant, can only consume elements
interface Holder<in T> {
fun push(newValue: T) // consumes an element: OK
fun pop(): T // produces an element: ERROR: [TYPE_VARIANCE_CONFLICT_ERROR] Type
parameter T is declared as 'in' but occurs in 'out' position in type T
fun size(): Int // does not interact with T: OK
}
Several examples
G<out T> // covariant, can only produce elements
interface Holder<out T> {
fun push(newValue: T) // consumes an element: ERROR:
[TYPE_VARIANCE_CONFLICT_ERROR] Type parameter T is declared as 'out' but occurs in 'in' position
in type T
fun pop(): T // produces an element: OK
fun size(): Int // does not interact with T: OK
}
Several examples
interface Holder<T> {
fun push(newValue: T) // consumes an element: OK
fun pop(): T // produces an element: OK
fun size(): Int // does not interact with T: OK
}
fun <T> foo1(holder: Holder<T>, t: T) {
holder.push(t) // OK
} fun foo1(holder: Holder<Any>, t: Any) {
holder.push(t) // OK
fun <T> foo2(holder: Holder<*>, t: T) {
}
holder.push(t) // ERROR: [TYPE_MISMATCH] Type mismatch. Required: Nothing. Found: T
}
Subtyping
open class A
open class B : A() —---> Nothing <: C <: B <: A <: Any
class C : B()
class Holder<T>(val value: T) { ... }
Holder<Nothing> ??? Holder<C> ??? Holder<B> ??? Holder<A> ??? Holder<Any>
Subtyping
open class A
open class B : A() —---> Nothing <: C <: B <: A <: Any
class C : B()
class Holder<T>(val value: T) { ... }
Holder<Nothing> <:> Holder<C> <:> Holder<B> <:> Holder<A> <:> Holder<Any>
Generics are invariant!!
val c: C = C() val holderC: Holder<C> = Holder(C())
val b: B = c // C <: B, OK val holderB: Holder<B> = holderC // ERROR: Type mismatch.
V
Required: Holder<B>. Found: Holder<C>.
S
Subtyping
open class A
open class B : A() —---> Nothing <: C <: B <: A <: Any
class C : B()
class Holder<T>(val value: T) { ... }
val holderC: Holder<C> = Holder(C())
val holderB: Holder<B> = holderC //ERROR: Type mismatch. Required: Holder<B>. Found: Holder<C>.
BUT
val holderB: Holder<B> = Holder(C()) // OK, because of casting
Subtyping
class Holder<T> (var value: T?) {
fun pop(): T? = value.also { value = null }
fun push(newValue: T?): T? = value.also { value = newValue }
fun steal(other: Holder<T>) { value = other.pop() }
fun gift(other: Holder<T>) { other.push(pop()) }
}
Holder<Nothing> <:> Holder<C> <:> Holder<B> <:> Holder<A> <:> Holder<Any>
val holderB: Holder<B> = Holder(B())
val holderA: Holder<A> = Holder(null)
holderA.steal(holderB) // ERROR: Type mismatch. Required: Holder<A>. Found: Holder<B>.
holderB.gift(holderA) // ERROR: Type mismatch. Required: Holder<B>. Found: Holder<A>.
Type projection: in
class Holder<T> (var value: T?) {
...
fun gift(other: Holder<in T>) { other.push(pop()) }
}
holderB.gift(holderA) // OK
Type projection: other is a restricted (projected) generic. You can only call methods that accept the type parameter T,
which in this case means that you can only call push().
This is contravariance:
Nothing <: C <: B <: A <: Any
Holder<Nothing> :> Holder<C> :> Holder<B> :> Holder<A> :> Holder<Any>
Type projection: out
class Holder<T> (var value: T?) {
...
fun steal(other: Holder<out T>) { value = other.pop() }
}
holderA.steal(holderB) // OK
Type projection: other is a restricted (projected) generic. You can only call methods that return the type parameter T,
which in this case means that you can only call pop().
This is covariance:
Nothing <: C <: B <: A <: Any
Holder<Nothing> <: Holder<C> <: Holder<B> <: Holder<A> <: Holder<Any>
Type projections
class Holder<T> (var value: T?) {
fun steal(other: Holder<out T>) {
val oldValue = push(other.pop())
other.push(oldValue) // ERROR: Type mismatch. Required: Nothing?. Found: T?.
}
fun gift(other: Holder<in T>) {
val otherValue = other.push(pop())
push(otherValue) // ERROR: Type mismatch. Required: T?. Found: Any?.
}
}
out T returns something that can be cast to T and accepts literally Nothing.
in T accepts something that can be cast to T and returns a meaningless Any?.
Type erasure
At runtime, the instances of generic types do not hold any information about their actual
type arguments. The type information is said to be erased. The same byte-code is used in all
usages of the generic as opposed to C++, where each template is compiled separately for
each type parameter provided.
● Any MutableMap<K, V> becomes MutableMap<*, *> in the runtime*.
● Any Pilot<T : Movable> becomes Pilot<Movable>.
* Actually, in the Kotlin/JVM runtime we have just java.util.Map to preserve compatibility
with Java.
Type erasure
As a corollary, you cannot override a function (in Kotlin/JVM) by changing generic type
parameters:
fun quickSort(collection: Collection<Int>) { ... }
fun quickSort(collection: Collection<Double>) { ... }
Both become quickSort(collection: Collection<*>) and their signatures clash.
But you can use the JvmName annotation:
@JvmName("quickSortInt")
fun quickSort(collection: Collection<Int>) { ... }
fun quickSort(collection: Collection<Double>) { ... }
Nullability in generics
Contrary to common sense, in Kotlin a type parameter specified as T can be nullable.
class Holder<T>(val value: T) { ... } // Notice there is no `?`
val holderA: Holder<A?> = Holder(null) // T = A? and that is OK
To prohibit such behavior, you can use a non-nullable Any as a constraint.
class Holder<T : Any>(val value: T) { ... }
val holderA: Holder<A?> = Holder(null) // ERROR: Type argument is not within its bounds. Expected: Any. Found: A?.
You may also find intersection helpful:
fun <T> elvisLike(x: T, y: T & Any): T & Any = x ?: y
T & Any is populated with all values from T besides null
Inline functions
If they are used as first-class objects, functions are stored as objects, thus requiring memory
allocations, which introduce runtime overhead.
fun foo(str: String, call: (String) -> Unit) {
call(str)
}
fun main() {
foo("Top level function with lambda example") { print(it) }
}
Inline functions
fun foo(str: String, call: (String) -> Unit) {
call(str)
}
public static final void foo(@NotNull String str, @NotNull Function1 call) {
Intrinsics.checkNotNullParameter(str, "str");
Intrinsics.checkNotNullParameter(call, "call");
call.invoke(str);
}
public static final void main() {
foo("Top level function with lambda example", (Function1)foo$call$lambda$1.INSTANCE);
}
This call invokes the print function by passing the string as an argument.
Inline functions
public static final void foo(@NotNull String str, @NotNull Function1 call) {
Intrinsics.checkNotNullParameter(str, "str");
Intrinsics.checkNotNullParameter(call, "call");
call.invoke(str);
}
“Under the hood” an instance of a Function class is created, i.e. allocated:
foo("...", new Function() {
@Override
public void invoke() {
...
}
});
Inline functions
We can use the inline keyword to inline the function, copying its code to the call site:
inline fun foo(str: String, call: (String) -> Unit) {
call(str)
}
fun main() {
foo("Top level function with lambda example", ::print)
}
public static final void main() {
String str$iv = "Top level function with lambda example";
int $i$f$foo = false;
int var3 = false;
System.out.print(str$iv);
}
Inline functions
inline affects not only the function itself, but also all the lambdas passed as arguments.
If you do not want some of the lambdas passed to an inline function to be inlined (for
example, inlining large functions is not recommended), you can mark some of the function
parameters with the noinline modifier.
inline fun foo(str: String, call1: (String) -> Unit, noinline call2: (String) -> Unit) {
call1(str) // Will be inlined
call2(str) // Will not be inlined
}
Inline functions
You can use return in inlined lambdas, this is called non-local return, which can lead to
unexpected behaviour:
inline fun foo(call1: () -> Unit, call2: () -> Unit) {
call1()
call2()
}
fun main() {
println("Step#1")
foo({ println("Step#2")
return },
{ println("Step#3") })
println("Step#4") -> Output:
}
Step#1
Step#2
Inline functions
To prohibit returning from the lambda expression we can mark the lambda as crossinline.
inline fun foo(crossinline call1: () -> Unit, call2: () -> Unit) {
call1()
call2()
}
fun main() {
println("Step#1")
foo({ println("Step#2")
return }, // ERROR: 'return' is not allowed here
{ println("Step#3") })
println("Step#4")
}
return@foo is allowed and
fine, though
Inline functions
crossinline is especially useful when the lambda from an inline function is being called from
another context, for example, if it is used to instantiate a Runnable:
inline fun drive(crossinline specialCall: (String) -> Unit, call: (String) -> Unit) {
val nightCall = Runnable { specialCall("There's something inside you") }
call("I'm giving you a nightcall to tell you how I feel")
thread { nightCall.run() }
call("I'm gonna drive you through the night, down the hills")
}
fun main() {
drive({ System.err.println(it) }) { println(it) }
}
Inline reified functions
Sometimes you need to access a type passed as a parameter:
fun <T: Animal> foo() {
println(T::class) // ERROR: Cannot use 'T' as reified type parameter. Use a class instead —--> add a
param: t: KClass<T>
}
You can use the reified keyword with inline functions:
inline fun <reified T: Animal> foo() {
println(T::class) // OK
}
Note that the compiler has to be able to know the actual type passed as a type argument so
that it can modify the generated bytecode to use the corresponding class directly.
Thanks!
@kotlin | Developed by JetBrains