Generics
Generics
Generics
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
Does the quickSort algorithm actually care what is it sorting? No, as long as it can compare two
values against each other.
quickSort(listOf(1, 2, 3)) // OK
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.
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.
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).
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?
interface Holder<T> {
fun push(newValue: T) // consumes an element
interface Holder<T> {
fun push(newValue: T) // consumes an element
interface Holder<T> {
fun push(newValue: T)// consumes an element: OK
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
}
open class A
open class B : A() —//-> Nothing /: C /: B /: A /: Any
class C : B()
open class A
open class B : A() —//-> Nothing /: C /: B /: A /: Any
class C : B()
open class A
open class B : A() —//-> Nothing /: C /: B /: A /: Any
class C : B()
BUT
val holderB: Holder<B> = Holder(C()) // OK, because of casting
Subtyping
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
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
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.
* 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>) { //. }
If they are used as first-class objects, functions are stored as objects, thus requiring memory
allocations, which introduce runtime overhead.
fun main() {
foo("Top level function with lambda example") { print(it) }
}
Inline functions
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);
}
We can use the inline keyword to inline the function, copying its code to the call site:
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:
To prohibit returning from the lambda expression we can mark the lambda as crossinline.
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
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.
open class A
class B : A()
class C : A() { fun consume(other: A): C = this }
fun main() {
val wtf = mutableListOf<A>()
val src = mapOf(3.14 to B(), 2 to B(), "Hello" to B())
val c = C()
funny(src.values.iterator(), wtf, c) { r, t -> r.consume(t) }
}
(in)Variance
open class A
class C : B()
A hierarchy is in place (and don’t forget about the same hierarchy with nullability):
val holderB: Holder<B> = holderC // Error: Type mismatch. Required: Holder<B>. Found: Holder<C>.
NB: code below works, since C() passed as an argument is being cast to B, nothing to do with variance.
val kotlinIsSmart: Holder<B> = Holder(C())
More
But why not? B can be easily cast to A inside steal or gift and everything should be fine.
Thanks!