Data Class und Copy in Kotlin
1. Übersicht
data class ist eine der Funktionen, die es Kotlin ermöglicht, Code bei der Arbeit mit Klassen, die nur Daten enthalten (DTO, Model, Value Object), viel kürzer als in Java zu schreiben. Dieser Artikel hilft dir zu verstehen, was Kotlin für eine data class automatisch erzeugt, wie du copy() verwendest, um modifizierte Kopien mit geänderten Feldern zu erstellen, und welche häufigen Fallstricke es gibt – falsche Vergleiche, die Verwendung von var in einer data class oder die Vererbung von data classes.
2. Was ist eine Data Class?
Wenn du eine Klasse mit dem Schlüsselwort data deklarierst, erzeugt der Kotlin-Compiler automatisch mehrere Funktionen basierend auf den Properties, die im Primärkonstruktor (primary constructor) deklariert sind.
data class Customer(
val id: Long,
val name: String,
val email: String
)
Mit nur diesen 4 Zeilen erzeugt Kotlin automatisch:
equals()/hashCode()basierend aufid,name,email.toString()in der FormCustomer(id=1, name=..., email=...).copy()um eine Kopie zu erstellen, in der einige Felder geändert werden können.componentN()um Destructuring-Deklarationen zu unterstützen.
Im Vergleich zu Java müsstest du eine Klasse mit Konstruktor, Gettern, equals, hashCode, toString schreiben — etwa 40-50 Zeilen für die gleiche Funktionalität.
2.1. toString() als Standard
fun main() {
val customer = Customer(1, "An Nguyen", "an@nonladev.com")
println(customer)
}
Customer(id=1, name=An Nguyen, email=an@nonladev.com)
Das automatisch erzeugte toString() ist sehr praktisch beim Logging oder Debugging – du brauchst es nicht selbst zu schreiben.
3. equals() und hashCode() Nach Wert
Der größte Unterschied zwischen data class und regulären Klassen ist, dass equals() nach Wert (structural equality) vergleicht und nicht nach Referenz.
fun main() {
val c1 = Customer(1, "An Nguyen", "an@nonladev.com")
val c2 = Customer(1, "An Nguyen", "an@nonladev.com")
println(c1 == c2) // Wertvergleich
println(c1 === c2) // Referenzvergleich
}
true
false
==ruftequals()auf. Zwei Objekte mit denselben Feldern sind gleich, auch wenn sie verschiedene Referenzen sind.===prüft, ob zwei Variablen auf das gleiche Objekt im Speicher zeigen.
Hinweis: Bei regulären Klassen (keine
data class) vergleicht==standardmäßig wie===nach Referenz, es sei denn, du overridestequals()selbst.
Da hashCode() auch basierend auf den Properties erzeugt wird, kannst du data class sicher als Schlüssel in einer HashMap oder als Element in einem HashSet verwenden.
fun main() {
val set = hashSetOf(
Customer(1, "An Nguyen", "an@nonladev.com"),
Customer(1, "An Nguyen", "an@nonladev.com")
)
println(set.size)
}
1
Zwei Objekte mit identischen Werten werden als gleich angesehen. Das HashSet behält nur 1 Element.
4. Die copy() Funktion — Unveränderliche Kopien Erstellen
Wenn du val für Properties verwendest, ist Customer unveränderlich (immutable) – die Felder können nach der Erstellung nicht direkt geändert werden. Um ein paar Werte zu ändern, erzeugt Kotlin eine copy()-Funktion, mit der du ein neues Objekt erstellen kannst, wobei die nicht übergebenen Felder unverändert bleiben.
fun main() {
val original = Customer(1, "An Nguyen", "an@nonladev.com")
val updated = original.copy(email = "an.new@nonladev.com")
println(original)
println(updated)
}
Customer(id=1, name=An Nguyen, email=an@nonladev.com)
Customer(id=1, name=An Nguyen, email=an.new@nonladev.com)
originalwird überhaupt nicht geändert – das ist der Kern der Unveränderlichkeit.updatedist ein neues Objekt, das sich nur in deremailunterscheidet. Die anderen Felder werden kopiert.
Dieses Muster wird häufig verwendet, wenn man mit State in der UI arbeitet (Compose, MVI) oder wenn man Entitäten in der Business-Logik aktualisiert, ohne die ursprüngliche direkt zu verändern.
4.1. copy() Mit Named Arguments
Du kannst mehrere Parameter gleichzeitig übergeben und dabei ihre Namen verwenden (named arguments), um Verwechslungen mit der Reihenfolge zu vermeiden:
val promoted = original.copy(name = "An Nguyen (VIP)", email = "vip@nonladev.com")
Wenn du nicht mit named arguments arbeitest, wird der Code leicht verwirrend, wenn die Klasse viele Felder vom gleichen Typ String hat.
5. Flache Kopie (Shallow Copy) — Ein Fallstrick Bei Änderbaren Objekten Innen
copy() erstellt nur eine flache Kopie (shallow copy): Es kopiert die Referenzen der Felder, nicht tiefe Kopien (deep copy) komplexer Objekte innen.
data class Order(
val id: Long,
val items: MutableList<String>
)
fun main() {
val order1 = Order(1, mutableListOf("Book"))
val order2 = order1.copy()
order2.items.add("Pen")
println(order1.items) // wird beeinflusst!
println(order2.items)
}
[Book, Pen]
[Book, Pen]
order2 = order1.copy()erstellt nur ein neuesOrder-Objekt, aber das Felditemszeigt auf die gleicheMutableList.- Das Ändern von
order2.itemsändert auchorder1.items– das ist ein sehr häufiger Fehler, wenn man veränderbare Collections in data classes verwendet.
Abhilfe: Verwende unveränderliche List (toList()) oder kopiere die Collection selbst im Konstruktor.
data class Order(
val id: Long,
val items: List<String>
)
fun main() {
val order1 = Order(1, listOf("Book"))
val order2 = order1.copy(items = order1.items + "Pen")
println(order1.items)
println(order2.items)
}
[Book]
[Book, Pen]
Verwende List<String> unveränderlich und den +-Operator um eine neue Liste zu erstellen, statt die zu verändern. Das entspricht dem Konzept der Unveränderlichkeit in data class.
Hinweis: Wenn du echte Deep Copy brauchst (verschachtelte data class, die andere data class enthält), musst du die Kopierlogik selbst rekursiv schreiben. Kotlin erzeugt keine Deep Copy automatisch.
6. Destructuring Deklaration
Dank des automatisch erzeugten componentN() kannst du eine data class in mehrere einzelne Variablen „destructurieren”:
fun main() {
val customer = Customer(1, "An Nguyen", "an@nonladev.com")
val (id, name, email) = customer
println("$id - $name - $email")
}
1 - An Nguyen - an@nonladev.com
Die Reihenfolge der Variablen entspricht der Reihenfolge der Properties im Primärkonstruktor, nicht dem Namen. Das Ändern der Reihenfolge der Property-Deklaration führt zu falschem Destructuring – sei vorsichtig beim Refaktorieren.
Hinweis: Du solltest nur bei data classes mit wenigen Feldern (2-3) destructurieren. Bei Klassen mit vielen Feldern ist es besser, die Property-Namen direkt zu verwenden, um den Code lesbarer zu machen.
7. Einschränkungen Von Data Class
7.1. Keine Vererbung
Eine data class kann nicht von einer anderen data class erben:
// Compiler-Fehler wenn du das versuchst:
// data class VipCustomer(val tier: String) : Customer(...)
Kotlin erlaubt nur, dass data class ein Interface implementiert, oder von einer regulären Klasse (non-data) erbt, die keine Properties im Primärkonstruktor hat. Der Grund: Das automatisch erzeugte equals()/copy() würde sehr schwierig zu definieren sein, wenn mehrschichtige Feldvererbung vorhanden ist.
Häufige Lösung: Komposition statt Vererbung.
data class VipCustomer(
val customer: Customer,
val tier: String
)
7.2. Primärkonstruktor Muss Mindestens 1 Property Haben
// Fehler: data class muss mindestens 1 Parameter im Primärkonstruktor haben
// data class Empty
7.3. Nur Properties Im Primärkonstruktor Werden Berücksichtigt
data class Product(val name: String) {
var discount: Int = 0
}
fun main() {
val p1 = Product("Laptop").apply { discount = 10 }
val p2 = Product("Laptop").apply { discount = 20 }
println(p1 == p2) // true, discount wird nicht in equals() berücksichtigt
}
true
discount ist im Body deklariert, nicht im Primärkonstruktor, also wird es nicht in equals(), hashCode(), toString(), copy() berücksichtigt. Das ist leicht zu verwechseln – wenn ein Feld für den Vergleich wichtig ist, muss es in den Primärkonstruktor.
8. Tests Mit JUnit 5 Und AssertJ
Erforderliche Abhängigkeiten (Gradle Kotlin DSL):
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testImplementation("org.assertj:assertj-core:3.26.0")
Tests schreiben um das Verhalten von copy() und equals() zu überprüfen:
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class CustomerTest {
@Test
fun `copy should keep unchanged fields and update specified ones`() {
val original = Customer(1, "An Nguyen", "an@nonladev.com")
val updated = original.copy(email = "an.new@nonladev.com")
assertThat(updated.id).isEqualTo(original.id)
assertThat(updated.name).isEqualTo(original.name)
assertThat(updated.email).isEqualTo("an.new@nonladev.com")
assertThat(original.email).isEqualTo("an@nonladev.com")
}
@Test
fun `two customers with same fields should be equal`() {
val c1 = Customer(1, "An Nguyen", "an@nonladev.com")
val c2 = Customer(1, "An Nguyen", "an@nonladev.com")
assertThat(c1).isEqualTo(c2)
assertThat(c1).isNotSameAs(c2)
}
}
isEqualToprüft die Gleichheit des Wertes, was dem Wesen vonequals()indata classentspricht.isNotSameAsbestätigt, dassc1undc2zwei verschiedene Objekte im Speicher sind, obwohl sie gleich sind.- Der erste Test bestätigt, dass
copy()richtig funktioniert: nicht angegebene Felder bleiben gleich, angegebene Felder werden aktualisiert, das Originalobject ändert sich nicht.
9. Wann Sollte Man Keine Data Class Verwenden
- JPA/Hibernate Entity:
equals()/hashCode()werden für alle Felder verwendet (einschließlich generierter Felder wieidvor dem Persistieren). Das kann zu Fehlern führen, wenn Entities inHashSetverwendet oder mit persistierten/nicht persistierten Entities verglichen werden. Besser:equals()/hashCode()manuell nachidschreiben. - Klasse mit komplexer Business-Logik:
data classist für Datencontainer geeignet, nicht für Klassen mit viel Verhalten und mehrstufiger Vererbung. - Klasse, die Speicher mit vielen verschachtelten änderbaren Feldern optimieren muss: Überlege, ob ein Builder-Pattern oder eine reguläre Klasse besser passt, wenn Shallow Copy schwer zu kontrollieren ist.
10. Fazit
data class hilft dir, Boilerplate-Code zu vermeiden, wenn du mit DTOs/Models in Kotlin arbeitest: Sie erzeugt automatisch equals(), hashCode(), toString(), copy() und unterstützt Destructuring. Die copy()-Funktion ist ein mächtiges Werkzeug um Unveränderlichkeit zu bewahren, aber du musst bedenken, dass es nur eine flache Kopie erstellt – sei vorsichtig mit Feldern, die Collections oder veränderliche Objekte innen enthalten. Vermeide data class für JPA-Entitäten oder Klassen mit komplexer Business-Logik und stelle immer sicher, dass wichtige Felder im Primärkonstruktor sind, damit equals()/copy() wie erwartet funktioniert.