Generics Workbook

Nine short exercises on Kotlin generics — generic functions and classes, declaration-site variance with out and in, star projections, and reified type parameters.

Practice problems for Kotlin Generics: Variance Without the Wildcards. Each takes a minute or two. The generic-function exercises auto-check: implement the function and press Run — hidden tests go green when you’re right and red (with a hint) when you’re not, on JetBrains’ Kotlin server. The variance and declaration exercises stay attempt-then-reveal: press Run to compile, then open Show answer to check yourself. Nothing here is a trick question, just direct practice of the syntax from the lesson.

the basics

1. A generic function

Implement firstOrNull so it returns the first element of a List of any element type, or null when empty.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun firstOrNull() { Assert.assertEquals("first element of a non-empty list", 10, firstOrNull(listOf(10, 20, 30))) Assert.assertEquals("null when the list is empty", null, firstOrNull(emptyList<Int>())) } } //sampleStart fun <T> firstOrNull(list: List<T>): T? = null // TODO: return the first element, or null when the list is empty //sampleEnd
Show answer Hide answer
fun <T> firstOrNull(list: List<T>): T? =
    if (list.isEmpty()) null else list[0]

2. A generic class

Declare a Box that holds one value of any type.

Try it — edit, then press Run fun main() { // TODO: make Box generic so it can hold a value of ANY type class Box(val value: String) println(Box("hello").value) }
Show answer Hide answer
class Box<T>(val value: T)

3. Inference at the call site

Given the Box above, create one holding "hello". Do you write the type argument?

Try it — edit, then press Run fun main() { class Box<T>(val value: T) // TODO: create a Box holding "hello" — do you need to write the type argument? val b = Box("") println(b.value) }
Show answer Hide answer
val b = Box("hello")   // T inferred as String

No — the compiler infers T from the argument.

variance

4. A producer

Declare an interface Source that only ever produces a T (a next(): T method), marked so that a Source of Dog is usable as a Source of Animal.

Try it — edit, then press Run interface Source<T> { fun next(): T } open class Animal class Dog : Animal() fun main() { val dogs: Source<Dog> = object : Source<Dog> { override fun next() = Dog() } // TODO: mark T so the next line would compile // val animals: Source<Animal> = dogs println(dogs.next()) }
Show answer Hide answer
interface Source<out T> {
    fun next(): T
}

out makes it covariant — Kotlin’s declaration-site version of Java’s ? extends.

5. A consumer

Declare an interface Sink that only ever consumes a T (an accept(value: T) method), marked so that a Sink of Animal is usable as a Sink of Dog.

Try it — edit, then press Run interface Sink<T> { fun accept(value: T) } open class Animal class Dog : Animal() fun main() { val sink: Sink<Animal> = object : Sink<Animal> { override fun accept(value: Animal) = println("accepted") } sink.accept(Dog()) // TODO: mark T so the next line would compile // val dogSink: Sink<Dog> = sink }
Show answer Hide answer
interface Sink<in T> {
    fun accept(value: T)
}

in makes it contravariant — Kotlin’s version of ? super.

6. Covariant by declaration

Declare an interface Producer whose only method returns a T, marked so a Producer of String is usable where a Producer of Any is expected.

Try it — edit, then press Run interface Producer<T> { fun produce(): T } fun main() { val strings = object : Producer<String> { override fun produce() = "hi" } // TODO: mark T so the next line would compile // val anything: Producer<Any> = strings println(strings.produce()) }
Show answer Hide answer
interface Producer<out T> {
    fun produce(): T
}

val p: Producer<Any> = object : Producer<String> {
    override fun produce() = "hi"
}

out (covariance) is allowed because T only ever comes out.

projections and reified

7. Don’t care about the argument

Write a function size that accepts a List of any unknown element type and returns its size.

Try it — edit, then press Run fun main() { // TODO: accept a List of any unknown element type (star projection) fun size(items: List<Any?>): Int = items.size println(size(listOf(1, 2, 3))) }
Show answer Hide answer
fun size(items: List<*>) = items.size

* is the star projection — Kotlin’s equivalent of Java’s bare ?.

8. Keep the type at runtime

Implement asOrNull so it safely casts its receiver to the reified type T, returning null on a mismatch.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun asOrNull() { val value: Any = "hello" Assert.assertEquals("returns the value when the type matches", "hello", value.asOrNull<String>()) Assert.assertEquals("null when the type does not match", null, value.asOrNull<Int>()) } } //sampleStart inline fun <reified T> Any.asOrNull(): T? = null // TODO: return this as T, or null on a type mismatch (use as?) //sampleEnd
Show answer Hide answer
inline fun <reified T> Any.asOrNull(): T? = this as? T

reified (only on inline functions) keeps T available at runtime, defeating erasure.

9. First element of a type

Implement firstOfType — an inline extension on Iterable<*> with a reified T — so it returns the first element that is a T, or null.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun firstOfType() { val mixed = listOf(1, "two", 3.0, "four") Assert.assertEquals("first element that is a String", "two", mixed.firstOfType<String>()) Assert.assertEquals("first element that is a Double", 3.0, mixed.firstOfType<Double>()) } } //sampleStart inline fun <reified T> Iterable<*>.firstOfType(): T? = null // TODO: return the first element that is a T, or null //sampleEnd
Show answer Hide answer
inline fun <reified T> Iterable<*>.firstOfType(): T? =
    firstOrNull { it is T } as T?

reified keeps T at runtime so it is T compiles — possible only because the function is inline.


Going deeper: bounds, reified, and variance

12. Constrain the type

Write a generic largest that returns the greater of two values, for any type that can compare to itself.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun largest() { Assert.assertEquals(7, largest(3, 7)) Assert.assertEquals("b", largest("a", "b")) } } //sampleStart fun <T> largest(a: T, b: T): T = a // TODO: return the greater — you'll need a bound so > works //sampleEnd
Show answer Hide answer
fun <T : Comparable<T>> largest(a: T, b: T): T = if (a > b) a else b

The bound T : Comparable<T> is what lets the body use >. Without it, T is just Any? and has no ordering.

13. A checked cast that survives erasure

Implement asOrNull so value.asOrNull<String>() returns the value as a String?, or null if it isn’t one — using a reified type parameter.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun asOrNull() { val v: Any = "hi" Assert.assertEquals("hi", v.asOrNull<String>()) Assert.assertNull(v.asOrNull<Int>()) } } //sampleStart inline fun <reified T> Any.asOrNull(): T? = null // TODO: this as? T //sampleEnd
Show answer Hide answer
inline fun <reified T> Any.asOrNull(): T? = this as? T

as? T needs T at runtime, which erasure normally strips — reified (on an inline function) keeps it by substituting the concrete type at each call site.

14. Why is a producer covariant?

Source<out T> lets you assign a Source<Dog> to a Source<Animal>, but MutableList<T> won’t let you assign a MutableList<Dog> to a MutableList<Animal>. What’s the difference? Reveal to check.

Show answer Hide answer

Source only produces T (hands it out), so treating dogs-out as animals-out is safe — that’s what out (covariance) declares. MutableList also consumes T (you can add to it), and if a MutableList<Dog> were a MutableList<Animal> you could add a Cat and corrupt it — so it stays invariant. The rule: out for what you take out, in for what you put in.


Back to the lesson, Kotlin Generics, or on to the next one: exceptions.

Comments