# Introduction to Generics
Generics create parameterized types: components that work across multiple types.
The term “generic” means “pertaining or appropriate to large groups of classes.” The original intent of generics in programming languages was to provide the programmer maximum expressiveness when writing classes or functions, by loosening type constraints on those classes or functions.
One of the most compelling initial motivations for generics is to create collection classes, which you’ve seen in the List
s, Set
s and Map
s used for the examples in this book. A collection is an object that holds other objects. Many programs require you to hold a group of objects while you use them, so collections are one of the most reusable of class libraries.
Let’s look at a class that holds a single object. This class specifies the exact type of that object:
// IntroGenerics/RigidHolder.kt
package introgenerics
import atomictest.eq
data class Automobile(val brand: String)
class RigidHolder(private val a: Automobile) {
fun getValue() = a
}
fun main() {
val holder = RigidHolder(Automobile("BMW"))
holder.getValue() eq
"Automobile(brand=BMW)"
}
RigidHolder
is not a particularly reusable tool; it can’t hold anything but an Automobile
. We would prefer not to write a new type of holder for every different type. To achieve this, we use a type parameter instead of Automobile
.
To define a generic type, add angle brackets (<>
) containing one or more generic placeholders and put this generic specification after the class name. Here, the generic placeholder T
represents the unknown type and is used within the class as if it were a regular type:
// IntroGenerics/GenericHolder.kt
package introgenerics
import atomictest.eq
class GenericHolder<T>( // [1]
private val value: T
) {
fun getValue(): T = value
}
fun main() {
val h1 = GenericHolder(Automobile("Ford"))
val a: Automobile = h1.getValue() // [2]
a eq "Automobile(brand=Ford)"
val h2 = GenericHolder(1)
val i: Int = h2.getValue() // [3]
i eq 1
val h3 = GenericHolder("Chartreuse")
val s: String = h3.getValue() // [4]
s eq "Chartreuse"
}
- [1]
GenericHolder
stores aT
, and its member functiongetValue()
returns aT
.
When you call getValue()
as in [2], [3] or [4] , the result is automatically the right type.
It seems like we might be able to solve this problem with a “universal type”—a type that is the parent of all other types. In Kotlin, this universal type is called Any
. As the name implies, Any
allows any type of argument. If you want to pass a variety of types to a function and they have nothing in common, Any
solves the problem.
At a glance, it looks like we might be able to use Any
instead of T
in GenericHolder.kt
:
// IntroGenerics/AnyInstead.kt
package introgenerics
import atomictest.eq
class AnyHolder(private val value: Any) {
fun getValue(): Any = value
}
class Dog {
fun bark() = "Ruff!"
}
fun main() {
val holder = AnyHolder(Dog())
val any = holder.getValue()
// Doesn't compile:
// any.bark()
val genericHolder = GenericHolder(Dog())
val dog = genericHolder.getValue()
dog.bark() eq "Ruff!"
}
Any
does in fact work for simple cases, but as soon as we need the specific type—to call bark()
for the Dog
—it doesn’t work because we lose track of the fact that it’s a Dog
when it is assigned to the Any
. When we pass a Dog
as an Any
, the result is just an Any
, which has no bark()
.
Using generics retains the information that, in this case, we actually have a Dog
, which means we can perform Dog
operations on the object returned by getValue()
.
# Generic Functions
To define a generic function, specify a generic type parameter in angle brackets before the function name:
// IntroGenerics/GenericFunction.kt
package introgenerics
import atomictest.eq
fun <T> identity(arg: T): T = arg
fun main() {
identity("Yellow") eq "Yellow"
identity(1) eq 1
val d: Dog = identity(Dog())
d.bark() eq "Ruff!"
}
d
has type Dog
because identity()
is a generic function and returns a T
.
The Kotlin standard library contains many generic extension functions for collections. To write a generic extension function, put the generic specification before the receiver. For example, notice how first()
and firstOrNull()
are defined:
// IntroGenerics/GenericListExtensions.kt
package introgenerics
import atomictest.eq
fun <T> List<T>.first(): T {
if (isEmpty())
throw NoSuchElementException("Empty List")
return this[0]
}
fun <T> List<T>.firstOrNull(): T? =
if (isEmpty()) null else this[0]
fun main() {
listOf(1, 2, 3).first() eq 1
val i: Int? = // [1]
listOf(1, 2, 3).firstOrNull()
i eq 1
val s: String? = // [2]
listOf<String>().firstOrNull()
s eq null
}
first()
and firstOrNull()
work with any kind of List
. To return a T
, they must be generic functions.
Notice how firstOrNull()
specifies a nullable return type. Line [1] shows that calling the function on List<Int>
returns the nullable type Int?
. Line [2] shows that calling firstOrNull()
on List<String>
returns String?
. Kotlin requires the ?
on lines [1] and [2]—take them out and see the error messages.
Exercises and solutions can be found at www.AtomicKotlin.com.