Data classes and objects
Kotlin’s distinctive data-modelling features: data classes (auto-generated equals, hashCode, toString, copy, componentN), object declarations (singletons), companion objects (class-level members), enum classes (with associated data and methods), and the substantial standard collections (List, MutableList, Set, MutableSet, Map, MutableMap). The collections distinguish read-only (covariant) and mutable (invariant) interfaces — substantial type safety. The combination — data classes for value records, object/companion for singletons, the rich collection library, the substantial Map / List / Set interface, the read-only/mutable distinction — is the substance of Kotlin’s data-modelling surface.
Data classes
The data class admits auto-generated record-like behaviour:
data class Person(val name: String, val age: Int)
val alice = Person("Alice", 30)
val bob = Person("Bob", 25)
// Auto-generated:
println(alice) // "Person(name=Alice, age=30)"
println(alice == Person("Alice", 30)) // true (value equality)
println(alice.hashCode()) // hash code
val copy = alice.copy(age = 31) // copy with modification
val (name, age) = alice // destructuring
The compiler synthesises:
equals(other: Any?): Boolean— value equality based on properties.hashCode(): Int— hash based on properties.toString(): String— human-readable representation.copy(...)— admits property updates.componentN()methods (component1(), etc.) — admit destructuring.
The principal restrictions:
- Primary constructor must have at least one parameter.
- Primary constructor parameters must be
valorvar. - Cannot be
abstract,open,sealed, orinner.
The conventional Kotlin discipline favours data class for value-typed records.
copy() for immutable updates
data class User(val name: String, val age: Int, val email: String)
val u = User("Alice", 30, "alice@b.c")
val updated = u.copy(age = 31) // changes only age
val renamed = u.copy(name = "Alice Smith")
The form admits substantial immutable update patterns.
Destructuring
val (name, age) = alice
// In for loops:
for ((name, age) in users) {
println("$name: $age")
}
// In lambdas:
users.forEach { (name, age) -> println("$name: $age") }
// Selective destructuring:
val (_, age) = alice // ignore name
Destructuring is positional — the order of properties in the primary constructor matters.
object declarations
Singletons:
object DatabaseConnection {
private val connectionString = "jdbc:postgres:..."
private var connection: Connection? = null
fun connect(): Connection {
return connection ?: createConnection().also { connection = it }
}
fun close() {
connection?.close()
connection = null
}
private fun createConnection(): Connection { /* ... */ }
}
DatabaseConnection.connect()
DatabaseConnection.close()
The object admits a single instance — initialised on first access; thread-safe by default.
For namespacing constants:
object Config {
const val MAX_RETRIES = 3
const val DEFAULT_TIMEOUT = 30
const val APP_NAME = "MyApp"
object URLs {
const val API = "https://api.example.com"
const val DOCS = "https://docs.example.com"
}
}
Config.MAX_RETRIES
Config.URLs.API
Object declarations may extend classes and implement interfaces:
object Logger : Closeable {
override fun close() { /* ... */ }
fun log(message: String) { /* ... */ }
}
Companion objects
Inside a class, companion object admits class-level members:
class User private constructor(val id: Int, val name: String) {
companion object {
const val MAX_NAME_LENGTH = 100
fun create(name: String): User {
require(name.length <= MAX_NAME_LENGTH)
return User(generateId(), name)
}
private fun generateId(): Int = (Math.random() * 1_000_000).toInt()
}
}
User.MAX_NAME_LENGTH
User.create("Alice")
Treated in Classes and OOP.
Anonymous objects
For one-off implementations:
val handler = object : Runnable {
override fun run() {
println("running")
}
}
// With multiple interfaces:
val listener = object : ClickListener, KeyListener {
override fun onClick(event: Event) { /* ... */ }
override fun onKey(event: Event) { /* ... */ }
}
button.setOnClickListener(handler)
For SAM (single abstract method) interfaces, lambdas are conventionally clearer:
button.setOnClickListener { event -> handle(event) }
Enum classes
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
val d: Direction = Direction.NORTH
println(d.name) // "NORTH"
println(d.ordinal) // 0
// All values:
Direction.values() // Array<Direction>
Direction.entries // List<Direction> (since 1.9)
// From string:
Direction.valueOf("NORTH") // Direction.NORTH
Enum with associated data
enum class Planet(val mass: Double, val radius: Double) {
MERCURY(3.303e+23, 2.4397e6),
VENUS(4.869e+24, 6.0518e6),
EARTH(5.976e+24, 6.37814e6),
MARS(6.421e+23, 3.3972e6);
val surfaceGravity: Double
get() = G * mass / (radius * radius)
companion object {
const val G = 6.67300E-11
}
}
println(Planet.EARTH.surfaceGravity) // 9.80...
The enum admits properties, methods, and companion objects.
Enum with abstract method
enum class Operation {
PLUS {
override fun apply(a: Int, b: Int) = a + b
},
MINUS {
override fun apply(a: Int, b: Int) = a - b
},
TIMES {
override fun apply(a: Int, b: Int) = a * b
};
abstract fun apply(a: Int, b: Int): Int
}
Operation.PLUS.apply(3, 5) // 8
The pattern admits substantial polymorphism per case.
Collections
Kotlin distinguishes read-only and mutable collections:
| Read-only | Mutable |
|---|---|
List<T> | MutableList<T> |
Set<T> | MutableSet<T> |
Map<K, V> | MutableMap<K, V> |
The read-only interfaces admit covariance (List<out T>); the mutable interfaces are invariant.
List
val list: List<Int> = listOf(1, 2, 3) // read-only
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
mutable.add(4)
mutable.remove(1)
mutable[0] = 99
// list.add(4) // ERROR
// Read operations:
list[0]
list.first()
list.last()
list.size
list.isEmpty()
list.contains(2)
list.indexOf(2)
Set
val set: Set<String> = setOf("apple", "banana", "apple") // {apple, banana}
val mutable: MutableSet<String> = mutableSetOf("a", "b")
mutable.add("c")
mutable.remove("a")
// Set operations:
val a = setOf(1, 2, 3)
val b = setOf(2, 3, 4)
a union b // {1, 2, 3, 4}
a intersect b // {2, 3}
a subtract b // {1}
Map
val map: Map<String, Int> = mapOf("a" to 1, "b" to 2)
val mutable: MutableMap<String, Int> = mutableMapOf("a" to 1)
mutable["b"] = 2
mutable.remove("a")
// Read:
map["a"] // Int? (returns null if missing)
map.getOrDefault("c", 0)
map.getValue("a") // throws if missing
map.keys
map.values
map.entries
// Iteration:
for ((key, value) in map) {
println("$key: $value")
}
Other collection types
val arrayList = ArrayList<Int>(initialCapacity = 100) // explicit ArrayList
val linkedList = LinkedList<Int>() // java.util.LinkedList
val arrayDeque = ArrayDeque<Int>() // double-ended queue
val hashMap = HashMap<String, Int>()
val linkedHashMap = LinkedHashMap<String, Int>() // preserves insertion order
val treeMap = TreeMap<String, Int>() // sorted by key
val intArray = IntArray(10) { 0 } // primitive int array
val intArray = intArrayOf(1, 2, 3)
Tuples
Kotlin admits Pair and Triple for ad-hoc tuples:
val pair: Pair<String, Int> = Pair("Alice", 30)
val pair = "Alice" to 30 // infix `to`
val (name, age) = pair // destructuring
val triple = Triple("Alice", 30, "alice@b.c")
val (name, age, email) = triple
For substantial multi-field returns, data classes are conventionally clearer.
Common patterns
Data class for a record
data class User(
val id: Int,
val name: String,
val email: String,
val role: Role = Role.USER
)
val u = User(1, "Alice", "alice@b.c")
val admin = u.copy(role = Role.ADMIN)
Data class for state
data class GameState(
val level: Int = 1,
val score: Int = 0,
val livesRemaining: Int = 3,
val isPaused: Boolean = false
) {
val isOver: Boolean get() = livesRemaining == 0
}
Builder pattern via copy
data class HttpRequest(
val url: String,
val method: String = "GET",
val headers: Map<String, String> = emptyMap(),
val body: String? = null
)
val base = HttpRequest("https://example.com")
val authed = base.copy(headers = mapOf("Authorization" to "Bearer $token"))
val post = authed.copy(method = "POST", body = jsonString)
Object as namespace
object Config {
const val APP_NAME = "MyApp"
const val VERSION = "1.0.0"
object Server {
const val HOST = "localhost"
const val PORT = 8080
}
object Limits {
const val MAX_USERS = 1000
const val MAX_REQUESTS_PER_HOUR = 60
}
}
Singleton service
object AnalyticsService {
private val events = mutableListOf<Event>()
fun track(event: Event) {
events.add(event)
send(event)
}
private fun send(event: Event) { /* ... */ }
}
AnalyticsService.track(Event.PageView("/home"))
Enum with helper methods
enum class Status {
ACTIVE, INACTIVE, BANNED;
val isAllowed: Boolean
get() = this == ACTIVE
companion object {
fun fromString(s: String): Status? = entries.firstOrNull { it.name == s }
}
}
Status.ACTIVE.isAllowed // true
Status.fromString("ACTIVE") // Status.ACTIVE
Map with default
val counts = mutableMapOf<String, Int>()
for (word in words) {
counts[word] = (counts[word] ?: 0) + 1
}
// Or:
val counts = words.fold(mutableMapOf<String, Int>()) { acc, word ->
acc[word] = (acc[word] ?: 0) + 1
acc
}
// Or:
val counts = words.groupingBy { it }.eachCount()
Group-by
val grouped = users.groupBy { it.department }
val byAge = users.groupBy {
when {
it.age < 18 -> "minor"
it.age < 65 -> "adult"
else -> "senior"
}
}
Set operations
val required = setOf("name", "email", "password")
val provided = setOf("name", "email")
val missing = required - provided // {"password"}
// Or:
val missing = required.subtract(provided)
List manipulation
val list = listOf(1, 2, 3, 4, 5)
list.first() // 1
list.last() // 5
list.first { it > 2 } // 3
list.firstOrNull { it > 100 } // null
list.take(3) // [1, 2, 3]
list.takeLast(2) // [4, 5]
list.drop(2) // [3, 4, 5]
list.dropLast(2) // [1, 2, 3]
list.reversed()
list.sorted()
list.distinct()
list + 6 // [1, 2, 3, 4, 5, 6] (new list)
list - 3 // [1, 2, 4, 5]
list + listOf(6, 7) // concatenation
MutableList
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
mutable.add(0, 0) // insert at index
mutable.remove(2)
mutable.removeAt(0)
mutable.removeAll { it > 3 }
mutable.clear()
Map iteration
for ((key, value) in map) {
println("$key: $value")
}
map.forEach { (key, value) -> println("$key: $value") }
// Sorted by key:
for ((key, value) in map.toSortedMap()) {
println("$key: $value")
}
// Filter:
val positive = map.filter { (_, v) -> v > 0 }
Array vs List
// Array<T> — boxed Java Object array (for object types):
val arr: Array<Int> = arrayOf(1, 2, 3) // Integer[] under the hood
val arr: Array<String> = arrayOf("a", "b")
// Primitive arrays — unboxed:
val intArr: IntArray = intArrayOf(1, 2, 3) // int[]
val doubleArr: DoubleArray = doubleArrayOf(1.0, 2.0)
val byteArr: ByteArray = byteArrayOf(0x01, 0x02)
// List<T> — interface with multiple implementations:
val list: List<Int> = listOf(1, 2, 3) // ArrayList by default
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
The conventional discipline:
- Use
List/MutableListfor routine sequences. - Use
IntArray/DoubleArrayetc. for substantial primitive arrays (no boxing). - Use
Array<T>rarely — for Java interop or generic array needs.
Nested data class
data class Address(
val street: String,
val city: String,
val country: String
)
data class Person(
val name: String,
val age: Int,
val address: Address
)
val p = Person("Alice", 30, Address("Main St", "Helsinki", "FI"))
val moved = p.copy(address = p.address.copy(city = "Stockholm"))
Sealed type hierarchy
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: String) : Result<Nothing>()
object Pending : Result<Nothing>()
}
Treated in Sealed types.
Range
val range: IntRange = 1..10 // inclusive
val range: IntRange = 1 until 10 // exclusive
val range: CharRange = 'a'..'z'
range.contains(5)
range.first
range.last
for (i in 1..10) print(i)
A note on componentN() for destructuring
The componentN() methods admit destructuring; auto-generated for data class:
data class Point(val x: Double, val y: Double)
// Auto-generated:
// operator fun component1(): Double = x
// operator fun component2(): Double = y
val p = Point(1.0, 2.0)
val (x, y) = p // calls component1, component2
For non-data classes, manual operator fun component1() admits destructuring.
A note on the conventional discipline
The contemporary Kotlin data-structures advice:
- Use
data classfor value-typed records. - Use
copy()for immutable updates. - Use
objectfor singletons and namespaces. - Use
companion objectfor class-level members. - Use
enum classwith properties and methods. - Use
List/MutableListby default for sequences. - Use
Map/MutableMapwith substantial Map operations. - Use
Setfor unique-value collections. - Use
IntArray/DoubleArrayfor primitive arrays. - Use destructuring (
val (a, b) = pair). - Use
to(infix) forPaircreation in maps. - Trust read-only collection types — admit substantial type safety.
The combination — data classes for records, object/companion for singletons, enum classes with substantial features, the read-only/mutable collection distinction, the substantial standard collection library, the componentN-based destructuring — is the substance of Kotlin’s data-structure surface. The discipline produces concise, type-safe, expressive data modelling with substantial flexibility.