Class Types Workbook (Part 2)
Ten short exercises on Kotlin's specialized classes — data classes, enums, sealed hierarchies, objects and companions, inner classes, and value classes.
Practice problems for One Keyword Does the Work. Each takes a minute or two. Some exercises auto-check: implement the function (or the class its method uses) and press Run — hidden tests go green when you’re right and red (with a hint) when you’re not, all on JetBrains’ Kotlin compiler server. The pure declaration exercises stay attempt-then-reveal: fill in the TODO, press Run to compile it, then click Show answer. Nothing here is a trick question, just direct practice of the syntax from the lesson.
data classes
1. A data class
Declare a data class User(val id: Int, val name: String).
// TODO: make this a data class
class User(val id: Int, val name: String)
fun main() {
val u = User(1, "Ada")
println(u) // a data class gives a readable toString()
} Show answer Hide answer
data class User(val id: Int, val name: String) 2. Copy with a change
Implement renamed so it returns a copy of the user with the same id but the name "Ada L.".
import org.junit.Test
import org.junit.Assert
data class User(val id: Int, val name: String)
class Test {
@Test fun copiesWithName() {
Assert.assertEquals("keep the id, change the name",
User(1, "Ada L."), renamed(User(1, "Ada")))
}
}
//sampleStart
fun renamed(u: User): User =
u // TODO: return a copy with the name "Ada L."
//sampleEnd Show answer Hide answer
fun renamed(u: User) = u.copy(name = "Ada L.") 3. Value equality for free
Declare data class User(val id: Int, val name: String), then write the expression comparing two separate User(1, "Ada") instances for value equality.
data class User(val id: Int, val name: String)
fun main() {
// TODO: compare two separate User(1, "Ada") instances for value equality
val equal = false
println(equal)
} Show answer Hide answer
data class User(val id: Int, val name: String)
User(1, "Ada") == User(1, "Ada") // true — generated equals compares fields enum, sealed, object
4. A small enum
Declare enum class Direction { NORTH, SOUTH, EAST, WEST }.
// TODO: give Direction all four constants: NORTH, SOUTH, EAST, WEST
enum class Direction { NORTH }
fun main() {
println(Direction.NORTH) // then reference SOUTH, EAST, WEST too
} Show answer Hide answer
enum class Direction { NORTH, SOUTH, EAST, WEST } 5. A sealed hierarchy
Declare a sealed Result with a Success(val data: String) and an Error(val message: String) subtype.
// TODO: make Result sealed, with Success(val data: String) and Error(val message: String) subtypes
class Result
fun main() {
val r = Result() // replace with Result.Success("ok") once the subtypes exist
println(r)
} Show answer Hide answer
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
} 6. Exhaustive when on a sealed type
Implement describe with a when over a Result that returns the data on success and the message on error — with no else.
import org.junit.Test
import org.junit.Assert
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
}
class Test {
@Test fun describes() {
Assert.assertEquals("return the data on success",
"ok", describe(Result.Success("ok")))
Assert.assertEquals("return the message on error",
"boom", describe(Result.Error("boom")))
}
}
//sampleStart
fun describe(r: Result): String {
// TODO: return r.data on Success, r.message on Error — a when with no else
return "TODO"
}
//sampleEnd Show answer Hide answer
fun describe(r: Result) = when (r) {
is Result.Success -> r.data
is Result.Error -> r.message
}The compiler knows the hierarchy is closed, so the when is exhaustive without an else.
7. A singleton
Declare a singleton object Registry with a mutable list entries and an add(s: String) method.
// TODO: make Registry a singleton 'object' and add an add(s: String) method
class Registry {
val entries = mutableListOf<String>()
}
fun main() {
val r = Registry() // an 'object' wouldn't need this — use Registry directly
println(r.entries)
} Show answer Hide answer
object Registry {
val entries = mutableListOf<String>()
fun add(s: String) { entries.add(s) }
} 8. A companion factory
Give class Config private constructor(val raw: String) a companion object with a fromText(text: String): Config factory.
// TODO: add a companion object with a fromText(text: String): Config factory
class Config private constructor(val raw: String) {
companion object {
}
}
fun main() {
// once the factory exists: val c = Config.fromText("hi"); println(c.raw)
println("build a Config via Config.fromText(...)")
} Show answer Hide answer
class Config private constructor(val raw: String) {
companion object {
fun fromText(text: String) = Config(text)
}
}
Config.fromText("...") inner and value classes
9. An inner class
Outer holds a val name. Add an inner class Printer whose show() prints Outer’s name.
// TODO: add an inner class Printer whose show() prints the outer 'name'
class Outer(val name: String)
fun main() {
val o = Outer("Ada")
println(o.name) // then: o.Printer().show()
} Show answer Hide answer
class Outer(val name: String) {
inner class Printer {
fun show() = println(name)
}
}inner lets the nested class reach the outer instance’s members; a plain nested class cannot.
10. A value class
Declare a value class UserId(val raw: Int) so an id can’t be mixed up with a plain Int.
// TODO: make UserId a value class (annotate with @JvmInline) wrapping a single Int
class UserId(val raw: Int)
fun main() {
val id = UserId(7)
println(id.raw)
} Show answer Hide answer
@JvmInline
value class UserId(val raw: Int)A value class wraps a single value for type safety with no runtime allocation.
Going deeper: copy, finality, and value classes
12. Copy with a change
Use copy to return a new User with a different name, leaving the original untouched.
import org.junit.Test
import org.junit.Assert
data class User(val id: Int, val name: String)
class Test {
@Test fun renamed() {
Assert.assertEquals(User(1, "Grace"), renamed(User(1, "Ada"), "Grace"))
}
}
//sampleStart
fun renamed(u: User, newName: String): User =
u // TODO: return a copy with name = newName
//sampleEnd Show answer Hide answer
fun renamed(u: User, newName: String) = u.copy(name = newName)copy duplicates the object with the named fields changed. Remember it’s shallow — referenced objects are shared, not cloned.
13. Why won’t it extend?
You try open data class User(...) so you can subclass it, and the compiler refuses. Why? Reveal to check.
Show answer Hide answer
A data class is final and cannot be made open — the error is “this type is final, so it cannot be extended.” An inheritable data class would make equals symmetry impossible to guarantee, so Kotlin forbids it. Data classes model values, and values don’t have subclasses.
14. When does a value class stop being free?
A @JvmInline value class Email(val raw: String) is normally inlined to a bare String at runtime. Name two situations where the compiler has to box it into a real object instead. Reveal to check.
Show answer Hide answer
When it’s used as a nullable (Email?), as a generic type argument (List<Email>), or where a supertype is expected. In those positions erasure forces a real wrapper — the same pressure that boxes Int into Integer. A value class is free in the direct path, not everywhere.
Back to the lesson, One Keyword Does the Work, or on to the next one: enums up close.
Comments