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.
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.
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?
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 StringNo — 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.
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.
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.
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.
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.
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? Treified (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.
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.
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 bThe 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.
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? Tas? 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