Menu
Coddy logo textTech

Generic Classes

Part of the Object Oriented Programming section of Coddy's Kotlin journey. Lesson 38 of 57.

A class can have type parameters too. Box<T> holds a value of any type, and each instance fixes the type: a Box<Int> only accepts Int values:

class Box<T>(var value: T) {
    fun describe() = "box with $value"
}

Inside main:

val numberBox = Box(42)
val textBox = Box("hello")
numberBox.value += 1
println(numberBox.describe())
println(textBox.describe())
// numberBox.value = "x"   // error: a String is not an Int

Output:

box with 43
box with hello

Containers are the typical generic classes. One Stack<T> implementation works for every element type, and the compiler checks what goes in and comes out:

class Stack<T> {
    private val items = mutableListOf<T>()
    val size: Int
        get() = items.size

    fun push(item: T) { items.add(item) }
    fun pop(): T? = items.removeLastOrNull()
    fun peek(): T? = items.lastOrNull()
}

Inside main:

val stack = Stack<String>()
stack.push("a")
stack.push("b")
println(stack.pop())
println(stack.peek())
println(stack.size)

Output:

b
a
1

The type argument is inferred from the constructor arguments when there are any, as in Box(42). Otherwise write it: Stack<String>(). Instances with different type arguments are different types, so a Stack<String> is never mixed up with a Stack<Int>. Functions of a generic class can also add type parameters of their own:

class Box<T>(val value: T) {
    fun <R> map(f: (T) -> R): Box<R> = Box(f(value))
}

Inside main:

val length = Box("kotlin").map { it.length }
println(length.value + 1)

Output:

7

Generic interfaces follow the same rules. A class can implement one generically, or for one particular type argument:

interface Repository<T> {
    fun add(item: T)
    fun all(): List<T>
}
class MemoryRepository<T> : Repository<T> {
    private val items = mutableListOf<T>()
    override fun add(item: T) { items.add(item) }
    override fun all() = items.toList()
}
class TagRepository : Repository<String> {
    private val tags = sortedSetOf<String>()
    override fun add(item: String) { tags.add(item.lowercase()) }
    override fun all() = tags.toList()
}

Inside main:

val ids = MemoryRepository<Int>()
ids.add(3); ids.add(1)
val tags = TagRepository()
tags.add("Kotlin"); tags.add("JVM")
println("${ids.all()} ${tags.all()}")

Output:

[3, 1] [jvm, kotlin]
challenge icon

Challenge

Easy

Write the generic class History<T>(limit) that remembers the last limit items. add(item) appends an item and drops the oldest when there are more than limit. latest is the newest item or null, and all() returns the items from oldest to newest as a read-only list. Then write the extension History<Int>.average(), which exists only for histories of numbers and returns the integer average of the kept numbers (0 when empty).

The supplied code reads the limit, a line of words and a line of numbers. It fills a History<String> and a History<Int> and prints the kept words, the latest word, the kept numbers, their average and empty latest: null.

Your code goes in History.kt. Main.kt holds the supplied input/output code and cannot be edited.

Try it yourself

fun main() {
    // Supplied input/output code: keep it as it is
    val input = generateSequence(::readLine).toList()
    val limit = input[0].toInt()
    val words = History<String>(limit)
    for (w in input[1].split(" ")) words.add(w)
    val numbers = History<Int>(limit)
    for (n in input[2].split(" ")) numbers.add(n.toInt())
    println("words: ${words.all()}")
    println("latest word: ${words.latest}")
    println("numbers: ${numbers.all()}")
    println("average: ${numbers.average()}")
    println("empty latest: ${History<Char>(3).latest}")
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Kotlin playground