zurück zum Artikel

Kotlin statt Java: Effizienter entwickeln

Benjamin Schmid, Ralph Guderlei

(Bild: Shutterstock)

Kotlin hat als Java-Herausforderer einen steilen Aufstieg genommen. Dank klarer Struktur und strikter Standardvorgaben lassen sich typische Fehler vermeiden.

Eine ausgereifte Programmiersprache, die prägnant, sicher, eingängig und interoperabel mit Java und anderen JVM-Sprachen ist. Die Entwurfsziele von Kotlin sind schnell benannt, und das Wichtigste ist: Kotlin soll Spaß machen und produktives Programmieren fördern.

Bereits 2011 stellte JetBrains seine Programmiersprache Kotlin offiziell vor, gönnte sich danach aber knapp fünf weitere Jahre für Experimente und Reifeprozess. Das Unternehmen gab die Java-Alternative 2016 für den produktiven Einsatz frei und garantierte Langzeit-Support. Die deutliche Ausrichtung auf einen stabilen und ausgereiften Sprachkern mit modernen Sprachfeatures und zugleich voller Java-Interoperabilität überzeugte: 2019 ernannte Google Kotlin zur präferierten Sprache für die Android-Entwicklung und gründete gemeinsam mit JetBrains die gemeinnützige Kotlin Foundation. Diese lenkt seitdem alle Belange der Sprache und treibt ihre Entwicklung voran.

Auf den Erfolg von Kotlin hat sich über die Jahre auch das Java-Ökosystem eingestellt: Etablierte Framework-Größen wie Spring, Vaadin, Vert.x, Quarkus oder Micronaut bekennen sich explizit zu Kotlin. Auch für das konservative Umfeld ist es also an der Zeit, einen Blick über den Tellerrand zu wagen und sich mit dieser Java-Sprachalternative für den industriellen Einsatz näher zu beschäftigen.

Kotlin inklusive seines Ökosystems eignet sich als General Purpose Language für ein breites Aufgabenspektrum. Neben den klassischen Feldern der Backend-Entwicklung und dem Programmieren nativer Android-Apps lassen sich mit Kotlin mobile Cross-Plattform-Apps, JavaScript-Anwendungen beispielsweise mit React und sogar nativer Machinencode mit LLVM erstellen. Das Wiederverwenden von Code zwischen den Plattformen ist über das Auslagern in Multiplattform-Bibliotheken ebenfalls möglich. Der folgende Text fokussiert sich auf die grundlegenden Eigenschaften der Sprache und die Entwicklung für die JVM.

Der besondere Charme von Kotlin liegt in der Ausdrucksstärke und guten Lesbarkeit der Sprache. Einen ersten Eindruck vermittelt folgendes erweitertes "Hello World". Das Beispiel sollte für Java-Entwicklerinnen und -Entwickler gut verständlich sein:

import java.time.Instant

fun main(args: Array<String>) {
    val name = if (args.size > 0) args[0] else "Leser"
    val leser = Gast(name, anrede = Anrede.werter)

    println("Hallo ${leser.anrede} $name!")
    println(leser)
}

enum class Anrede { Herr, Frau, werter }

data class Gast(val name: String,
                var zeit: Instant = Instant.now(),
                val anrede: Anrede?)

Das Beispiel zeigt bereits einige der Eigenschaften, die das Arbeiten mit Kotlin effizient gestalten. Dazu gehören die prägnanten Schlüsselwörter wie fun für Funktionen, das Arbeiten ohne Klassen durch Top Level Functions, die optionalen Semikolons am Zeilenende oder in String Templates eingebundene Ausdrücke. Funktionsparameter können Default-Werte tragen und werden damit optional. Die Angabe von new zum Erzeugen neuer Objektinstanzen entfällt.

Prägnanz und Kürze gehören zu Kotlins Stärken. Als statisch und streng typisierte Sprache bietet Kotlin Typsicherheit und erspart dennoch Tipparbeit durch mehr Intelligenz seitens des Compilers. Über Typinferenz erschließt er den Typ der lokalen Variablen und Funktionssignaturen, sodass explizite Typangaben üblicherweise nur an Schnittstellen notwendig sind. Mit Type-safe Builders und etwas syntaktischem Zucker lassen sich darüber sogar eigene Domain-specific Languages erstellen wie die Kotlin-DSL von Gradle. Durch den stärkeren funktionalen Fokus hat in Kotlin das Thema Unveränderbarkeit (Immutability) mehr Gewicht als bei Java: Es ist einfacher und kürzer, Klassen, Variablen, Attribute und Collections in einer rein lesbaren beziehungsweise unveränderbaren Form zu deklarieren.

Ein zentraler Sicherheitsbaustein ist die dedizierte Behandlung von null-Werten im Typsystem. Das zwingt Entwicklerinnen und Entwickler, potenzielle null-Werte korrekt zu behandeln und vermeidet Java-typische Laufzeitfehler weitgehend. Dass die Vorgaben in der Praxis kaum nennenswerten Mehraufwand nach sich ziehen, ist zahlreichen Helferlein und nicht zuletzt einem pragmatischen Umgang mit Rückgabewerten aus Java-Code zu verdanken.

Sonderheft "Programmiersprachen – Next Generation"

Dieser Artikel stammt aus dem neuen iX-Developer-Sonderheft "Programmiersprachen – Next Generation". Es beschäftigt sich auf 156 Seiten schwerpunktmäßig mit den Sprachen TypeScript, Kotlin, Rust und Go. Daneben wirft es einen Blick auf aktuelle Entwicklungen bei Java, C++ und der Programmierung von Quantencomputern.

Die PDF-Ausgabe des Sonderhefts ist zum Preis von 12,99 Euro im heise-shop verfügbar [1]. Die gedruckte Ausgabe kostet 14,90 Euro. Außerdem bietet der heise Shop ein Bundle aus gedruckter Ausgabe plus PDF für 19,90 Euro an [2]. Das Heft ist zudem in gut sortierten Kiosken und Buchhandlungen verfügbar.

Trotz der Mehrwerte ist und bleibt die volle Java-Interoperabilität eines der obersten Entwurfsziele von Kotlin. Jede Java-Klasse und jedes Java-Framework lässt sich direkt verwenden. Anwendungen können Kotlin und Java frei mischen. Java-Konstrukte wie Felder oder Java-Record-Datentypen, die in Kotlin nicht direkt vorgesehen sind, generiert der Compiler auf Anfrage beispielsweise über Annotationen. Die nahtlose Interoperabilität ist nicht zuletzt dem Umstand zu verdanken, dass Kotlin auf die bestehenden JDK-Klassen setzt und beispielsweise keine konkurrierenden Runtime-Implementierungen für Collections oder andere Datentypen mitbringt. Stattdessen bildet der Compiler die Kotlin-Varianten transparent mit den Java-Typen ab.

Da Kotlin trotz seiner Knappheit eine intuitive Lesbarkeit aufweist, ist in der Praxis die Lernkurve angenehm flach. Routinierten Java-Entwicklern und -Entwicklerinnen genügt oft etwas Basiswissen und der Wunsch nach eleganterem Code, um sich – geführt durch Compiler und die Autovervollständigung der IDE – den Großteil der Kotlin-Funktionsweise zu erschließen. Dort, wo Intuition nicht mehr genügt, hilft ein kurzer Blick in die gelungene Sprachreferenz [3].

Beim ersten Kennenlernen und zum schnellen Ausprobieren ist der Kotlin Playground [4] praktisch. Wer nach dem Lesen dieses Artikels Lust auf mehr bekommt und ganz praktisch einzelne Aspekte vertiefen möchte, mag die Kotlin Koans hilfreich finden [5]: Das interaktive, strukturierte Curriculum aus 43 Lerneinheiten bietet Interessierten zahlreiche kleine Aufgaben an, die sich im Browser oder komfortabler über das Plug-in EduTools [6] in IntelliJ IDEA oder Android Studio verwenden lassen (s. Abb. 1).

Das Plug-in EduTools führt durch die Kotlin Koans und gibt Hilfestellungen zum Finden der Lösung (Abb. 1).,

Das Plug-in EduTools führt durch die Kotlin Koans und gibt Hilfestellungen zum Finden der Lösung (Abb. 1).

Wer Java entwickelt, bewegt sich bei Kotlin weiter im gewohnten Umfeld. Die ebenfalls von JetBrains stammende Entwicklungsumgebung IntelliJ IDEA und ihr Spin-off Android Studio bringen die erforderlichen Werkzeuge mit. In Eclipse oder Visual Studio Code rüsten Plug-ins Unterstützung nach, können aber hinsichtlich des Komforts und der Qualität nicht mithalten. Im Gegensatz zu anderen JVM-Sprachen bringt Kotlin kein eigenes Build-Tool mit. Stattdessen kann man die aus Java gewohnten Werkzeuge wie Ant, Gradle oder Maven über Compiler-Plug-ins nutzen.

Der Kotlin-Compiler ist angenehm schnell und verarbeitet sowohl Kotlin- als auch Java-Code. Ein automatisierter Java-zu-Kotlin-Konverter der IDE hilft bei der inkrementellen Migration (s. Abb. 2) und bietet zahlreiche Vorschläge für die weitere Optimierung. Somit gestaltet sich für Bestandsprojekte ein stufenweiser Umstieg von Java nach Kotlin risikoarm. Die hinzugewonnene Sicherheit beispielsweise bezüglich null-Safety hat dabei schon manchen Bug zutage gebracht.

Automatische Konverter helfen bei der Migration von Java nach Kotlin (Abb. 2).,

Automatische Konverter helfen bei der Migration von Java nach Kotlin (Abb. 2).

Das Einstiegsbeispiel kam teilweise noch ohne sie aus, aber auch in Kotlin spielen Klassen eine zentrale Rolle und lassen sich daher in einfachen Fällen schnell und komfortabel deklarieren: Der primäre Konstruktor direkt am Klassennamen erlaubt es, eine Klasse samt zugehöriger Properties in einem Rutsch zu definieren.

class Heft(val num: Int, 
           var titel: String)

Der folgende Code erzeugt eine komplette Klasse mit zwei Attributen. Dabei definiert var eine änderbare und val eine rein lesbare Property. Das Schlüsselwort constructor dient zum Definieren zusätzlicher oder komplexerer Konstruktoren mit Annotationen oder reinen Parametern, die allerdings den primary constructor – sofern vorhanden – verwenden müssen.

class Rechteck {
  var breite: Int
  var laenge: Int
  val flaeche  // read-only Property als Getter
    get() = breite * laenge

  constructor (b: Int, l: Int) {
    breite = b
    laenge = l
  }

  constructor(seite: Int) : this(seite, seite)
}

Vergleichbar mit .NET ersetzen Properties die typische Java-Beans-Kombination aus privatem Feld plus Getter- und Setter-Methode. Die Zugriffsmethoden generiert der Kotlin-Compiler automatisch transparent. In anderer Richtung präsentieren sich Getter/Setter-Kombinationen aus Java- oder JDK-Klassen als Properties in Kotlin (s. Abb. 3).

Getter/Setter-Kombinationen aus Java-Klassen versteht Kotlin als Properties (Abb. 3).,

Getter/Setter-Kombinationen aus Java-Klassen versteht Kotlin als Properties (Abb. 3).

Die Datenklasse Gast aus dem anfangs gezeigten "Hello World"-Beispiel ist ein einfacher, erweiterbarer Wertecontainer, der viel üblichen Boilerplate-Code erspart. Dank des zusätzlichen Schlüsselworts data werden automatisch die Funktionen equals(), hashCode(), toString(), copy() und componentN() basierend auf den Properties und ihrer Reihenfolge in der Klasse erzeugt.

Somit entspricht eine Data Class den üblichen Konventionen für Java Beans bei einem Bruchteil der Tipparbeit. Das Java-11-Äquivalent bringt es noch auf stattliche 100 Zeilen. In Java 14 hat mit den Records inzwischen ein ähnliches Konzept Einzug gehalten, allerdings nur für rein lesbare Wertecontainer und ohne die Mehrwertfunktionen. Über die Annotation @JvmRecord erzeugt der Kotlin-Compiler aus einer Datenklasse alternativ nur einen Java 14 Record.

Folgende Tabelle zeigt Java-Konstrukte mit ihren Pendants in Kotlin:

Java Kotlin
Datentypen
Object Any
void Unit
int Int
java.lang.Integer Int?
Integer[] Array<Int>
int[] IntArray
List bzw. List<?> List<out Any?>!
Konstrukte
final var a = new X(5); val a = X(5)
public final class X {...} class X {...}

Trotz Eleganz und Prägnanz: Konstruktive Sicherheit steht bei Kotlin ganz oben. Das beginnt damit, dass die Sprache auf die Bequemlichkeit der User setzt und Unveränderbarkeit aufwandsfrei beziehungsweise als Standard an erste Stelle setzt: Variablen mit val statt var sind unveränderbar. Auch die Interfaces der Standard-Collections wie List, Set oder Map sind readonly. Für veränderbare Listen stehen MutableList, MutableSet oder MutableMap im Repertoire.

Ähnlich ist die Situation bei Klassen und Methoden, die ohne die explizite Angabe des Schlüsselwortes open standardmäßig final und damit nicht offen für Vererbung oder das Überladen ihrer Methoden sind. Das Überschreiben einer Klassenmethode oder das Implementieren einer Interfacemethode muss stets explizit mit override angezeigt werden. Das ist auf der einen Seite eine mutige Entscheidung im Sprachentwurf, denn sie erfordert Achtsamkeit beim Entwurf der Bibliotheken, um bewusst an den geeigneten Stellen Erweiterbarkeit zu ermöglichen. Auf der anderen Seite hilft die Reserviertheit ab Werk, die Komplexität gering zu halten. Zudem gibt sie dem Compiler mehr Gelegenheiten für Performance-Optimierungen, da Konstrukte wie der Polymorphie-Overhead in der JVM entfallen. Nicht zuletzt hilft Unveränderbarkeit in Multithreading-Kontexten gegen Bugs und unnötige Kopfschmerzen.

open class ErweiterbareKlasse {
    open fun ueberschreibbareMethode(): String {
        return "Ich bin dein Vater."
    }
}

class AbgeleiteteKlasse : ErweiterbareKlasse() {
    override fun ueberschreibbareMethode() =
        "... ich dein Sohn!"
}

Den größten Gewinn an Führung und Sicherheit, den alteingesessene Java-Entwicklerinnern und -Entwickler zu schätzen wissen dürften, ist der effektive und proaktive Umgang mit der Nullability von Werten. Kotlin hat sich das Ziel gesetzt, Fehler aufgrund von Nullwerten beim Kompilieren zu melden, statt zu NullPointerException-Fehlern zur Laufzeit werden zu lassen. Dazu unterscheidet das Typsystem zwischen nullable und non-nullable types. Kotlin kennt beispielsweise zwei Typen für String-Werte: Der Typ String muss immer einen Wert tragen, während String? potenziell null sein kann. Ungeprüfte Dereferenzierungen, die häufig zur Mutter aller Java-Exceptions führen, erkennt der Compiler und fordert zum Beseitigen auf.

Gegen unnötigen Frust helfen Smart Cast: Kurzoperatoren und Utility-Methoden beim Umgang mit Nullwerten. Erkennt der Compiler beispielsweise durch ein if-Statement, dass ein Wert gesichert vorhanden ist, überführt er den Typ T? automatisch in T. Mit !! lässt er sich jederzeit ohne vorherige Prüfung zum Konvertieren zwingen – mit allen Konsequenzen wie Laufzeitfehlern.

var nonNullable: String = "Oh"
var nullable: String? =
    if (Random.nextBoolean()) "yes."
    else null

// Die foglenden Zeilen wuerden Compile-Fehler erzeugen. 
//    nonNullable = null
//    nonNullable = nullable
//    nullable.trim()

// Kotlin Smartcasts helfen
if (nullable != null) {
    nonNullable = nullable.trim()
}

// Safe nagivation operator
nullable = nullable?.trim()

// Elvis operator
nonNullable = nullable ?: "no."

// Ich-weiss-es-besser
//  NPE-Gefahr voraus!
nonNullable = nullable!!
nullable!!.trim()

Interessant ist dabei der Umgang von Kotlin mit Rückgabewerten aus Java-Code, bei denen zunächst unklar ist, ob sie null-Werte enthalten oder nicht. Der Compiler berücksichtigt passend konfigurierte Nullability-Annotationen und arbeitet entsprechend mit T oder T?. Da das meist nicht der Fall ist, greift ein pragmatischer Kompromiss in Kotlin: Derartige Werte führt der Compiler zunächst als internen Plattformtyp T!, der gleichzeitig sowohl T als auch T? entspricht. Der Clou: Werte vom Typ T! lassen sich ohne erzwungene Prüfungen direkt als non-nullable Typ T verwenden. Das ermöglicht ein reibungsloses Arbeiten mit Java-Bibliotheken, ohne unnötig zu frustrieren. Der Preis dafür sind potenzielle Schlupflöcher für NullPointerExceptions an ungenügend behandelten Java-Bibliotheksaufrufen.

Kotlin greift nicht nur bei der Sicherheit, sondern auch bei Funktionen hilfreich unter die Arme. Über Extension Methods und Extension Properties lassen sich fremde Klassen um zusätzliche Funktionen und feldfreie Properties erweitern, ohne die Originalklasse zu verändern. Davon macht insbesondere die Kotlin-Standardbibliothek ausgiebig Gebrauch und rüstet allein an den Java Collections unzählige Helferlein wie distinct(), filter(), forEach()oder shuffle() nach. Kotlin 1.7 stellt allein bei den Collections 165 zusätzliche Funktionen bereit.

Entsprechend gestaltete Extensions können mit fehlenden Werten umgehen. In folgendem Codeausschnitt findet die Prüfung auf null in der Extension-Funktion prepend statt, weshalb der Compiler einen Aufruf mit null erlaubt, der nicht zu einer Exception führt.

// Neue Extension-Funktion fuer Strings & null
fun String?.prepend(c: String): String {
    return if (this != null) c + this else c;
}

// Extension Property. Hier: ohne null
val String.exclaimed : String
    get() = "$this!"


var s: String? = null
s = s.prepend("Hi ") // fehlerfrei: "Hi "
println(s.exclaimed) // Dank smart cast: "Hi !"

Konventionen bringen noch mehr Eleganz. Kotlin erlaubt es, Operatoren für eigene Typen zu definieren. Per Konvention wird für ein a + b nach einer als Operator markierten und von den Typen passenden Funktion für den Aufruf a.plus(b) gesucht. Der folgende Code zeigt das Vorgehen und illustriert darüber hinaus eine weitere Konvention: Funktionen in der Form component1(), component2() und so weiter lassen sich für ein komfortables Destructuring nutzen. Da Kotlin sie für Data Classes automatisch erzeugt, lässt sich das Ergebnis der Operation über val (w, l, h) = b wieder in seine Einzelteile zerlegen.

data class Point(val x: Int, val y: Int, val z: Int) {
    operator fun plus(o: Int): Point {
        return Point(x + o, y + o, z + o)
    }
}

fun main() {
    val a = Point(7, 3, 1)
    val b = a + 2

    // Destructuring via component1()..3()
    val (w, l, h) = b
    println("$b: Raumvolumen = ${w*h*l}")
    // Point(x=9, y=5, z=3): Raumvolumen = 135
}

Ein letztes Beispiel für Kotlins Ausdrucksstärke: when ist das Gegenstück zu switch in Java und anderen C-ähnlichen Sprachen. Der wesentliche Unterschied ist, dass das Statement nicht nur exakte Übereinstimmung mit Werten prüfen kann, sondern darüber hinaus Ranges, Ausdrücke und Instanz-Prüfungen kennt. Da sich when als Ausdruck verwenden lässt, reicht der Block in folgendem Code das Ergebnis direkt weiter:

private fun describe(x: Number):String {
    val specialNums = setOf(13, 42, 99)
    return when (x) {
        1, 0 -> "binaer"

        !is Int -> "kein Integer"

        in -9..9 -> "klein"
        in specialNums -> "besonders"
        !in -99..99 -> "ausserhalb des Rahmens"

        else -> "nichts von alledem"
    }
}

fun main() {
    listOf(0, 3.14, 42, 98).forEach {
        println("$it ist ${describe(it)}")
    }
    // 0 ist binaer
    // 3.14 ist kein Integer
    // 42 ist besonders
    // 98 ist nichts von alledem
}

Das Programmierparadigma der funktionalen Programmierung hat in den letzten Jahren an Gewicht gewonnen. Im Gegensatz zu Java, das funktionale Elemente wie Lambda-Ausdrücke erst relativ spät gelernt hat, haben die Gestalter von Kotlin die funktionale Programmierung von Beginn an berücksichtigt. Daher sind Funktionen Sprachelemente erster Klasse (First-class Functions): Sie lassen sich Variablen und Datenstrukturen zuweisen, anderen Funktionen als Parameter übergeben sowie als deren Rückgabewerte entgegennehmen. Funktionen, die andere Funktionen als Parameter erhalten, bezeichnet man als Higher-order Functions, also Funktionen höherer Ordnung.

val numbers = listOf(1,2,3,4,5,6)
val aggregator: (Int, Int) -> Int = 
    { acc: Int, number: Int -> acc + number }
val sum = numbers.fold(0, aggregator)

// Lambda-Parameter als Block; implizites `it`
val evenNums = numbers.dropWhile { 
    it % 2 == 0
} 

Der Code weist der Variable aggregator eine anonyme Funktion in Form eines Lambda-Ausdrucks zu. Die Funktion kombiniert zwei Int- Werte zu einem Ergebnis und dient anschließend in der Extension Function fold als Parameter für eine Funktion höherer Ordnung. Diese typischen funktionalen Muster sind mit Kotlin deutlich weniger sperrig als mit Java und der Streams API. Für Lambda-Ausdrücke gibt es weitere Vereinfachungen, um den Umgang bequemer zu gestalten.

Praktisch ist zudem ein syntaktischer Zucker für Lambda-Ausdrücke, die als letzte Parameter einer Funktion dienen. Sie können direkt als Block nach dem Funktionsaufruf stehen. Erwartet die Lambda-Funktion nur einen Funktionsparameter, kann dessen Deklaration ebenfalls entfallen und direkt die implizit deklarierte Variable it referenziert werden. Den passenden Typ erschließt sich der Compiler dank Typinferenz.

// Dank syntaktischem Zucker fuer trailing lambdas
// sind die folgenden Statements alle gleichwertig:

view.setOnClickListener({ e -> doSomething(e) })

view.setOnClickListener() { e -> doSomething(e) }

view.setOnClickListener { e -> doSomething(e) }

view.setOnClickListener { doSomething(it) }

Koroutinen sind erst seit Kotlin 1.3 Bestandteil der Sprache. Sie helfen bei der nebenläufigen Programmierung, die durch die stark parallelisierte Verarbeitung vernetzter Anwendungen und moderner Multicore-Prozessoren deutlich an Relevanz gewonnen hat. In der Vergangenheit mussten sich die Entwickler bei der nebenläufigen Programmierung um viele technische Details wie die Synchronisation kümmern, was zeitaufwendig und fehleranfällig war. Koroutinen vereinfachen die nebenläufige Programmierung. Das Prinzip ähnelt den Goroutines aus Go [7].

Koroutinen sind pausierbare Berechnungen, die gleichzeitig mit anderem Code laufen. Sie sind grundlegend mit Threads vergleichbar, aber wesentlich schlanker und nicht an einen Thread gebunden. Nach dem Pausieren können sie auf einem anderen Thread weiterlaufen. Die Kernsprache kennt nur die grundlegenden Mittel. Die Bibliothek kotlinx.coroutines stellt die einzelnen High-Level-Funktionen wie launch und runBlocking zur Kommunikation und Synchronisation bereit und erledigt das Thread-Handling.

import kotlinx.coroutines.*

fun main() {
  runBlocking {      // Eroeffnet blockierend einen CoroutineScope
    launch {         // Startet im Hintergrund eine Coroutine
    // Dieser Lambda-Block wird in der Coroutine ausgefuehrt
    delay(999L)      // Suspendiert die Coroutine
    println("Welt!") // Ausgabe nach Fortsetzung
    }
    // Starte parallel 20x suspendierbare Funktionen
    repeat(20) { launch { printDot() } }
    print("Hallo") 
  }
println("Ende")
}

// Suspendable function: Nur aus CoroutineScope aufrufbar 
suspend fun printDot() {
  delay(500L)
  print(".")
}

Der Code zeigt ein einfaches Beispiel: Zunächst eröffnet runBlocking() einen neuen CoroutineScope. Koroutinen laufen immer im Kontext eines solchen Scope, der als Brücke zwischen der normalen Ablaufumgebung im Hauptthread und der Laufzeitumgebung agiert, die den Lebenszyklus der Koroutinen steuert. Der runBlocking-Aufruf blockiert, bis alle darin gestarteten Koroutinen beendet sind.

Anschließend startet der Code über launch() eine erste Koroutine als Lambda-Ausdruck im Hintergrund. Sie gibt den Text "World!" verzögert aus. Der delay()-Aufruf ist von besonderer Bedeutung: Diese spezielle Suspending Function stoppt die enthaltene Koroutine für eine bestimmte Zeit, während der Thread andere Koroutinen ausführen kann. printDot() definiert eine Suspending Function über das suspend-Schlüsselwort. Sie lässt sich aus Koroutinen wie eine normale Funktion aufrufen und kann ihrerseits andere Suspending Functions wie delay() aufrufen. In regulären Funktionen unterbindet der Compiler den Einsatz dagegen.

Häufig ist das Ergebnis einer nebenläufigen Berechnung für den weiteren Programmablauf wichtig. Für die Kommunikation zwischen Koroutinen dienen  Channels. Sie sind vergleichbar mit Messages Queues, über die Nachrichten beziehungsweise Daten ausgetauscht werden. Die Zustellung der Nachrichten erfolgt nach dem FIFO-Prinzip (First in, First out).

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main() = runBlocking {
    val channel = Channel<Int>()  // Channel definieren
    launch {
        val result = someExpensiveComputation()
        channel.send(result) // Ergebnisse kommunizieren
        channel.close()      // Letztes Ergebnis signalisieren
    }
    // Via for-Schleife lesen der Channel-Werte bis zum close()
    for (y in channel) println(y)
}

Ein Produzent kann beliebig viele Nachrichten erzeugen. Das Schließen eines Channels mit close() zeigt an, dass keine weiteren Nachrichten folgen. Prinzipiell können mehrere Koroutinen gleichzeitig Nachrichten an einen Channel senden (Fan-in). Ebenso können mehrere gleichzeitig Nachrichten empfangen. Jede Nachricht wird an genau einen Empfänger zugestellt.

Die Kotlin-Koroutinen bieten weit mehr Funktionen, und für Interessierte empfiehlt sich ein Blick auf das Kotlin-Framework für vernetzte Anwendungen Ktor, das mit CIO (Coroutine-based I/O) eine komplett asynchrone Engine mitbringt, die auf die leichtgewichtigen Koroutinen setzt.

Die Fehlerbehandlung in Koroutinen ist leider etwas umständlich. Fehler lassen sich nur innerhalb einer Koroutine mit einem Try-Catch-Block behandeln, nicht in anderen Koroutinen. Die Exception wird stattdessen in der Hierarchie nach oben propagiert und sorgt für die Terminierung der Koroutinen im CoroutineScope. Zum Behandeln von Exceptions dient der CoroutineExceptionHandler, der entweder im CoroutineScope oder in der Top-Level-Koroutine existieren muss:

import kotlinx.coroutines.* 

fun main() {
  val handler = CoroutineExceptionHandler { _, exception ->
    println("CoroutineExceptionHandler got $exception")
  }
    
  val topLevelScope = CoroutineScope(Job())

  topLevelScope.launch(handler) {
    launch {
      throw 
        RuntimeException("RuntimeException in nested coroutine")
    }
  }

  Thread.sleep(100)
}

Kotlin hat eine ausgeprägte Fangemeinde, und das zu Recht: Mit der Sprache ist JetBrains eine runde Alternative zu Java gelungen. Viele sinnvolle Ergänzungen unterstützen beim produktiven Entwickeln und bereiten zudem Spaß im Alltag. Auch wenn die Innovationsgeschwindigkeit im Java-Universum in den letzten Jahren wieder zugenommen hat – Kotlins Mehrwerte und sein Vorsprung dürften auf absehbare Zeit nicht bedroht sein. Vor allem den besseren Umgang mit null lernt man beim Umstieg von Java schnell zu schätzen.

Mit Google und Android im Rücken gehört Kotlin längst nicht mehr zu den Nischenprodukten, sondern ist eine robuste Technik mit Langzeitunterstützung. Durch die hervorragende Java-Kompatibilität steht weiterhin das komplette Java-Ökosystem zur Verfügung. Sprache und Werkzeuge sind ausgereift und unterstützen unter anderem bei der Migration bestehender Java-Projekte oder dem gemischten Einsatz beider Sprachen. Für Bibliotheken, die sich vorrangig an Java-Entwickler richten, kann das Festhalten an Java jedoch weiterhin sinnvoll sein. Eine Kotlin-Library mit angenehmen APIs für Java-Anwenderinnen erfordert den Griff in die Trickkiste, um auf die Java-Konventionen "downzugraden".

Alle Codebeispiele finden sich im GitHub-Repository zum Artikel [8].

Benjamin Schmid
brennt für lösungsorientierte Innovationen, Effizienz und Qualität in der agilen Softwareentwicklung. In Rollen wie CTO, Technology Advisor und Manager R&I ist er immer wieder erster Ansprechpartner in allen technologischen und methodischen Fragestellungen.

Ralph Guderlei
macht digitale Produkte erfolgreich. Dazu identifiziert er adäquate Technologien und Architekturen, optimiert Entwicklungsprozesse und verwendet agile Produktmanagement-Methoden.
(rme [9])


URL dieses Artikels:
https://www.heise.de/-7304972

Links in diesem Artikel:
[1] https://shop.heise.de/ix-developer-programmiersprachen-next-generation/Print?wt_mc=intern.shop.shop.ix-dev-programmiersprachen22.red_ho.beitrag.beitrag
[2] https://shop.heise.de/bundle-ix-developer-programmiersprachen-next-generation-heft-pdf?wt_mc=intern.shop.shop.ix-dev-programmiersprachen22.red_ho.beitrag.beitrag
[3] http://kotlinlang.org/docs/reference/
[4] https://play.kotlinlang.org/
[5] https://play.kotlinlang.org/koans/overview
[6] https://plugins.jetbrains.com/plugin/10081-edutools/docs/install-edutools-plugin.html
[7] https://www.heise.de/hintergrund/Softwareentwicklung-Concurrency-in-Go-7152320.html
[8] https://github.com/ix-magazin/dev22-Effizienter_entwickeln_mit_Kotlin
[9] mailto:rme@ix.de