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)