
This is a chapter from the book Kotlin Essentials. You can find it on LeanPub or Amazon. It is also available as a course.
List, instead of specific lists with specific parameter types, like List<String> or List<Int>. The List type in Java accepts all kinds of values; when you ask for a value at a certain position, the result type is Object (which, in Java, is the supertype of all the types).// Java
List names = new ArrayList();
names.add("Alex");
names.add("Ben");
names.add(123); // this is incorrect, but compiles
for(int i = 0; i < names.size(); i++){
String name= (String) names.get(i); // exception at i==2
System.out.println(name.toUpperCase());
}
- generic functions,
- generic classes,
- generic interfaces.
fun keyword. By convention, type parameter names are capitalized. When a function defines a type parameter, we have to specify the type arguments when calling this function. The type parameter is a placeholder for a concrete type; the type argument is the actual type that is used when a function is called. To specify type arguments explicitly, we also use angle brackets.fun <T> a() {} // T is type parameter a<Int>() // Int is used here as a type argument a<String>() // String is used here as a type argument
T (from "type"); if there are multiple type arguments, they are called T with consecutive numbers. However, this practice is not a fixed rule, and there are many other conventions for naming type parameters.fun <T> a() {} fun <T1, T2> b() {}
fun <T> a() {} fun <T1, T2> b() {} fun <T> c(t: T) {} fun <T1, T2> d(a: T1, b: T2) {} fun <T> e(): T = TODO() fun main() { // Type arguments specified explicitly a<Int>() a<String>() b<Double, Char>() b<Float, Long>() // Inferred type arguments c(10) // The inferred type of T is Int d("AAA", 10.0) // The inferred type of T1 is String, and of T2 is Double val e: Boolean = e() // The inferred type of T is Boolean }
import kotlin.random.Random // The result type is the same as the argument type fun <T> id(value: T): T = value // The result type is the closest supertype of arguments fun <T> randomOf(a: T, b: T): T = if (Random.nextBoolean()) a else b fun main() { val a = id(10) // Inferred a type is Int val b = id("AAA") // Inferred b type is String val c = randomOf("A", "B") // Inferred c type is String val d = randomOf(1, 1.5) // Inferred d type is Number }
ValueWithHistory<String> and then call setValue in the example below, you must use an object of type String; when you call currentValue, the result object will be typed as String; and when you call history, its result is of type List<String>. It’s the same for all other possible type arguments.class ValueWithHistory<T>( private var value: T ) { private var history: List<T> = listOf(value) fun setValue(value: T) { this.value = value this.history += value } fun currentValue(): T = value fun history(): List<T> = history } fun main() { val letter = ValueWithHistory<String>("A") // The type of letter is ValueWithHistory<String> letter.setValue("B") // letter.setValue(123) <- this would not compile val l = letter.currentValue() // the type of l is String println(l) // B val h = letter.history() // the type of h is List<String> println(h) // [A, B] }
val letter = ValueWithHistory("A") // The type of letter is ValueWithHistory<String>
Any as a type argument. We can specify this by specifying the type of variable letter as ValueWithHistory<Any>.val letter: ValueWithHistory<Any> = ValueWithHistory("A") // The type of letter is ValueWithHistory<Any>
ArrayList class from the Standard Library (stdlib). It is generic, so when we create an instance from this class we need to specify the types of elements. Thanks to that, Kotlin protects us by expecting only values with accepted types to be added to the list, and Kotlin uses this type when we operate on the elements in the list.fun main() { val letters = ArrayList<String>() letters.add("A") // the argument must be of type String letters.add("B") // the argument must be of type String // The type of letters is List<String> val a = letters[0] // the type of a is String println(a) // A for (l in letters) { // the type of l is String println(l) // first A, then B } }
ValueWithHistory<String?>. In such a case, the null value is a perfectly valid option.fun main() { val letter = ValueWithHistory<String?>(null) letter.setValue("A") letter.setValue(null) val l = letter.currentValue() // the type of l is String? println(l) // null val h = letter.history() // the type of h is List<String?> println(h) // [null, A, null] val letters = ArrayList<String?>() letters.add("A") letters.add(null) println(letters) // [A, null] val l2 = letters[1] // the type of l2 is String? println(l2) // null }
T might or might not be nullable, depending on the type argument, but the type T? is always nullable. We can assign null to variables of the type T?. Nullable generic type parameter T? must be unpacked before using it as T.class Box<T> { var value: T? = null fun getOrThrow(): T = value!! }
List<Int?>), we might specify a definitely non-nullable variant of this type by adding & Any after the type parameter. In the example below, the method orThrow can be invoked on any value, but it unpacks nullable types into non-nullable ones.fun <T> T.orThrow(): T & Any = this ?: throw Error() fun main() { val a: Int? = if (Random.nextBoolean()) 42 else null val b: Int = a.orThrow() val c: Int = b.orThrow() println(b) }
List interface.interface List<out E> : Collection<E> { override val size: Int override fun isEmpty(): Boolean override fun contains(element: @UnsafeVariance E): Boolean override fun iterator(): Iterator<E> override fun containsAll( elements: Collection<@UnsafeVariance E> ): Boolean operator fun get(index: Int): E fun indexOf(element: @UnsafeVariance E): Int fun lastIndexOf(element: @UnsafeVariance E): Int fun listIterator(): ListIterator<E> fun listIterator(index: Int): ListIterator<E> fun subList(fromIndex: Int, toIndex: Int): List<E> }
Theoutmodifier and theUnsafeVarianceannotation will be explained in the book Advanced Kotlin.

For
List<String> type, methods like contains expect an argument of type String, and methods like get declare String as the result type.
For
List<String>, methods like filter can infer String as a lambda parameter.Dog that inherits from Consumer<DogFood>, as shown in the snippet below. The interface Consumer expects a method consume with the type parameter T. This means our Dog must override the consume method with an argument of type DogFood. It must be DogFood because we implement the Consumer<DogFood> type, and the parameter type in the interface Consumer must match the type argument DogFood. Now, when you have an instance of Dog, you can up-cast it to Consumer<DogFood>.interface Consumer<T> { fun consume(value: T) } class DogFood class Dog : Consumer<DogFood> { override fun consume(value: DogFood) { println("Mlask mlask") } } fun main() { val dog: Dog = Dog() val consumer: Consumer<DogFood> = dog }
A inherits from C<Int> and implements I<String>.open class C<T> interface I<T> class A : C<Int>(), I<String> fun main() { val a = A() val c: C<Int> = a val i: I<String> = a }
MessageListAdapter presented below, which inherits from ArrayAdapter<String>.class MessageListAdapter( context: Context, val values: List<ClaimMessage> ) : ArrayAdapter<String>( context, R.layout.row_messages, values.map { it.title }.toTypedArray() ) { fun getView( position: Int, convertView: View?, parent: ViewGroup? ): View { // ... } }
A is generic and uses its type parameter T as an argument for both C and I. This means that if you create A<Int>, you will be able to up-cast it to C<Int> or I<Int>. However, if you create A<String>, you will be able to up-cast it to C<String> or to I<String>.open class C<T> interface I<T> class A<T> : C<T>(), I<T> fun main() { val a: A<Int> = A<Int>() val c1: C<Int> = a val i1: I<Int> = a val a1: A<String> = A<String>() val c2: C<String> = a1 val i2: I<String> = a1 }
MutableList<Int> implements List<Int>, which implements Collection<Int>, which implements Iterable<Int>.interface Iterable<out T> { // ... } interface MutableIterable<out T> : Iterable<T> { // ... } interface Collection<out E> : Iterable<E> { /// ... } interface MutableCollection<E> : Collection<E>,MutableIterable<E>{ // ... } interface List<out E> : Collection<E> { // ... } interface MutableList<E> : List<E>, MutableCollection<E> { // ... }
open class C<T> interface I<T> class A<T> : C<Int>(), I<String> fun main() { val a1: A<Double> = A<Double>() val c1: C<Int> = a1 val i1: I<String> = a1 }
List<String> becomes List, and emptyList<Double> becomes emptyList. The process of losing type arguments is known as type erasure. Due to this process, type parameters have some limitations compared to regular types. You cannot use them for is checks; you cannot reference them[^21_2]; and you cannot use them as reified type arguments[^21_3].import kotlin.reflect.typeOf fun <T> example(a: Any) { val check = a is T // ERROR val ref = T::class // ERROR val type = typeOf<T>() // ERROR }
import kotlin.reflect.typeOf inline fun <reified T> example(a: Any) { val check = a is T val ref = T::class val type = typeOf<T>() }
maxOf function, which returns the biggest of its arguments. To find the biggest one, the arguments need to be comparable. So, next to the type parameter, we can specify that we accept only types that are a subtype of Comparable<T>.import java.math.BigDecimal fun <T : Comparable<T>> maxOf(a: T, b: T): T { return if (a >= b) a else b } fun main() { val m = maxOf(BigDecimal("10.00"), BigDecimal("11.00")) println(m) // 11.00 class A maxOf(A(), A()) // Compilation error, // A is not Comparable<A> }
ListAdapter class below, which expects a type argument that is a subtype of ItemAdapter.class ListAdapter<T : ItemAdapter>(/*...*/) { /*...*/ }
T is constrained as a subtype of Iterable<Int>, we know that we can iterate over an instance of type T, and that elements returned by the iterator will be of type Int. When we are constrained to Comparable<T>, we know that this type can be compared with another instance of the same type. Another popular choice for a constraint is Any, which means that a type can be any non-nullable type.where to set more constraints. We add it after the class or function name, and we use it to specify more than one generic constraint for a single type.interface Animal { fun feed() } interface GoodTempered { fun pet() } fun <T> pet(animal: T) where T : Animal, T : GoodTempered { animal.pet() animal.feed() } class Cookie : Animal, GoodTempered { override fun pet() { // ... } override fun feed() { // ... } } class Cujo : Animal { override fun feed() { // ... } } fun main() { pet(Cookie()) // OK pet(Cujo()) //COMPILATION ERROR, Cujo is not GoodTempered }
*, which accepts any type. There are two situations where this is useful. The first is when you check if a variable is a list. In this case, you should use the is List<*> check. Star projection should be used in such a case because of type erasure. If you used List<Int>, it would be compiled to List under the hood anyway. This means a list of strings would pass the is List<Int> check. Such a check would be confusing and is illegal in Kotlin. You must use is List<*> instead.fun main() { val list = listOf("A", "B") println(list is List<*>) // true println(list is List<Int>) // Compilation error }
List<*> when you want to express that you want a list, no matter what the type of its elements. When you get elements from such a list, they are of type Any?, which is the supertype of all the types.fun printSize(list: List<*>) { println(list.size) } fun printList(list: List<*>) { for (e in list) { // the type of e is Any? println(e) } }
Any? type argument. To see this, let's compare MutableList<Any?> and MutableList<*>. Both of these types declare Any? as generic result types. However, when elements are added, MutableList<Any?> accepts anything (Any?), but MutableList<*> accepts Nothing, so it does not accept any values.fun main() { val l1: MutableList<Any?> = mutableListOf("A") val r1 = l1.first() // the type of r1 is Any? l1.add("B") // the expected argument type is Any? val l2: MutableList<*> = mutableListOf("A") val r2 = l2.first() // the type of r2 is Any? l2.add("B") // ERROR, // the expected argument type is Nothing, // so there is no value that might be used as an argument }
Any? in all the out-positions (result types), and it will be treated as Nothing in all the in-positions (parameter types)._ as a type argument. This operator specifies that we want to infer a type argument.inline fun <K, reified V> Map<K, *> .filterValueIsInstance(): Map<K, V> = filter { it.value is V } as Map<K, V> fun main() { val props = mapOf( "name" to "Alex", "age" to 25, "city" to "New York" ) // One type argument inferred with _, one specified val strProps = props.filterValueIsInstance<_, String>() println(strProps) // {name=Alex, city=New York} }
out and in).[^21_2]: Class and type references are explained in the book Advanced Kotlin.
[^21_3]: Reified type arguments are explained in the book Functional Kotlin.
