© Peter Späth 2019
Peter SpäthLearn Kotlin for Android Developmenthttps://doi.org/10.1007/978-1-4842-4467-8_13

13. About Type Safety: Generics

Peter Späth1 
(1)
Leipzig, Germany
 
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()

Note

Generics are compile-time constructs. In the code the compiler generates, no generics information appears. This effect is commonly referred to as type erasure.

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.

Note

The out variance for classes also gets called covariance, and the in variance is called contravariance .

The name declaration-side variance stems from declaring the in or out variance in the declaration of the class. Other languages, such as Java, use a different type of variance that takes effect while using the class and hence gets called use-side variance.

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
<T : SpecificType>
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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.67.251