Kotlin Generic Methods

Generic methods are methods that work on a variety of different type arguments. Some concrete examples of generic methods found in the Kotlin standard library are listOf(), setOf() mapOf(), with(), apply(), etc. All of these methods are compatible with any type of variable and work correctly for each type. A generic method promotes code reuse because a method can be written with a variable type argument that is later substituted with a real type by the compiler.

It’s very easy to write our own generic methods. We need only declare our type arguments inside of angle brackets after the fun keyword. Here is an example of a method that fills a MutableList with a value.

/**
 * Generic method that fills a MutableList with a value. The type argument is declared
 * as T which is later substituted with real types by the compiler.
 */
fun <T> fillList(list : MutableList<T>, value : T, length : Int){
    for (i in 0..length){
        list.add(value)
    }
}

Our fill list isn’t anything fancy, but it is very powerful. The fillList() function is compatible with any type of variable because it uses a generic type, T, as a type argument. Later on, the Kotlin compiler will substitute T with Int, String, or any other type that we need for our purposes.

It is worthwhile to note that all of the typed parameters in fillList() are the same type. In other words, the list variable and the value variable both have to be the same type of argument. We are not allowed to pass in a MutableList<String> along with a value of Int. That would result in a compiler error. If we need multiple type arguments, we need to declare them inside of the angle brackets found after the fun keyword.

We can use our fillList() function like any other function when ready. Here is an example of using our function.

package ch6.genericmethods

/**
 * Generic method that fills a MutableList with a value. The type argument is declared
 * as T which is later substituted with real types by the compiler.
 */
fun <T> fillList(list : MutableList<T>, value : T, length : Int){
    for (i in 0..length){
        list.add(value)
    }
}

fun main(args : Array<String>){
    val intList = mutableListOf<Int>()
    val stringList = mutableListOf<String>()

    fillList(intList, 100, 5)
    fillList(stringList, "Bob", 5)
    //fillList(intList, "Bob", 5) Not OK! intList is MutableList<Int> while Bob is a String

    println(intList)
    println(stringList)
}

When run, this program outputs the following.

[100, 100, 100, 100, 100, 100]
[Bob, Bob, Bob, Bob, Bob, Bob]

Kotlin Generic Classes

Generic classes offer a way for developers to create a class that works with multiple types while retaining type safety. A list is one of the most commonly used generic classes in Kotlin because it allows developers to group a bunch of related objects together in a sequence. The list also works with different types of variables.

//Declare a list of Strings
val stringList = listOf("str1", "str2", "str3")

//Declear a list of ints
val intList = listOf(1, 2, 3, 4)

Both types of lists are created using the listOf() of function. The compiler knows that stringList is a list of Strings because the values passed to listOf are strings. Likewise, the compiler can determine that intList is a list of ints because the values passed are ints.

The listOf() function and the object it returns are generic. In other words, they function and the object are the same for both cases of listOf() as opposed to function overloading which defines different functions based on the type of object. Using generics allows for code reuse because we only write a class or a function once and the compiler substitutes the generic type for a real type when needed.

We will use the following Cook class as an example to demonstrate how to write our own generic classes.

data class Cook<T>(
        var id : T, //Generic property. Can be any type
        var name : String)

The Cook class looks like any other class, except for this <T> that appears after the name of the class. That is the type argument and T is a variable for the Type. The first property of Cook, id, is of type T. Later on, the T parameter can become a String, Int, or basically any valid Kotlin type, including other classes. This allows the Cook class to be flexible about what type of values are stored in the id property.

We need to supply the type of id when we create an instance of Cook.

//bob will be a Cook<String> because the compiler knows that "Bob's Burgers" is a string
val bob = Cook("Bob's Burgers", "Bob")

//jimmy will be a Cook<Int> because the compiler knows that 10001 is an Int
val jimmy = Cook(10001, "Jimmy")

In bob’s case, the compiler knows that “Bob’s Burgers” is a String. So the id in Cook becomes a String and takes on the value of “Bob’s Burgers”. Going forward, bob.id will hold Strings. In jimmy’s case, the compiler knows that 10001 is an Int type, so the id property becomes an Int and jimmy.id will only hold ints.

Using generics protects us from type errors later on. Suppose we were to try assigning a number to bob’s id property.

bob.id = 1000 //BAD! This won't compile

The compiler will refuse to compile the above code because when we created Bob, we used a String for the id property. The compiler check is good because it protects us against ClassCastExceptions at runtime. Since Cook is a generic class, we only need to write the class once and we can use T to represent anytime that is needed for the ID property.

Putting it Together

Here is a working program that shows our Cook generic class in action.

package ch6.genericclasses

data class Cook<T>(
        var id : T, //Generic property. Can be any type
        var name : String)

fun main(args : Array<String>){
    //bob will be a Cook<String> because the compiler knows that "Bob's Burgers" is a string
    val bob = Cook("Bob's Burgers", "Bob")

    //jimmy will be a Cook<Int> because the compiler knows that 10001 is an Int
    val jimmy = Cook(10001, "Jimmy")

    println(bob)
    println(jimmy)

    //The next two lines do not compile
    //bob.id = 1000 (Remember, id is a String in Bob's case and 1000 is an Int so this doesn't compile)
    //jimmy.id = "Jimmy's Pizzeria" (In jimmy's case, id is an Int and "Jimmy's Pizzeria" is a string so this doesn't compile)
}

Output

Cook(id=Bob's Burgers, name=Bob)
Cook(id=10001, name=Jimmy)
%d bloggers like this: