Blog

7 häufige Fehler in Kotlin

Jeroen Rosenberg

Aktualisiert Oktober 15, 2025
12 Minuten

Dieser Artikel wurde ursprünglich in meinem persönlichen Blog am 9. März 2023 veröffentlicht.

Haben Sie sich beim Programmieren jemals wie dieses Kind gefühlt? Ja, das habe ich! Als ich mit der Kotlin-Entwicklung begann, fühlte ich mich viele Male wie dieses Kind. Und selbst jetzt, wo ich über einige Erfahrung verfüge, stoße ich hin und wieder bei Kotlin-Schulungen oder Kotlin-Aufgaben auf Situationen, in denen ich einfach nicht erklären kann, warum der Code nicht so funktioniert, wie ich es beabsichtigt hatte. Oder vielleicht noch schlimmer: Ich kann nicht erklären, warum der Code funktioniert. In diesem Beitrag zeige ich Ihnen 7 Codeschnipsel mit häufigen Fehlern (die ich gemacht habe) bei der Programmierung in Kotlin.

Hallo Welt!

Beginnen wir mit einem recht einfachen "Hallo Welt"-Beispiel. Betrachten Sie den unten stehenden Code. Welches Ergebnis erwarten Sie von diesem Programm?

Spaß main(args: Array<String>) {
 fun hallo() = print("Hallo")

    Spaß Welt() = {
 print("Welt")
 }

  hallo()
 welt()
}

Es mag Sie überraschen, aber die Ausgabe dieses Programms lautet einfach Hello.

Dieser Punkt ist besonders verwirrend und ein häufiger Fehler für Scala-Entwickler, die zu Kotlin wechseln. Ich arbeite seit Jahren mit Scala und mit Scala wären beide Funktionen gleichwertig. In Kotlin hingegen bedeutet die Verwendung der Kombination von = und {}, dass Ihre Funktion ein Lambda zurückgibt und es NICHT ausführt.

Um dieses Beispielprogramm Hello World ausgeben zu lassen, müssten Sie entweder = oder {} in der Funktion world entfernen, z.B. so:

Spaß main(args: Array<String>) {
 fun hallo() = print("Hallo")

    Spaß Welt() {
 print("Welt")
 }

  hallo()
 welt()
}

Viel Glück, Scala-Entwickler!

Fallback

Im nächsten Beitrag geht es um eine raffinierte kleine (Erweiterungs-)Funktion, withDefault, die auf Kotlin Map Instanzen verfügbar ist. Mit dieser Funktion können Sie einen Standardwert definieren, der zurückgegeben wird, wenn ein fehlender Schlüssel abgerufen wird. Im folgenden Beispiel erstelle ich eine leere Map und versuche sicherzustellen, dass sie bei fehlenden Schlüsseln die Phrase "default" zurückgibt. Wird dies gelingen?

Spaß main(args: Array<String>) {
 val map = mapOf<String, Int>().withDefault{ "default" }

 println(map["1"])
}

Leider lautet die Antwort nein. Dieses Programm wird null ausgeben, was wahrscheinlich nicht das ist, was Sie hier erwarten würden. Der Standardwert wird NUR berücksichtigt, wenn Sie die Funktion getValue für den Zugriff auf die Karte verwenden. Wenn Sie den Indizierungsoperator, die eckigen Klammern, verwenden, wird unter der Haube die Funktion get verwendet und diese gibt immer null zurück, wenn der Schlüssel fehlt.

Lassen Sie uns dies mit getValue umschreiben.

Spaß main(args: Array<String>) {
 val map = mapOf<String, Int>().withDefault{ "default" }

 println(map.getValue("1"))
}

Das obige Programm gibt in der Tat default aus. Die Lösung besteht also darin, immer Werte aus einem Map mit getValue abzurufen, wenn Sie withDefault verwenden? Nicht ganz. Betrachten Sie den folgenden Ausschnitt:

Spaß main(args: Array<String>) {
 val map = mapOf<String, Int>().withDefault{ "default" }

 println(map.filterKeys { it != "2" }.getValue("1"))
}

Dieser Code wirft tatsächlich ein java.util.NoSuchElementException!

Exception im Thread "main" java.util.NoSuchElementException: Schlüssel 1 fehlt in der Map.
 at kotlin.collections.MapsKt__MapWithDefaultKt.getOrImplicitDefaultNullable(MapWithDefault.kt:24)
 at kotlin.collections.MapsKt__MapsKt.getValue(Maps.kt:348)
 at DefaultKt.main(Default.kt:4)  

Warum ist das so? Die Anwendung von Transformationsoperationen auf schreibgeschützte Kotlin-Sammlungen (mehr dazu im nächsten Abschnitt) liefert tatsächlich eine neue Instanz des jeweiligen Sammlungstyps. Wenn wir filterKeys auf unsere Instanz Map anwenden, erhalten wir eine neue Instanz von Map. Eine Instanz ohne den definierten Standardwert. Daher hat getValue immer noch keinen Standardwert, auf den es zurückgreifen kann, und löst eine Ausnahme aus.

Zusammenfassend möchte ich Ihnen raten, withDefault nicht zu verwenden. Sie ist nicht zuverlässig. Geben Sie stattdessen den Fallback-Wert immer explizit an, wenn Sie von einem Map abrufen, indem Sie die Funktion getOrDefault verwenden. Zum Beispiel:

Spaß main(args: Array<String>) {
 val map = mapOf<String, Int>()
 println(map.filterKeys { it != "2" }.getOrDefault("1", "default"))
}

Dieses Programm wird tatsächlich default ausgeben, wie wir es beabsichtigt haben (weil der Schlüssel "2" fehlt). Verwenden Sie nicht withDefault, sondern getOrDefault, wenn Sie sich auf einen Ausweichwert verlassen müssen.

Unveränderlichkeit

Wie bereits oben erwähnt, verfügt Kotlin über schreibgeschützte Sammlungen. Wenn Sie eine der <collection type>Of() Funktionen (z.B. listOf() oder mapOf()) verwenden, erhalten Sie eine schreibgeschützte Instanz des betreffenden Sammlungstyps. Wenn Sie eine der mutable<collection type>Of() Funktionen verwenden, wird eine veränderbare Sammlung dieses Typs erstellt.

Es ist ein häufiger Fehler in Kotlin, schreibgeschützte Sammlungen mit unveränderlichen Sammlungen zu verwechseln. Schreibgeschützte Sammlungen sind unveränderlichen Sammlungen insofern ähnlich, als beide Sammlungen nach ihrer Erstellung nicht mehr verändert werden können. Es gibt jedoch einige Unterschiede zwischen den beiden Konzepten. Bei schreibgeschützten Sammlungen handelt es sich um Sammlungen, die zwar von der zugrundeliegenden Datenquelle geändert werden können, aber nicht direkt über den Verweis auf die Sammlung. Wenn z.B. eine schreibgeschützte Liste durch eine ArrayList unterstützt wird, kann der Inhalt der ArrayList noch geändert werden, aber der Verweis auf die schreibgeschützte Liste kann nicht zum Hinzufügen, Entfernen oder Ändern von Elementen verwendet werden.

Unveränderliche Sammlungen (z.B. in Scala) sind wirklich unveränderlich und können in keiner Weise verändert werden. Sobald eine unveränderliche Sammlung erstellt wurde, kann ihr Inhalt nicht mehr geändert werden, und jeder Versuch, dies zu tun, führt dazu, dass eine neue unveränderliche Sammlung erstellt wird. Das bedeutet, dass eine unveränderliche Sammlung stärkere Garantien für Sicherheit und Unveränderlichkeit bieten kann als eine schreibgeschützte Sammlung in Kotlin.

Ich werde dies anhand des folgenden Codes demonstrieren

import java.util.*

Spaß main(args: Array<String>) {
 val readonly = listOf(1, 2, 3)

    if(readonly is MutableList) {
 readonly.add(4)
 }

  println(readonly)
}

Sie könnten erwarten, dass dieser Code nicht einmal kompiliert werden kann, da die Operation add nicht für schreibgeschützte Sammlungen verfügbar ist und wir gerade gelernt haben, dass listOf eine schreibgeschützte Liste erstellt. Die Schnittstelle List definiert keine Funktion add. Leider lässt sich dieser Code tatsächlich kompilieren und wenn Sie ihn ausführen, wird ein UnsupportedOperationException ausgelöst.

Exception in thread "main" java.lang.UnsupportedOperationException
 at java.base/java.util.AbstractList.add(AbstractList.java:153)
 at java.base/java.util.AbstractList.add(AbstractList.java:111)
 at ImmutabilityKt.main(Immutability.kt:6)

Warum lässt sich dieser Code überhaupt kompilieren? Hier kommt der intelligente Casting-Mechanismus von Kotlin zum Tragen. Der Compiler verfolgt die is-Prüfungen und fügt automatisch (sichere) Casts ein. Infolgedessen ändert er den Typ der Variable in . Im Gegensatz zur Schnittstelle List von Kotlin verfügt die SchnittstelleMutableList über die Operation add.

Aber warum sollte die Prüfung von is MutableList überhaupt erfolgreich sein? An dieser Stelle wird der wahre Unterschied zwischen schreibgeschützten und unveränderlichen Sammlungen deutlich. Während die Funktion listOf eine schreibgeschützte Liste vom Typ List zurückgibt, ist der konkrete Typ die einfache alteArrayList (java.util.Arrays$ArrayList). Diese Sammlung ist von vornherein veränderbar. Unsere schreibgeschützte Liste wird also durch eine veränderbare Implementierung unterstützt! Schreibgeschützte Sammlungen schützen Sie nur auf der Ebene der Schnittstelle. Unter der Haube werden standardmäßige veränderbare Java-Sammlungen verwendet, so dass Sie immer noch Mist bauen können, wenn Sie nicht vorsichtig sind.

Können wir mit diesem Wissen die Dinge noch mehr kaputt machen? Sicher können wir das! Betrachten Sie den folgenden Code

Spaß main(args: Array<String>) {
 val readonly = listOf(1, 2, 3)

  Collections.sort(readonly, reverseOrder())

  println(readonly)
}

Es mag Sie überraschen (sollte es aber nicht mehr, nachdem wir oben eine Entdeckung gemacht haben), dass dieses Programm tatsächlich [3, 2, 1] ausgibt. Es ist uns gelungen, unsere schreibgeschützte Liste (!) zu verändern, indem wir sie an eine Sortierfunktion übergeben haben.

Ich habe mir selbst in den Fuß geschossen, indem ich viel zu oft veränderbare Datenstrukturen verwendet habe. Seien Sie besonders vorsichtig, wenn Sie in Kotlin schreibgeschützte Sammlungen verwenden. Sie schützen Sie nur bis zu einem gewissen Grad.

Gleichstellung

Das nächste Beispiel betrifft Kotlin-Datenklassen, die mit dem Schlüsselwort data gekennzeichnet sind. Dabei handelt es sich um Klassen, deren Hauptzweck darin besteht, Daten zu speichern und deren Standardfunktionen vom Compiler aus den Daten abgeleitet werden können. Zu diesen automatisch abgeleiteten Funktionen gehören die Funktionen equals() und hashCode(). Diese Funktionen werden automatisch vom Compiler auf der Grundlage der Eigenschaften der Klasse generiert. Sie werden verwendet, um Gleichheit und Kollisionen zu bestimmen, zum Beispiel beim Hinzufügen von Elementen zu einer Menge.

Werfen wir einen Blick auf den folgenden Code. Wir erstellen zwei Instanzen der Datenklasse A, a und b, mit demselben Wert für die Eigenschaft num. Wir ändern den Wert der Eigenschaft name von b in "b". Dann fügen wir a und b zu einer Set hinzu, um nur eindeutige Instanzen der Datenklasse A zu erhalten.

Spaß main(args: Array<String>)  {
    data class A( val num: Int) { 
        var name: String = "a"
 } 
    val a = A( 1)
    val b = A( 1)
  b.name =  "b"

    val unique = setOf(a, b)
 println(unique)
}

Sie könnten denken, dass a und b nicht als gleichwertig betrachtet werden sollten, da sie einen unterschiedlichen Wert für die Eigenschaft name haben und daher beide zu unique Set hinzugefügt werden sollten. Der Compiler berücksichtigt jedoch nur Eigenschaften, die Teil des Konstruktors einer Datenklasse sind. In diesem Fall berücksichtigt er also nur num.

Ich bin selbst nie auf diesen Fehler gestoßen, aber er ist implizit und obskur genug, um als Fallstrick in Kotlin betrachtet zu werden, wenn nicht sogar als häufiger Fehler.

Typ Sicherheit

Den nächsten Fallstrick entdeckte ich während einer Kotlin-Grundschulung, die ich durchführte. Mein Ziel war es, meinen Studenten starke Typisierung und Generika zu demonstrieren. In einer REPL zeigte ich ihnen den folgenden Beispielcode.

Spaß main(args: Array<String>) {
 Klasse Container<T>(val Wert: T)

    val noValue: Any? = null
 val container = Container(noValue)
 println(container.value is Int?)
} 

Es war ein ziemlicher Schock zu erfahren, dass dieses Programm tatsächlich true ausgibt. Aber warum? Wenn ich Any? eingebe, wird daraus Container<Any?>. Die Eigenschaft ist ebenfalls vom Typ . Die Typüberprüfung container.value is Int? sollte also false zurückgeben.

Der Grund für dieses Verhalten wird deutlich, wenn Sie den Bytecode dekompilieren, wie im Video unten gezeigt.

[embed]https://youtu.be/ugu2PCXJKNY[/embed]
Unerwartetes Verhalten bei der Typensicherheit

Wie Sie im Video sehen können, liefert der resultierende Java-Code aus dem dekompilierten Bytecode die Antwort auf dieses Rätsel. Offenbar gibt die Typüberprüfung immer true zurück, wenn der Wert null ist. Oh je!

Halten Sie den Mund!

In dem folgenden Programm versuchen wir, zwei Listen, eine Liste von Schlüsseln und eine Liste von Werten, zu zippen und eine Map der resultierenden Liste von Tupeln zu erstellen. Die Funktion zip gibt eine Liste von Tupeln mit dem Typ List<Pair<Int, String>> zurück. Die Funktion mapOf akzeptiert Tupel als vararg-Argument: vararg pairs: Pair<K, V>. Wir verwenden den Spread-Operator * auf zipped, um ihn als vararg-Argument übergeben zu können.

Spaß main(args: Array<String>)  {
    val keys = listOf(1,  2,  3)
    val values = listOf( "a", "b", "c")  
    val zipped = schlüssel.zip(werte)

    // * ist der Spread-Operator, der das Array in eine Liste von Werten entpackt
 println(mapOf(*zipped))
}

Angesichts des Typs von zipped und der Signatur von mapOf könnten wir erwarten, dass dieses Programm {1=a, 2=b, 3=c} ausgibt. Leider führt es zu einem ziemlich kryptischen Kompilierungsfehler.

Keine der folgenden Funktionen kann mit den angegebenen Argumenten aufgerufen werden.

mapOf( vararg Pair<TypVariable(K), TypVariable(V)>)  
  wobei K = TypVariable(K), V = TypVariable(V) für 
 fun <K, V> mapOf(vararg pairs: Pair< K, V>) : Karte <K, V>  definiert in kotlin.collections 
mapOf(Paar<TypVariable(K), TypVariable(V)>)  
  wobei K = TypVariable(K), V = TypVariable(V) für 
 fun <K, V> mapOf(Paar: Paar< K, V>) : Karte <K, V>  definiert in kotlin.collections 

Dies lässt sich durch das Löschen von Typen erklären. Aufgrund von Type Erasure sind einige der Typinformationen nur zur Kompilierzeit und nicht zur Laufzeit verfügbar. Es ist ein häufiger Fehler in Java oder Kotlin, die Typauslöschung zu vergessen, und das ist etwas, das man leicht in Kauf nehmen kann, wenn man mit parametrisierten Typen arbeitet.

Im obigen Beispielcode ist der Typ von zipped List<Pair<Int, String>>. Die Typargumente Int und String sind jedoch nur zur Kompilierzeit bekannt. Zur Laufzeit wissen wir nur, dass es sich um Pair handelt. Mit dem Spread-Operator * übergeben wir Pair Werte als vararg-Argumente. Wir haben die Typinformationen der inneren Typen nicht mehr zur Verfügung, daher der Kompilierungsfehler.

Wir können dieses Programm mit einer kleinen Änderung reparieren. Um die Typinformationen bei der Übergabe von Argumenten an eine varargs-Funktion zu erhalten, müssen Sie die Funktion toTypedArray verwenden.

Spaß main(args: Array<String>)  {
    val keys = listOf(1,  2,  3)
    val values = listOf( "a", "b", "c")  
    val gezippt: Liste<Paar<Int, String>>  = schlüssel.zip(werte)

    // * ist der Spread-Operator, der das Array in eine Liste von Werten entpackt
 println(mapOf(*zipped.toTypedArray()))
}

Unser modifiziertes Programm wird{1=a, 2=b, 3=c} wie vorgesehen ausgeben.

Erweiterungen

Das letzte Kotlin-Fauxpas betrifft Erweiterungsfunktionen und die Typenhierarchie. Betrachten Sie das folgende Programm. Wir haben zwei Klassen, A und B, wobei B von A erbt. Wir definieren für jede dieser Klassen eine Erweiterungsfunktion extensionFun, die "A" bzw. "B" ausgibt. Dann definieren wir eine dritte Funktion, extensionFun, die ein Argument, myVar, vom Typ A, entgegennimmt. Diese Funktion ruft die entsprechende Erweiterungsfunktion von myVar auf und gibt das Ergebnis auf der Konsole aus. Wie lautet die Ausgabe?

Spaß main(args: Array<String>) {
 open Klasse A
 Klasse B: A()

    fun A.extensionFun() = "A"
 fun B.extensionFun() = "B"

    fun extensionFun(myVar: A) {
 println(meinVar.extensionFun())
 }
 extensionFun(B())
}

Die Ausgabe dieses Programms ist "A". Zur Kompilierzeit wird festgelegt, welche Erweiterungsfunktion aufgerufen werden soll. Es spielt keine Rolle, was der Laufzeittyp ist. In diesem Fall sieht der Compiler, dass myVar vom Typ A ist, also wird Version A von extensionFun aufgerufen.

Erweiterungsfunktionen sind ein wirklich mächtiges Werkzeug. Aber Sie sollten sehr vorsichtig sein, wenn Sie Erweiterungsfunktionen in einer Typenhierarchie "überladen".

Fazit

Das war's für heute! Ich habe 7 häufige Fehler in Kotlin aufgezeigt, von denen mir die meisten selbst unterlaufen sind. Um die "Features" zusammenzufassen, die Sie beachten sollten:

  • Die Verwendung von = und {} in Ihrer Funktionsdefinition bedeutet, dass Ihre Funktion ein Lambda zurückgibt und es NICHT ausführt. Ich schaue Sie an, Scala-Entwickler ;)
  • Vermeiden Sie die Verwendung von withDefault. Geben Sie stattdessen den Fallback-Wert immer explizit an, wenn Sie mit der Funktion getOrDefault von einem Map abrufen.
  • Schreibgeschützt != unveränderlich. Seien Sie sich dessen bewusst, wenn Sie mit schreibgeschützten Sammlungen arbeiten und seien Sie besonders vorsichtig, wenn Sie schreibgeschützte Sammlungen an Java-APIs übergeben. Hier ist ein weiterer großartiger Beitrag, der die Unterschiede zwischen schreibgeschützt und unveränderbar hervorhebt, in diesem Fall in Bezug auf das Schlüsselwort val.
  • Automatisch generierte Gleichheitsprüfungen berücksichtigen nur Eigenschaften, die Teil des Konstruktors einer Datenklasse sind
  • Typ-Vergleiche bei nullbaren Typen können knifflig sein! Bei Nullwerten werden die Typen als gleich betrachtet.
  • Bei der Verwendung des Spreizungsoperators (*) kann das Löschen von Schriftzeichen hinderlich sein. Verwenden Sie toTypedArray, um die Typinformationen zu erhalten.
  • Laufzeit-Polymorphismus vermischt sich nicht mit Erweiterungsfunktionen. Welche Erweiterungsfunktion aufgerufen wird, wird zur Kompilierzeit festgelegt.

Verfasst von

Jeroen Rosenberg

Dev of the Ops / foodie / speaker / blogger. Founder of @amsscala. Passionate about Agile, Continuous Delivery. Proud father of three.

Contact

Let’s discuss how we can support your journey.