The Programmers Guide To Kotlin - Generics
Written by Mike James   
Monday, 04 June 2018
Article Index
The Programmers Guide To Kotlin - Generics
Generic Constraints

Generics are an essential part of any modern language. Kotlin has generics and at first they look like any other languages generics but, as with most Kotlin features, they have some interesting differences. 

Programmer's Guide To Kotlin Third Edition

kotlin3e360

You can buy it from: Amazon

Contents

  1. What makes Kotlin Special
  2. The Basics:Variables,Primitive Types and Functions 
  3. Control
         Extract: If and When 
  4. Strings and Arrays
  5. The Class & The Object ***NEW!
  6. Inheritance
  7. The Type Hierarchy
  8. Generics
  9. Collections, Iterators, Sequences & Ranges
        Extract: Iterators & Sequences 
  10. Advanced functions 
  11. Anonymous, Lamdas & Inline Functions
  12. Data classes, enums and destructuring
        Extract: Destructuring 
  13. Exceptions, Annotations & Reflection
  14. Coroutines
        Extract: Coroutines 
  15. Working with Java
        Extract: Using Swing
  16. Compose Multiplatform
        Extract: Compose Layout 

<ASIN:B0D8H4N8SK>

Why Generics?

We start out programming not really worrying too much about type and then it becomes central to everything we do. Everything has a type and when you write code it works with a specific set of types. However, some algorithms are the same no matter what type they work with. 

For example, a sorting algorithm doesn't really care what it is sorting as long as it has a way to decide if one element is bigger than another.

Yet in a strongly typed language you have to write a sort routine for each type you want to sort. Of course this isn't what happens. If you want to do something in a type independent way then you can give up strong typing and work with Any and down casting.

Unfortunately an array of Any objects isn't very useful as Any doesn't bring with it many methods and these are all you can use. To make a typed approach to creating generics work you have to have a base class that has the methods you want to use to manipulate all of the derived classes, and class hierarchies generally aren't designed with this in mind. 

For example Number is the top level class for any numeric type in Kotlin, but it doesn't have a comparison operator that is defined in Number. This means you can' t write a sorting function using this approach.

As Number has an equality operator, you can, however, write a find function:

fun find(a:Array<out Number>,Target:Number):Int{
    for( i in a.indices){
        if( a[i]==Target ){
            return i         }
    }
  return -1
}

This works because the array type is Array<out Number> and Number is the base class for all numerics. Don't worry about the use of out in the array declaration, it is explained later.

The problem is that as a Kotlin array is defined as a generic we can't actually avoid generics in this implementation. 

With this function definition we can now call find using an array of elements of any numeric type.

For example:

var b = arrayOf(1, 2, 3)
println(find(b, 2))

or:

var b = arrayOf(1.1, 2.1, 3.2)
println(find(b, 2.1))

However if you try:

var b = arrayOf('A','B','C')
println(find(b, 'A'))

You will find that it doesn't work for Array<Char>. To make it work you would have to change Number to Any as Char isn’t a numeric even if it has a comparison operator.

This is a completely general approach to implementing algorithms that work with a range of objects types. All you have to do is write a function that accepts the base class for all of the objects and work with it. If you want to access any of the methods beyond the base class you will need a down cast.

This approach is workable but not particularly flexible. Generics is a purpose built solution for writing general algorithms that work on a range of types in a type-safe way.

However, it is worth pointing out that Generics don't offer that much above working with variables that reference Any. They often impose restrictions which have to be overcome to implement what you want. They are not the perfect solution to type-free programming.

Basic Generics

The basic idea of generics is very simple – allow type parameters in class, interface and function definitions. To make use of such generic declarations  you have to supply real type for the type parameters at the time they are used. 

A type parameter is defined by being enclosed in <> used as brackets and can be used anywhere a type specification can be used. 

So for example the function that we used earlier to find an element in an array can be re-written:

fun <T> find(a: Array<T>, Target: T): Int {
        for (i in a.indices) {
            if (a[i] == Target) return i
        }
        return -1
    }

The <T> at the start declares the type parameter. You don't have to use T and you can have as many type parameters as you like. Within the function definition, the type parameter is just used as T without angle brackets. Notice that Array is now clearly a generic type as it too has a type parameter that we have to specify using the same angle bracket form. 

Following this we can call the generic function:

println(find<Int>(b, 2))

which will work assuming that b is an array of Ints.

Class and Interface generic declarations work in the same way – add a type parameter in angle brackets at the start of the declaration and use the parameter as if was a type  in the body of the declaration.

For example:

class MyClassA<T>{
    fun myMethod(a:T){        
    }    

declares a class with a single type parameter and a method that accepts a single parameter of that type. You can include a base class and interfaces within the class definition. 

That is, apart from the use of the type parameter, the class declaration is standard. The same is true for a generic interface. 

Now we come to the big restriction inherent in using a generic. As the type parameter is unknown at the time of declaring the class, interface or function, you can't use any of its methods other then the ones supported by Any. 

So for example if you try to write:

fun <T> add(a:T,b:T):T{
        return a+b
}

the result is an error message saying that the system can't work out what a+b is. 

That is:

  • when you are writing a generic <T> is the same as Any.

This isn't unreasonable as without knowing what T is, how can the system determine at compile time how to implement the + operator?

A second problem is that at runtime the type of any parameter passed to the function is lost. That is, if you pass an Int as a and b, you can’t discover this at runtime – the types of the parameters are erased. This is obviously enough called type erasure and it is how Java implements generics.

Generic Properties

As a consequence of not being able to do anything to a type that you don't know also notice that you cannot define a standard generic property within a class. The reason is that it would have to be initialized and what do you initialize an unknown type to?

That is

class MyClass<T>{
  var myProp:T
}

will cause the compiler to insist that you initialize the property or declare it virtual and you cannot create an instance of T as you don't at this stage know what T is. If you set myProp to a nullable type then you can set it to Any:

var myProp:T?=Any()

but this forces all instances to have a property of type Any which isn't really what you want.

What you can do is create a property using the primary constructor as this forces an initialization when the constructor is called, and Kotlin is satisfied that the property is initialized:

class MyClass<T>( val myProp: T) {}

After this you can create an instance with a property of the correct type:

var a=MyClass<Int>(12)
println(a.myProp)
println(a.javaClass)

prints 10 and Int.

So, if you want a generic property, use the primary constructor.

 

kotlinlogo



Last Updated ( Monday, 04 June 2018 )