Generics is a term used for denoting a set of language features that allow us to add type parameters to
types. Consider, for example, a simple class with a function for adding elements in the form of
Int objects:
class AdderInt {
fun add(i:Int) {
...
}
}
and another one for
String objects:
class AdderString {
fun add(s:String) {
...
}
}
Apart from what happens inside the
add() function, these classes look suspiciously similar, so we could think of a language feature that abstracts the type for the element to add. Such a language feature exists in Kotlin, and it is called
generics
. A corresponding construct reads as follows:
class Adder<T> {
fun add(toAdd:T) {
...
}
}
where T is the type parameter
. Here instead of T any other name could be used for the type parameter, but in many projects you will often find T, R, S, U, A, or B as type parameter names.
For instantiating such classes, the type must be known to the compiler. Either you have to explicitly specify the type, as in
class Adder<T> {
fun add(toAdd:T) {
...
}
}
val intAdder = Adder<Int>()
val stringAdder = Adder<String>()
or the compiler must be able to infer the type, as in
class Adder<T> {
fun add(toAdd:T) {
...
}
}
val intAdder:Adder<Int> = Adder()
val stringAdder:Adder<String> = Adder()
We’ve already used such a generic type a couple of times in the book. You might remember as a holder for two data elements we talked about the
Pair type, which is parameterized:
val p1 = Pair<String, String>("A", "B")
val p2 = Pair<Int,String>(1, "A")
Of course, we also talked about various collection types, for example:
val l1: List<String> = listOf("A","B","C")
val l2: MutableList<Int> = mutableListOf(1, 2, 3)
Until now we just took generics as they are, without further explaining them. After all, writing List<String>, the deduction that we are talking about a list of strings is apparent.
The story gets interesting once we start more thoroughly looking at collections. The question is this: If we have a MutableList<Any> and a MutableList<String>, how do they relate? Can we write val l:MutableList<Any> = mutableListOf<String>("A", "B")? Or in other words, is MutableList<-String> a subclass of MutableList<Any>? It isn’t, and in the rest of this chapter we talk about generics in depth and try to understand type relationships.
Simple Generics
First, let’s address the basics. To type-parameterize a class or an interface, you add a comma-separated list of formal
type parameters inside angle brackets after the type name:
class TheClass<[type-list]> {
[class-body]
}
interface TheInterface<[type-list]> {
[interface-body]
}
Inside the class or interface, including any constructor and
init{} block, you can then use the
type parameters like any other type. For example:
class TheClass<A, B>(val p1: A, val p2: B?) {
constructor(p1:A) : this(p1, null)
init {
var x:A = p1
...
}
fun function(p: A) : B? = p2
}
Exercise 1
Similar to the Pair class, create a class Quadruple that can hold four data elements. Create an instance with sample Int, Int, Double, and String type elements.
Declaration-Side Variance
If we talk about generics, the term
variance denotes the ability to use more specific or less specific types in
assignments. Knowing that
Any is less specific compared to
String, variance shows up in the question of whether one of the following is possible:
class A<T> { ... }
var a = A<String>()
var b = A<Any>()
a = b // variance?
... or ...
b = a // variance?
Why is that important for us? The answer to that question becomes clear if we look at type safety. Consider the following code snippet:
class A<T> {
fun add(p:T) { ... }
}
var a = A<String>()
var b = A<Any>()
b = a // variance?
b.add(37)
Adding 37 to a A<Any> does not pose a problem, because any type is a subclass of Any. However, because b by virtue of b = a now points to an instance of A<String>, we’ll get a runtime error, because 37 is not a string. The Kotlin compiler recognizes this problem and doesn’t allow the b = a assignment
.
Likewise, assigning
a = b also poses a problem. This one is even more obvious, because
a only is for
String elements and cannot handle an
Int typed value, as
b does.
class A<T> {
fun extract(): T = ...
}
var a = A<String>()
var b = A<Any>()
a = b // variance?
val extracted:String = a.extract()
The a.extract() in the last statement could evaluate to both an Any and a String type, because b and now a may, for example, contain an Int object, but a is not allowed to contain an Int object, because it can handle String elements
only. Hence Kotlin does not allow a = b either.
What can we do? To not allow any variance could be an option, but this would be too harsh. Again, looking at the first sample with the
b = a assignment we can see that writing to
b causes the error. How about reading? Consider this:
class A<T> {
fun extract(): T = ...
}
var a = A<String>()
var b = A<Any>()
b = a // variance?
val extracted:String = b.extract()
The last operation now is safe as types are concerned, so we actually should not have a problem here.
The exact opposite case, taking the
a = b sample and applying a writing instead of a reading operation, as in
class A<T> {
fun add(p:T) { ... }
}
var a = A<String>()
var b = A<Any>()
a = b // variance?
a.add("World")
should not pose a problem either. To both a and b we can add strings.
To make this kind of variance possible, Kotlin allows us to add a
variance annotation to a generic
parameter. The first example with
b = a does compile if we add the
out annotation to the type parameter:
class A<out T> {
fun extract(): T = ...
}
var a = A<String>()
var b = A<Any>()
b = a // variance? YES!
val extracted:String = b.extract()
// OK, because we are reading!
The second example with
a = b compiles if we add the
in annotation to the type
parameter:
class A<in T> {
fun add(p:T) { ... }
}
var a = A<String>()
var b = A<Any>()
a = b // variance? YES!.add("World")
// OK, because we are writing!
So with the in or out variance annotation added to the type parameters, and confining class operations to allow for either only an input of the generic type or only an output of the generic type, variance is possible in Kotlin! If you need both, you can use a different construct, as covered in the section “Type Projections” later in this chapter.
Variance for Immutable Collections
Because immutable collections cannot be written to, Kotlin automatically makes them covariant. If you prefer, you can think of Kotlin implicitly adding the out variance annotation to immutable collections.
Due to this fact, a
List<SomeClass> can be assigned to a
List<SomeClassSuper> where
SomeClassSuper is a superclass of
SomeClass. For example:
val coll1 = listOf("A", "B") // immutable
val coll2:List<Any> = coll1 // allowed!
Type Projections
In the previous section we saw that for the out style variance the corresponding class is not allowed to have functions with the generic type as a function parameter, and that for the in style variance we accordingly cannot have a function returning the generic type. This is, of course, unsatisfactory if we need both kinds of functions in a class. Kotlin also has an answer for this type of requirement. It is called type projection and because it aims at variance while using different functions of a class, it is the Kotlin equivalent of use-side variance.
The idea goes as follows: We still use the
in and
out variance annotations, but instead of declaring them for the whole class we add them to function parameters. We slightly rewrite the example from the previous section and add
in and
out variance annotations:
class Producer<T> {
fun getData(): Iterable<T>? = null
}
class Consumer<T> {
fun setData(p:Iterable<T>) { }
}
class A<T> {
fun add(p:Producer<out T>) { }
fun extractTo(p:Consumer<in T>) { }
}
The
out in the
add() functions
says that we need an object that produces
T objects, and the
in in the
extractTo() function
designates an object that
consumes T objects. Let us look at some client code:
var a = A<String>()
var b = A<Any>()
var inputStrings = Producer<String>()
var inputAny = Producer<Any>()
a.add(inputStrings)
a.add(inputAny) // FAILS!
b.add(inputStrings) // only because o "out"
b.add(inputAny)
var outputAny = Consumer<Any>()
var outputStrings = Consumer<String>()
a.extractTo(outputAny) // only because of "in" a.extractTo(outputStrings)
b.extractTo(outputAny)
b.extractTo(outputStrings) // FAILS!
You can see that a.add(inputAny) fails because inputAny produces all kinds of objects but a can only take String objects. Similarly, b.extractTo(outputStrings) fails because b contains any kind of object but outputStrings can only receive String objects. This so far has nothing to do with variance. The story gets interesting for b.add(inputStrings). The behavior to allow for strings to be added to A<Any> certainly makes sense, but it only works because we added the out projection to the function parameter. Similarly, a.extractTo(outputAny), although certainly desirable, only works because of the in projection.
Star Projections
If you have a class or an interface with
in or
out variance
annotations, you can use the special wildcard
*, which means the following:
For the out variance annotation, * means out Any?.
For the in variance annotation, * means in Nothing.
Remember that Any is the superclass of any class, and Nothing is the subclass of any class.
For example:
interface Interf<in A, out B> {
...
}
val x:Interf<*, Int> = ...
// ... same as Interf<in Nothing, Int>
val y:Interf<Int, *> = ...
// ... same as Interf<Int, out Any?>
You use the star wildcard in cases where you know nothing about the type, but still want to satisfy variance semantics prescribed by class or interface declarations.
Generic Functions
Functions in Kotlin can also be generic, which means their parameters or some of their parameters can have a generic type. In such cases, the generic type designators must be added as a comma-separated list in angle brackets after the
function keyword. The generic types can also show up in the function’s return type. Here is an example.
fun <A> fun1(par1:A, par2:Int) {
...
}
fun <A, B> fun2(par1:A, par2:B) {
...
}
fun <A> fun3(par1:String) : A {
...
}
fun <A> fun4(par1:String) : List<A> {
...
}
To call such a
function, the concrete type principally must be specified after the function’s name in angle brackets:
fun1<String>("Hello", 37)
fun2<Int, String>(37, "World")
val s:String = fun3<String>("A")
However, as is often the case in Kotlin, the type arguments can be omitted if Kotlin can infer the type.
Generic Constraints
Until now there was no restriction to the type a generic type
identifier could be mapped to during instantiation. Therefore in
class TheClass<T> the
T generic type could be anything, a
TheClass<Int>,
TheClass<String>,
TheClass<Any>, or whatever. It is, however, possible to restrict the type to a certain class or interface or one of its
subtypes. For that aim you write
as in
class <T : Number> { ... }
which confines T to a Number or any of its subclasses, like Int or Double.
This is very useful. Consider, for example, a class that allows us to add something to a
Double property.
class Adder<T> {
var v:Double = 0.0
fun add(value:T) {
v += value.toDouble()
}
}
Do you see why this code is illegal? We say that value is of type T, but it is not known to the class what T happens to be during instantiation, so it is not clear whether or not a T.toDouble() function actually exists. Because we know that after compilation all types are erased, the compiler has no chance to check whether there is a toDouble() and it hence marks the code as illegal. If you look at the API documentation you will find out that Int, Long, Short, Byte, Float, and Double all are subclasses of kotlin.Number and they all have a toDouble() function
. If we had a way to say that T is a Number or a subclass thereof, we could thus make the code legal.
Kotlin does have a way to confine generic
types that way, and it reads
<T : SpecificType>. Because
T then is confined to
SpecificType or any subtype of it lower in the type hierarchy, this is also said to be an
upper type bound. To make our
Adder class legal all we have to do is write
class Adder<T : Number> {
var v:Double = 0.0
fun add(value:T) {
// T is a Number, so it _has_ a toDouble()
v += value.toDouble()
}
}
Such type
constraints can also be added to generic functions, so we actually could rewrite the
Adder class to:
class Adder {
var v:Double = 0.0
fun <T:Number> add(value:T) {
v += value.toDouble()
}
}
This has the particular advantage that the generic type does not need to be resolved during instantiation.
val adder = Adder()
adder.add(37)
adder.add(3.14)
adder.add(1.0f)
Note that unlike class inheritance, type bounds can be multiply
declared. This just can’t happen inside the angle brackets, but there is a special construct for handling such cases.
class TheClass<T> where T : UpperBound1,
T : UpperBound2, ...
{
...
}
or
fun <T> functionName(...) where T : UpperBound1,
T : UpperBound2, ...
{
...
}
for generic functions.
Something that you might have to get used to but that helps for generic constraints having generic parameters
themselves, is that generic classes might show up on both sides of the colon (
:) It is thus completely acceptable to write
class TheClass <T : Comparable<T>> {
...
}
to express that T must be a subclass of Comparable
.
Exercise 2
Write a generic class Sorter with a type parameter T and suitable type bound, which has a property val list:MutableList<T> and a function fun add(value:T). With each function invocation, the parameter must be added to the list and the list property must be sorted according to its natural sorting order.