kotlin - JAX https://jax.de/tag/kotlin/ Java, Architecture & Software Innovation Wed, 02 Oct 2024 13:38:29 +0000 de-DE hourly 1 https://wordpress.org/?v=6.7.2 Showdown der funktionalen Programmierung auf der JVM https://jax.de/blog/kotlin-vs-scala-funktionale-programmierung-jvm/ Mon, 19 Feb 2024 10:08:20 +0000 https://jax.de/?p=89461 Kotlin ist eine Weiterentwicklung von Java mit zusätzlichen Features und Vereinfachungen. Die Ideen dafür kamen überwiegend aus der funktionalen Programmierung. Kein Wunder, bietet diese doch gegenüber OOP viele Vorteile, wie architektonische Entkopplung durch unveränderliche Daten und bessere Domänenmodelle durch mächtigere Abstraktionen. Aber reichen Kotlins Innovationen aus, um es zu einer waschechten funktionalen Programmiersprache zu machen?

The post Showdown der funktionalen Programmierung auf der JVM appeared first on JAX.

]]>
Der Begriff „funktionale Programmiersprache“ gründete sich ursprünglich darauf, dass die Sprache Funktionen „erstklassig“, also als normale Objekte behandelt und erlaubt, sie zur Laufzeit zu erzeugen. Bei statisch typisierten Sprachen (und dazu gehört auch Kotlin) bedeutet der Begriff außerdem, dass es das Typsystem erlaubt, Funktionen zu beschreiben, und die erstklassige Manipulation von Funktionen zumindest nicht behindert.

Ein wichtiger pragmatischer Aspekt funktionaler Programmierung (FP) ist die Verwendung unveränderlicher (immutable) Daten. Diese kann eine Programmiersprache mehr oder weniger gut unterstützen. Dazu gehört auch die Möglichkeit, Funktionen ohne die Verwendung von Zuweisungen zu schreiben. Schließlich zählt die Unterstützung von höherstehenden Abstraktionen wie Monaden zur Standardausrüstung funktionaler Sprachen.

Unveränderliche Daten

Das mit den unveränderlichen Daten klingt eigentlich ganz einfach: Wir verzichten auf Zuweisungen, fertig. Damit das praktikabel wird, gehört allerdings doch mehr dazu. Wenn eine Klasse unveränderliche Objekte beschreibt, ist es meist sinnvoll, einen Konstruktor zu definieren, der für jedes Attribut der Klasse ein Argument akzeptiert, außerdem equals– und hashCode()-Methoden, die ausschließlich über die Gleichheit beziehungsweise die Hashfunktion der Attribute definiert sind, Ähnliches gilt für die toString()-Methode. In Java hat die IDE, die Konstruktor und Methoden automatisch generieren kann, diesen Job traditionell erledigt. Danach liegt aber eine Menge Code herum, der nur das Offensichtliche tut und Übersicht sowie Wartung behindert.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Glücklicherweise hat Kotlin sogenannte Data Classes, in denen dieser Code automatisch vom Compiler generiert wird. Das ist so eine Klasse:

data class Rectangle(val length: Double, val width: Double)

Das data vor class sorgt für die automatische Generierung von Konstruktor und Methoden, val sorgt dafür, dass die entsprechenden Attribute nicht verändert werden können. (Weil das so praktisch ist, hat Java längst mit Records nachgezogen.)

Bei funktionalen Sprachen sind Pendants zur Java Collections Library dabei, die allerdings ebenfalls ohne Veränderungen auskommen. Bei Kotlin sind scheinbar unveränderliche Collections dabei. So hängen wir zum Beispiel eine Liste an eine andere Liste:

val list1 = listOf(1,2,3)
val list2 = listOf(4,5,6)
val list3 = list1 + list2

Hier ist list3 eine neue Liste mit den gleichen Elementen wie list1 (die unverändert bleibt) und den Elementen von list2 zusätzlich dahinter. Hinter den Kulissen tut aber die gute alte Java ArrayList ihren Dienst: Das bedeutet, dass die Operation + list2 ein neues Array für list3 anlegt und alle Elemente sowohl von list1 als auch von list2 nach list3 kopiert – und das ist teuer. Aus diesem Grund verwenden die Collection Libraries funktionaler Sprachen sogenannte funktionale Datenstrukturen, die nicht bei jedem Update alles kopieren müssen. Insbesondere ist eine Liste in der funktionalen Programmierung eine konkrete Datenstruktur, die bei allen (anderen) funktionalen Sprachen fest eingebaut ist. Wir können sie aber in Kotlin nachbauen:

sealed interface List<out A>
object Empty : List<Nothing>
data class Cons<A>(val first: A, val rest: List<A>) : List<A>

Eine Liste ist also entweder Empty (die leere Liste) oder eine sogenannte Cons-Liste, bestehend aus erstem Element und rest-Liste (es handelt sich also um eine unveränderliche, einfach verkettete Liste). FP-Pendants zu den ersten beiden Listen von oben können wir so konstruieren:

val list1 = Cons(1, Cons(2, Cons(3, Empty)))
val list2 = Cons(4, Cons(5, Cons(6, Empty)))

Das ist etwas umständlich, verglichen mit listOf, dieses Manko könnten wir aber mit einer einfachen varargs-Funktion beheben. Die plus-Methode auf den eingebauten Kotlin-Listen können wir für die FP-Listen ebenfalls implementieren:

fun <A> List<A>.append(other: List<A>): List<A> =
  when (this) {
    is Empty -> other
    is Cons ->
       Cons(this.first, this.rest.append(other))
  }

Das Beispiel von oben funktioniert mit FP-Listen so:

val list3 = list1.append(list2)

Die append-Methode wirft ein paar Fragen auf: Warum ist sie eine „Extension Method“? Könnte das bei der Rekursion Probleme machen? Dazu später mehr, hier geht es ja erst einmal um unveränderliche Daten: Und tatsächlich verändert append gar nichts, kopiert die Elemente aus list1, hängt aber list2 an, ohne sie zu kopieren. Hinterher teilen sich also list3 und list2 dieselben Cons-Objekte. Weil Listen unveränderlich sind, ist das unproblematisch: Das Programm kann so tun, als wären list2 und list3 völlig unabhängig voneinander. Die append-Methode verbraucht nur so viel Laufzeit und Speicher, wie list1 lang ist – die Länge von list2 ist völlig irrelevant.

Listen sind für viele Situationen geeignet, aber natürlich nicht für alle – wenn list1 lang ist, braucht append auch mehr Zeit und viel Speicherplatz. Entsprechend haben die Collection Libraries funktionaler Sprachen in der Regel auch noch ausgefeiltere Datenstrukturen auf der Basis von Bäumen beziehungsweise Trees im Gepäck, bei denen die meisten Operationen quasi konstant laufen.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Solche funktionalen Datenstrukturen gibt es auch für Java und Kotlin in Form der Vavr-Library [1], die neben FP-Listen auch Vektoren mitbringt. Für Vavr gibt es auch das Add-on vavr-kotlin [2], das die Vavr Collections in Kotlin noch etwas idiomatischer zugänglich macht. Zwischenfazit: Funktionale Datenstrukturen sind bei Kotlin nicht standardmäßig dabei, können aber in Form von Vavr nachgeladen werden. Tolle Sache.

Erstklassige Funktionen

Kommen wir zur Unterstützung für Funktionen als Objekte: Da macht Kotlin gegenüber Java einen riesigen Sprung durch zwei Neuerungen:

  • Das Kotlin-Typsystem kennt Funktionstypen, die man sich in Java umständlich als Single-Method-Interface selbst definieren muss.
  • Kotlin macht effektiv keinen Unterschied zwischen Value Types und Reference Types.

Was das bringt, zeigt die Implementierung des FP-Klassikers map, den eine Funktion (die als Objekt übergeben wird) auf alle Elemente einer Liste anwendet und dann eine Liste der Ergebnisse zurückliefert:

fun <A, B> List<A>.map(f: (A) -> B): List<B> =
  when (this) {
    is Empty -> Empty
    is Cons ->
     Cons(f(this.first),
       this.rest.map(f))
  }

Super-Sache:

val list4 = list3.map { x -> x + 1 }

Das geht natürlich auch in Java, das Pendant sieht fast identisch aus:

var list4 = list3.map(x -> x + 1);

Allerdings funktioniert das in Java nur so richtig gut, wenn der Lambdaausdruck unmittelbar im Methodenaufruf steht. In dem Moment, in dem wir ihn herausziehen, sieht die Sache anders aus. In Kotlin ist das kein Problem:

val inc = { x: Int -> x + 1 }
val list4 = list3.map(inc)

Wir müssen bei inc den Typ von x angeben, weil noch andere Typen die Operation + 1 (Double, Long) unterstützen. Den Typ der Funktion inferiert Kotlin nun als (Int) -> Int. In Java müssen wir uns allerdings beim Typ von inc für ein Interface entscheiden – am Naheliegendsten ist IntUnaryOperator:

IntUnaryOperator inc = x -> x + 1;

Allerdings können wir den nicht an map übergeben – es kommt zu einer Fehlermeldung: „‘map(Function<Integer,B>)’ cannot be applied to ‘(IntUnaryOperator)’“. Das liegt daran, dass map ein Argument des Typs Function<A, B> akzeptiert – wir mussten uns ja für ein spezifisches Interface entscheiden. Leider passen Function und IntUnaryOperator nicht zueinander, weil Function auf Referenztypen besteht, während IntUnaryOperator auf den Value Type int festgelegt ist. Die Konsequenz ist, dass zum Beispiel das Stream-Interface in Java neben map auch mapToInt, mapToDouble und mapToLong anbieten muss, die jeweils ein IntStream, DoubleStream und LongStream produzieren. Das ist umständlich und macht fortgeschrittene Anwendungen von Funktionen höherer Ordnung in Java unpraktikabel – in Kotlin kein Problem, ein weiterer Pluspunkt.

Schleifen und Endrekursion

Zu erstklassiger Unterstützung von Funktionen gehört auch, dass wir in der Lage sein sollten, Funktionen ohne Verwendung von Zuweisungen zu schreiben. Dafür sind append und map gute Beispiele – rein funktional programmiert. Sie haben aber beide denselben Haken: Für lange Listen funktionieren sie nicht, weil sie einen Stack-Overflow verursachen. Das liegt an einigen Eigenheiten der JVM: Sie muss sich merken, wie es nach dem rekursiven Aufruf weitergeht (mit Cons) und verwendet dafür einen speziellen Speicherbereich, den Stack. Dieser Stack ist in seiner Größe beschränkt und viel kleiner als der Hauptspeicher, der eigentlich zur Verfügung stünde. Außerdem ist die Größe beim Start der JVM festgelegt und später nicht mehr zu ändern. Funktionale Sprachen mit eigener Runtime haben dieses Problem meist nicht, weil sie flexiblere Datenstrukturen anstatt eines Stacks mit fester Größe verwenden.

fun <A> List<A>.append(other: List<A>): List<A> {
  var list = this.reverse()
  var acc = other
  while (list is Cons) {
    acc = Cons(list.first, acc)
    list = list.rest
  }
  return acc
}

In Java wäre der einzige Weg, das Problem zu vermeiden, eine Schleife – und Schleifen benötigen Zuweisungen, um von einem Durchlauf zum nächsten zu kommunizieren. In Kotlin sieht das so aus wie in Listing 1. Die neue append-Funktion benutzt zwei Schleifenvariablen: die Eingabeliste und das Zwischenergebnis, den Akkumulator. Die Eingabeliste wird am Anfang umgedreht, weil Listen von hinten nach vorn konstruiert werden.

Das neue append funktioniert, ist aber aus funktionaler Sicht schlecht – insbesondere, weil while-Schleifen fehleranfällig sind. Werden zum Beispiel die beiden Zuweisungen an acc und list vertauscht, funktioniert die Methode nicht mehr. Diese Methode können wir auch ohne Zuweisungen mit einer Hilfsfunktion schreiben, die den Akkumulator als Parameter akzeptiert (Listing 2).

fun <A> List<A>.append(other: List<A>): List<A> =
  appendHelper(this.reverse(), other)

fun <A> appendHelper(list: List<A>, acc: List<A>): List<A> =
  when (list) {
    is Empty -> acc
    is Cons ->
      appendHelper(list.rest, Cons(list.first, acc))
  }

Diese Version kommt ohne Zuweisungen aus und benutzt einen rekursiven Aufruf statt der while-Schleife. (Die Hilfsfunktion könnten wir auch lokal innerhalb von append definieren – ein weiterer Punkt für FP-Support in Kotlin.) Anders als der rekursive Aufruf der ursprünglichen append-Version ist dieser hier ein sogenannter Tail Call – also ein Aufruf, der am Ende der Funktion steht.

Solche Tail Calls brauchen eigentlich keinen Platz auf dem Stack – entsprechend bestünde bei appendHelper auch keine Gefahr, dass der Stack überläuft. „Eigentlich“ deshalb, weil die JVM leider auch für Tail Calls völlig unnötig Stackplatz verbraucht. (Das ist ein seit mindestens 1996 bekannter Bug, und FP-Fans fiebern mit jedem neuen JVM-Release darauf, dass endlich ein Fix dabei ist.) Wir können aber den Kotlin-Compiler überreden, den rekursiven Tail Call intern in eine Schleife zu kompilieren, indem wir vor die Funktionsdefinition das Wort tailrec schreiben. Das geht leider nur bei statischen Selbstaufrufen (insbesondere also nicht bei Aufrufen von Methoden, die spät gebunden werden), aber immerhin, mehr ist auf der JVM auch nicht drin. Zwischenfazit: Funktionen sind in Kotlin tatsächlich so erstklassig, wie es auf der JVM geht. Die Richtung stimmt also auch hier.

Typen

Kommen wir nochmal zu den Typen: Wir haben ja schon gesehen, dass Kotlin mit Funktionstypen eine wesentliche Anforderung funktionaler Programmierung erfüllt. Leider hat das Typsystem aber auch einige Tücken. Um die zu verstehen, werfen wir einen Blick darauf, wie die Definition von Listen in der funktionalen Programmiersprache Haskell aussehen würde (wenn Listen da nicht schon eingebaut wären):

data List a = Empty | Cons a (List a)

Die Struktur ist ähnlich wie die Kotlin-Definition, aber zwei Unterschiede fallen auf: In Kotlin steht beim Typparameter A noch ein out davor. Außerdem steht beim Empty-Objekt Nothing als Typparameter von List.

Das kommt daher, dass Kotlin versucht, sowohl OO als auch FP zu unterstützen: OO braucht Klassenhierarchien, was auf der Typebene zu Subtyping führt. Deshalb ist es dort notwendig, dass bei Empty ein Typparameter für List angegeben wird – und da der Typparameter A für den Typ der Elemente der Liste steht, die leere Liste aber keine Elemente hat, müssen wir einen Typ angeben, der zu allen anderen passt. In Kotlin ist das eben Nothing, ein „magischer“ Typ ohne Elemente, der als Subtyp aller anderen Typen fungiert (das Gegenteil von Any sozusagen.) Außerdem ist out notwendig, damit eine Liste aus Elementen bestehen kann, die unterschiedliche Typen haben – solange sie einen Supertyp gemeinsam haben. Falls das kompliziert klingt: ist es. Wir können uns der Sache annähern, indem wir out entfernen. Sofort kommt eine Fehlermeldung, zum Beispiel bei:

val list1 = Cons(1, Cons(2, Cons(3, Empty)))

Abgekürzt lautet diese so: „Type mismatch: inferred type is Int but Nothing was expected“. Das liegt daran, dass Empty den Typ List<Empty> hat, wir aber für die Gesamtliste den Typ List<Int> brauchen. Das out sorgt jetzt dafür, dass List<Empty> ein Subtyp von List<Int> ist und damit alles wieder passt – ein Fall von sogenannter Kovarianz. (Wenn wir in geschrieben hätten, wäre Kontravarianz angesagt und es würde andere Fehlermeldungen geben.)

Das Wort out steht dafür, dass die Methoden von List andere Listen nur als „Output“ produzieren dürfen. Das bedeutet insbesondere, dass wir append nicht als „normale“ Methode innerhalb von List implementieren können, weil sie die zweite Liste als Eingabe akzeptiert. Diese Einschränkung gilt nicht für Extension Methods, weshalb wir append als eine solche definiert haben.

Die Situation ist aber immerhin besser als in Java, das erfordert, die Varianzannotationen an die Methoden zu schreiben. Die Java-Stream-Version von append, genannt concat (ebenfalls als statische Methode realisiert), hat zum Beispiel diese Signatur:

static <T> Stream<T> concat(Stream<? extends T> a,
                            Stream<? extends T> b)

Die Kombination von OO und FP führt also zu einigen Komplikationen, auch wenn Kotlin die Situation besser meistert als Java.

Higher-kinded Types

Kotlin hat sich sichtlich von Scala inspirieren lassen, was die FP-Unterstützung betrifft, aber auch auf einige Aspekte von Scala verzichtet – in der Hoffnung, die aus Scala bekannte Komplexität einzudämmen. Dem sind leider auch nützliche Features des Scala-Typsystems zum Opfer gefallen.

Eine wesentliche Einschränkung gegenüber Scala betrifft sogenannte Higher-kinded Types, die es in Scala gibt, in Kotlin leider nicht. Zur Erläuterung dieser Typen schauen wir uns eine Kotlin-Version des Optional-Typs aus Java an. Er ist nützlich, wenn eine Funktion manchmal ein Ergebnis liefert, manchmal aber auch nicht. Entsprechend gibt es zwei Fälle Some und None, ähnlich wie bei Listen:

sealed interface Option<out A>
object None : Option<Nothing>
data class Some<A>(val value: A) : Option<A>

Wir können für Option eine praktische map-Methode definieren:

fun <A, B> Option<A>.map(f: (A) -> B): Option<B> =
  when (this) {
    is None -> None
    is Some -> Some(f(this.value))
  }

Diese Verwandtschaft mit der map-Methode von List können wir sehen, wenn wir beide Signaturen untereinanderschreiben:

fun <A, B> List  <A>.map(f: (A) -> B): List  <B>
fun <A, B> Option<A>.map(f: (A) -> B): Option<B>

Typen wie List und Option, die eine map-Operation unterstützen, heißen in der funktionalen Programmierung Funktor und spielen dort eine wichtige Rolle. Deshalb liegt es nahe, ein Interface Functor zu definieren, in dem die map-Methode steht. Das könnte so aussehen:

interface Functor<F> {
  fun <A, B> map(f: (A) -> B): F<B>
}

Mit dieser Definition wäre Functor ein Higher-kinded Type: Ein Typkonstruktor (also ein Typ mit Generics), der einen anderen Typkonstruktor als Parameter akzeptiert. Das ist dann aber leider doch ein Schritt zu viel für Kotlin, der Compiler quittiert: „Type arguments are not allowed for type parameters“. Es gibt zwar einen Trick, um Higher-kinded Types zu simulieren, nachzulesen zum Beispiel in dem wunderbaren Buch „Functional Programming in Kotlin“ [3]. Er ist aber so umständlich, dass er in der Praxis kaum anwendbar ist.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Beim Typsystem ist in Kotlin also noch Luft nach oben. Dass es geht, zeigt Scala, das Higher-kinded Types unterstützt und für das es entsprechend Libraries mit höherstehenden Konzepten wie Functor gibt, die in Kotlin einfach nicht praktikabel sind. Fairerweise muss man aber zugegeben, dass Higher-kinded Types auch von manchen „richtigen“ funktionalen Sprachen wie F# nicht unterstützt werden.

Monaden

Wo funktionale Programmierung ist, sind Monaden oft nicht weit: Sie sind dort fürs Grobe zuständig und erlauben die Verknüpfung von Funktionen in sequenziellen Abläufen mit Effekten. Effekte sind Dinge, die neben der reinen Berechnung stattfinden – typischerweise Interaktionen mit der Außenwelt. Effekte können zum Beispiel benutzt werden, um in hexagonaler Architektur zwischen Ports und Adaptern zu vermitteln. Für eine komplette Einführung in Monaden fehlt uns hier der Platz, darum skizzieren wir nur, wie sie in Kotlin funktionieren.

Die Idee ist ganz einfach: Ein sequenzieller Prozess wird durch ein Objekt repräsentiert. Für die Objekte erstellen wir einen speziellen Typ (das ist die Monade), in dem genau die Operationen enthalten sind, die in dem Prozess zulässig sind. Zum Beispiel enthält die kleine Demo-Library kotlin-free-monads [4] ein Interface TtyM, um Prozesse abzubilden, die eine textuelle Ausgabe generiereren:

sealed interface TtyM<out A> {
  fun <B> bind(next: (A) -> TtyM<B>): TtyM<B>
}

Die bind-Operation (manchmal auch flatMap genannt) macht zusammen mit einer sogenannten pure-Operation TtyM zu einer Monade. Der Typparameter A ist für das Ergebnis des Prozesses zuständig. Wenn wir die TtyM-Monade benutzen, verwenden wir also nicht mehr println, sondern konstruieren stattdessen ein Write-Objekt, dessen Klasse TtyM implementiert.

Diese Vorgehensweise hat den Vorteil, dass wir das TtyM-Objekt in unterschiedlichen Kontexten ausführen können. Im normalen Betrieb macht das folgende Funktion, die für jedes Write einfach println ausführt:

fun <A> run(tty: TtyM<A>): A

Für automatisierte Tests wollen wir aber wissen, welcher Text ausgegeben wurde, und dafür gibt es eine Funktion, die die Zeilen des Outputs in eine Liste schreibt:

fun <A> output(tty: TtyM<A>, output: MutableList<String>): A

Das ist praktisch, wenn wir schon ein TtyM-Objekt haben. Aber dessen Konstruktion mit Konstruktor- und Methodenaufrufen ist normalerweise ziemlich umständlich, mit vielen geschachtelten Lambdaausdrücken und Klammern. Aus diesem Grund haben die meisten funktionalen Sprachen eine spezielle, deutlich kompaktere Syntax für monadische Programme. Kotlin bringt eine solche Syntax zwar nicht direkt mit, wir können sie aber mit Hilfe von suspend-Funktionen selbst bauen, sodass ein TtyM-Programm zum Beispiel so gebaut werden kann:

TtyDsl.effect {
  write("Mike")
  write("Sperber")
  pure(42)
}

Das Lambdaargument von effect ist eine suspend-Funktion. Das suspend sorgt dafür, dass der Compiler die Funktion in eine Coroutine transformiert. Die wiederum erlaubt der effect-Funktion, eine monadische Form der Funktion zu extrahieren. Wer sich für die Details interessiert, sei an das Kotlin-free-monads-Projekt [4] verwiesen.

Auch hier also ein Pluspunkt für Kotlin, zumindest gegenüber Java. Die Verwendung von suspend-Funktionen für monadische Syntax ist aber gegenüber funktionalen Sprachen deutlich aufwendiger und auch mit einigen Einschränkungen verbunden, sodass immer noch Luft nach oben bleibt.

Fazit

Mit zwei Fragen sind wir in diesen Artikel gestartet:

  1. Ist Kotlin funktional genug, dass es sich in lohnt, darin funktional zu programmieren?
  2. Ist Kotlin die richtige Sprache, wenn man auf der JVM funktional programmieren möchte?

Die Antwort auf die erste Frage ist ein entschiedenes Ja: Kotlin unterstützt die einfache Definition unveränderlicher Daten, kennt Typen für Funktionen, eliminiert die lästige Unterscheidung zwischen Referenztypen und Value Types, kompiliert rekursive Tail Calls korrekt, vereinfacht gegenüber Java den Umgang mit Generics und erlaubt die Definition monadischer Syntax. Das ist mehr als genug, um funktionale Programmierung gewinnbringend zu nutzen, mächtige Abstraktionen zu definieren und architektonisch von der Entkopplung durch Unveränderlichkeit zu profitieren. Wer mehr darüber wissen möchte, dem sei das oben erwähnte Buch [3] ans Herz gelegt. Eine grundsätzliche Einführung in funktionale Programmierung gibt es in meinem eigenen Buch „Schreibe Dein Programm!“ [5].

Die zweite Frage ist schwieriger zu beantworten: Auf der JVM gibt es zwei (weitere) funktionale Sprachen, nämlich Clojure und Scala. Beide sind großartig – während aber Clojure in vielerlei Hinsicht fundamental anders tickt als Kotlin und Java (kein Typsystem, Lisp-Syntax), ist Scala die Verlängerung der Linie von Java zu Kotlin.

 

Scala unterstützt funktionale Programmierung besser als Kotlin: Es gibt Higher-kinded Types, die Monaden funktionieren in Scala deutlich einfacher und außerdem erleichtern sogenannte Implicits es, automatisch die richtigen Implementierungen für Interfaces zu finden. Des Weiteren existiert um Scala herum ein umfangreiches Ökosystem funktionaler Libraries und Frameworks.

Die gegenüber Scala verringerte Komplexität von Kotlin fällt in der Praxis kaum ins Gewicht. Viele der typischen Schwierigkeiten in der täglichen Arbeit gibt es in Kotlin auch. Die IDE-Unterstützung ist für beide ähnlich gut. Wer also funktionale Programmierung mag und die Wahl hat, wird wahrscheinlich mit Scala glücklicher.


Links & Literatur

[1] Vavr, funktionale Library für Java: https://github.com/vavr-io/vavr

[2] Kotlin-Wrapper für Vavr: https://github.com/vavr-io/vavr-kotlin

[3] Vermeulen, Marco; Bjarnason, Rúnar; Chiusano, Paul: „Functional Programming in Kotlin“; Manning, 2021

[4] Monaden-Library für Kotlin: https://github.com/active-group/kotlin-free-monad

[5] Sperber, Michael; Klaeren, Herbert: „Schreibe Dein Programm!“; Tübingen University Press, 2023

The post Showdown der funktionalen Programmierung auf der JVM appeared first on JAX.

]]>
Mit Ktor und Spring Boot Server- und Clientanwendungen entwickeln https://jax.de/blog/mit-ktor-und-spring-boot-server-und-clientanwendungen-entwickeln/ Wed, 15 Mar 2023 08:17:46 +0000 https://jax.de/?p=88428 Wie können mit Spring Boot und Ktor in Kotlin Server- und Clientanwendungen entwickelt werden? Dieser Artikel begleitet durch das Zusammenspiel von Spring Boot und Ktor. Kotlin eignet sich hervorragend, um Non-blocking-Server- und -Clientanwendungen zu entwickeln. Dafür gibt es mit Ktor sogar ein spezielles Framework. Doch auch der Platzhirsch für Anwendungen im JVM-Ökosystem, Spring Boot, muss sich hier nicht verstecken, sondern kann hervorragend im Zusammenspiel mit Kotlin verwendet werden.

The post Mit Ktor und Spring Boot Server- und Clientanwendungen entwickeln appeared first on JAX.

]]>
Das Kotlin-Ökosystem ist umfangreich. Entwickler JetBrains arbeitet nicht nur an der Sprache selbst, sondern veröffentlicht diverse Frameworks, die speziell für Kotlin entwickelt werden. Ktor [1] ist ein solches Framework, das die Entwicklung von Server- und Clientanwendungen in Kotlin ermöglicht. Ktor sticht vor allem dadurch hervor, dass es auch für native Builds mit Kotlin Native geeignet ist. Das zweite Hauptargument für Ktor ist, dass es ausschließlich non-blocking arbeitet und daher asynchrone Verarbeitung der Standard ist.

Ktor-Server

Der Anfang einer Entwicklung mit Ktor ist dabei so einfach wie bei den meisten anderen aktuellen Frameworks im Umfeld der Java Virtual Machine (JVM). Die Entwickler bieten einen Starter [2], mit dessen Hilfe ein Maven- oder Gradle-Projekt erstellt werden kann. Hierbei können der verwendete Webserver und benötigte Plug-ins direkt ausgewählt werden. Abhängigkeiten zwischen den verschiedenen Komponenten löst der Projektgenerator auf und fügt notwendige, abhängige Plug-ins hinzu, sodass ein lauffähiges Projekt entsteht.

Für ein einfaches „Hello World“-Beispiel verwenden wir Netty als Webserver und die Plug-ins Routing und kotlinx.serialization, um zum einen Routing anhand von Pfaden zu ermöglichen und zum anderen das Kotlin-eigene Serialisierungsmodul zu nutzen. Letzteres fügt als zusätzliches Plug-in noch ContentNegotiation hinzu, das es Ktor ermöglicht, ContentType- und Accept-Header auszuwerten und darauf zu reagieren. Ktor bietet weitere Plug-ins, die für fortgeschrittene Anwendungsfälle geeignet sind, wie zum Beispiel Authentifizierung, Session-Management, LDAP-Anbindung, Template-Engines und WebSocket-Unterstützung, um nur einige Beispiele zu nennen. Die vollständige „Hello World“-Anwendung zeigt Listing 1.

import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
 
fun main() {
  embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
    install(ContentNegotiation) {
      json()
    }
    configureRouting()
  }.start(wait = true)
}
 
fun Application.configureRouting() {
  routing {
    route("/hello") {
      get("/{name?}") {
        val name = call.parameters["name"] ?: "World"
 
        call.respond("Hello $name!")
      }
    }
  }
}

In der main-Methode wird über die Methode embeddedServer die Ktor-Anwendung erzeugt und mit der start-Methode gestartet. Der Parameter wait = true gibt dabei an, dass der Hauptanwendungs-Thread blockiert werden soll, die Anwendung also so lange läuft, bis sie beendet wird.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Über die embeddedServer-Methode werden die Server-Engine, der Port und auch die Ktor-Plug-ins konfiguriert. Das ContentNegotiation-Plug-in wird zur Anwendung hinzugefügt und für JSON konfiguriert. Die Konfiguration des Routings könnte ebenso inline erfolgen, wird hier aber in eine Extension Function ausgelagert. Generell kann das Routing in mehrere Extension Functions ausgelagert werden und so eine Gruppierung nach Funktionalität, Pfad oder Ähnlichem erfolgen.

Das Routing selbst geschieht über die Ktor-DSL. Das Beispiel aus Listing 1 konfiguriert die Route / hello/$name, wobei $name ein optionaler Path-Parameter (erkennbar am ?) ist. Mit call.parameters kann auf die Request-Parameter zugegriffen werden, mit call.respond wird die Serverantwort geschrieben. Ein Aufruf von /hello gibt „Hello World!“ zurück, ein Aufruf von / hello/Ktor gibt „Hello Ktor!“ zurück.

Damit ist eine erste, sehr einfache Ktor-Anwendung fertig. Der Non-blocking-Aspekt von Ktor scheint in diesem simplen Beispiel verborgen zu sein, ist aber tatsächlich schon vorhanden. Die Signaturen der Methoden call.parameters und call.respond haben jeweils das Schlüsselwort suspend in ihrer Deklaration, was bedeutet, dass sie auf besondere Weise ausgeführt werden. Zunächst jedoch ein Blick darauf, wofür Non-Blocking relevant ist.

Non-blocking Serveranwendungen

Webserver stellen eine Menge Threads zur Verfügung. Jeder dieser Threads verarbeitet dabei einen Request nach dem anderen. Die Anzahl paralleler Requests ist also durch die Zahl der Threads limitiert. Komplexere Anwendungen können jedoch eine Anfrage oftmals nicht ohne externe Kommunikation verarbeiten. Externe Zugriffe sind nötig, zum Beispiel auf eine Datenbank, das Dateisystem oder auf externe HTTP-APIs. Beim Warten auf diese externen Zugriffe wird der Webserver-Thread blockiert. Der Skalierung der Webserver-Threads sind dabei schnell Grenzen gesetzt, da Threads generell ressourcenintensiv sind und der Bedarf an Arbeitsspeicher schnell ansteigt. Optimierungen im Code selbst, die zum Beispiel Zugriffe auf Datenbanken oder externe APIs via Threads parallelisieren, bringen nicht nur das Ressourcenproblem mit, sondern erhöhen zusätzlich die Komplexität des Codes signifikant, da die Threads synchronisiert werden müssen. Auch wird dabei nicht das Problem gelöst, dass der Webserver-Thread blockiert werden muss, um auf das Beenden aller gestarteten Threads zu warten.

Ein häufig von Frameworks angebotener Lösungsansatz hierfür ist Reactive Programming. Dabei wird durch ein entsprechendes Framework, wie zum Beispiel Project Reactor [3], das Scheduling übernommen. Statt zu blockieren, wird dabei der Thread genutzt, um weitere Anfragen zu verarbeiten. So ist ein höherer Parallelisierungsgrad möglich, ohne zusätzliche Threads zu benötigen. Mit Reactive Programming ist jedoch eine steile Lernkurve verbunden. Das Programmiermodell unterscheidet sich grundlegend von der imperativen Programmierung, da es für fast jedes Problem spezielle Operatoren gibt.

Ktor nutzt für die parallelisierte Verarbeitung einen anderen Ansatz, der es erlaubt, den imperativen Programmierstil beizubehalten: Kotlin Coroutines.

Kotlin Coroutines

Koroutinen [4] sind spezielle Funktionen, die unterbrochen und wieder fortgesetzt werden können. Kotlin unterstützt Koroutinen über eine Core Library namens Kotlin Coroutines [5]. Den Kern von Kotlin Coroutines bilden suspendierbare Funktionen, die mit dem Schlüsselwort suspend fun deklariert werden. Diese können nur innerhalb eines Coroutine Scopes ausgeführt werden. Das einfache Beispiel in Listing 2 startet einen Coroutine Scope mit der Methode runBlocking. Durch diese Methode wird der Hauptthread pausiert, bis alle gestarteten Koroutinen abgeschlossen sind. Mittels launch werden innerhalb einer for-Schleife 1 000 000 Koroutinen gestartet, die jeweils zehn Sekunden pausieren und anschließend einen Wert ausgeben. Die Ausführung zeigt, dass nach einer initialen Verzögerung von zehn Sekunden alle Zahlen kurz nacheinander ausgegeben werden. Die Ausführung – und damit auch die zehn Sekunden Wartezeit – wird also parallelisiert.

fun main() {
  runBlocking {
    for (i in 0..1_000_000) {
      launch { printIn10Seconds(i) }
    }
  }
}
 
suspend fun printIn10Seconds(value: Int) {
  delay(10_000)
  println(value)
}

Listing 3 zeigt denselben Code mit Threads. Dieser läuft (auf durchschnittlicher Hardware) spürbar weniger performant, da er deutlich mehr Systemressourcen benötigt.

fun main() {
  val threads = mutableListOf<Thread>()
  for (i in 0..1_000_000) {
    val thread = Thread { printIn10Seconds(i) }
    threads.add(thread)
    thread.start()
  }
  threads.forEach { it.join() }
}
 
fun printIn10Seconds(value: Int) {
  Thread.sleep(10_000)
  println(value)
}

suspend funs können nur innerhalb eines Coroutine Scopes ausgeführt werden. Das bedeutet in unserem initialen Ktor-Beispiel, das bereits zwei suspend funs benutzt hat, dass Ktor den gesamten Code in einem Coroutine Scope ausführt. Konkret startet Ktor für jeden Request eine eigene Koroutine und ermöglicht somit eine deutlich höhere Parallelisierung, ohne dass der Entwickler sich umstellen oder irgendetwas beachten muss. Jedoch bleibt ihm die Flexibilität erhalten, selbst suspendierbare Funktionen aufzurufen.

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Ktor-Client

Ktor bietet nicht nur eine einfache Möglichkeit, eine non-blocking Serveranwendung zu implementieren. Auch non-blocking Clientanwendungen sind möglich. Dass hierbei auch die Clients konsequent mit Hilfe von Coroutines implementiert werden, bringt auch hier den Vorteil, dass der Thread, der die Requests ausführt, nicht blockiert werden muss, während auf den Server gewartet wird, sondern weiterverwendet werden kann.

Als Beispiel wollen wir unsere „Hello World“-Anwendung erweitern, sodass sie User, die bei einem anderen Service via ID abgefragt werden, mit Namen begrüßen kann.

Hierfür benötigen wir weitere Abhängigkeiten, die wir in unsere Build-Konfiguration (Maven oder Gradle) aufnehmen: io.ktor:ktor-client-core, io.ktor:ktor-client-cio und io.ktor:ktor-client-content-negotiation. Den Client implementieren wir so, wie in Listing 4 dargestellt.

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
 
class UserService(
  val client: HttpClient = HttpClient(CIO) {
    expectSuccess = true
    install(ContentNegotiation) {
      json()
    }
  }
) {
 
  suspend fun findUser(userId: String): User =
    client.get("http://localhost:8181/users/$userId").body<User>()
}
 
@Serializable
data class User(
  val id: String,
  val name: String,
)

Wir erzeugen zunächst einen HTTP-Client, der JSON Accept-Header verschickt und JSON-Antworten deserialisieren kann. Außerdem konfigurieren wir ihn via expectSuccess = true so, dass alle nicht 2xx Response Codes zu einer Exception führen.

Die Methode findUser ruft ein HTTP API auf und deserialisiert die JSON-Antwort in ein User-Objekt. Dieses wird mit @Serializeable für das Kotlin-Serialisierungsmodul (kotlinx.serializable) serialisierbar gemacht. client.get ist dabei eine suspend fun, also muss entweder die Methode findUser ebenfalls eine suspend fun sein oder explizit einen Coroutine Scope starten. Da wir die Methode aus dem Ktor-Server heraus nutzen wollen, der bereits einen Coroutine Scope zur Verfügung stellt, definieren wir die Methode als suspend fun. Listing 5 zeigt die Integration des Clients in den Server.

fun Application.configureRouting() {
  routing {
    route("/hello") {
      get("/{name?}") {
        val name = call.parameters["name"] ?: "World"
 
        call.respond("Hello $name!")
      }
      get("/user/{userId}") {
        try {
          val user = userService.findUser(call.parameters.getOrFail("userId"))
          call.respond("Hello ${user.name}!")
        } catch (e: ClientRequestException) {
          if (e.response.status == HttpStatusCode.NotFound) {
            call.respond(HttpStatusCode.BadRequest)
          } else {
            call.respond(HttpStatusCode.InternalServerError)
          }
        }
      }
    }
  }
}

Hierfür wird die Funktion configureRouting erweitert. Wir definieren eine zusätzliche Route als GET Request auf /hello/user/$userId. Ein Aufruf versucht, die übermittelte User-ID aufzulösen und den Namen auszulesen. Wird der User nicht gefunden, wird ein passender Fehler zurückgegeben. Der zugehörige Server ist in Listing 6 dargestellt.

val userService = UserService()
 
fun main() {
  embeddedServer(Netty, port = 8181, host = "0.0.0.0") {
    install(ContentNegotiation) {
      json()
    }
    configureRouting()
  }.start(wait = true)
}
 
fun Application.configureRouting() {
  routing {
    route("/users") {
      get() {
        call.respond(
          userService.users
            .map { User(id = it.key, name = it.value) }
        )
      }
      get("/{id}") {
        val id = call.parameters.getOrFail("id")
        try {
          val name = userService.users.getValue(id)
 
          call.respond(User(id = id, name = name))
        } catch (_: NoSuchElementException) {
          call.respond(HttpStatusCode.NotFound)
        }
 
      }
      post("/{name}") {
      call.respond(userService.createUser(call.parameters.getOrFail("name")))
        }
    }
  }
}
 
@Serializable
data class User(
  val id: String,
  val name: String,
)
 
class UserService(
  val users: MutableMap<String, String> = mutableMapOf(),
) {
  fun createUser(
    name: String,
  ): String {
    val id = "${UUID.randomUUID()}"
    users[id] = name
 
    return id
  }
}

Neben GET Requests wird auch ein POST Request definiert. Außerdem können @Serializable-Klassen von Ktor ebenfalls zu JSON serialisiert werden, wenn diese als Antwort zurückgegeben werden sollen.

Mit Ktor ist es, wie die Beispiele zeigen, sehr einfach möglich, HTTP-Kommunikation zu implementieren. Diese ist standardmäßig non-blocking und damit sehr ressourceneffizient umgesetzt.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Kotlin Coroutines und Spring Boot

Den großen Vorteil von Kotlin Coroutines, einfach parallelisierbaren Code zu entwickeln, nutzt auch das am weitesten verbreitete Framework für die JVM, Spring Boot. Mit Version 5 hat das Spring Framework Unterstützung für non-blocking Verarbeitung der Requests erhalten: Spring Webflux [6]. Während Java-Entwickler hier auf den Reactive Programming Stack von Project Reactor angewiesen sind und damit ihre Entwicklungspattern umstellen müssen, können Kotlin-Entwickler ihr gewohntes Entwicklungspattern beibehalten, da jegliche Parallelität auch über Kotlin Coroutines abgebildet werden kann. Verantwortlich hierfür ist das Kotlin-Modul org.jetbrains.kotlinx:kotlinx-coroutines-reactor. Es bindet Kotlin Coroutines an Project Reactor an.

Der Einstieg in die Entwicklung ist dabei genauso einfach wie bei jedem anderen Spring-Projekt. Über den Spring Boot Starter [7] wird das Projekt konfiguriert. Statt der Abhängigkeit Spring Web wird Spring Reactive Web verwendet. Die Anbindung von Kotlin Coroutines an Project Reactor wird für Kotlin-Projekte automatisch hinzugefügt.

Die Implementierung erfolgt dann fast wie gewohnt (Listing 7). Der einzige Unterschied ist die Methodensignatur der Methode hello. Diese ist als suspend fun, also als Koroutine deklariert. Ist eine Methode mit RequestMapping als suspend fun definiert, so startet Spring automatisch einen Coroutine Scope für die Ausführung des Requests.

@RestController
@RequestMapping("/hello")
class HelloController(
  val userService: UserService
) {
 
  @GetMapping(value = ["" ,"/{name}"])
  suspend fun hello(
    @PathVariable(required = false) name: String?
  ) =
    "Hello ${name ?: "World"}!"
}

Der Coroutine Scope ermöglicht es auch, weitere suspend funs aufzurufen. Das ist insbesondere dann von Vorteil, wenn auch die aufgerufenen Funktionen non-blocking implementiert sind. Das ist zum Beispiel beim Spring WebClient der Fall. Dieser basiert auf dem Reactive Stack von Project Reactor und kann auch mit Kotlin Coroutines verwendet werden. Der UserService aus dem Ktor-Beispiel (Listing 4) kann mit dem Spring WebClient implementiert werden, wie in Listing 8 gezeigt.

@Service
class UserService(
  val webClient: WebClient = WebClient.create()
) {
  suspend fun findUser(userId: String): User {
    return webClient.get().uri("http://localhost:8181/users/$userId")
      .accept(APPLICATION_JSON)
      .awaitExchange {
        if (it.statusCode() == HttpStatus.NOT_FOUND) {
          throw UserIdNotFoundException()
        }
        it.awaitBody<User>()
      }
  }
}
 
data class User(
  val id: String,
  val name: String,
)
 
class UserIdNotFoundException: Exception()

Der Controller aus Listing 7 kann dann um den Code aus Listing 9 erweitert werden, sodass dieselbe Funktionalität entsteht wie im Ktor-Beispiel aus Listing 5.

@GetMapping("/user/{userId}")
suspend fun helloUser(
  @PathVariable userId: String
) =
    "Hello ${userService.findUser(userId).name}!"
 
@ExceptionHandler(UserIdNotFoundException::class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
suspend fun handleUserIdNotFoundException(e: UserIdNotFoundException) { }

Auch mit Spring Boot ist es sehr einfach möglich, die Vorteile von Kotlin Coroutines zur besseren Parallelisierung zu nutzen.

Fazit: Spring Boot und Ktor in Kotlin

Kotlin bietet mit Kotlin Coroutines eine sehr einfache Möglichkeit, parallelen Code zu schreiben, ohne die damit verbundene Komplexität explizit selbst verwalten zu müssen. Außerdem benötigen Kotlin Coroutines deutlich weniger Ressourcen als Threads. Coroutines haben außerdem eine sehr geringe Einstiegshürde, da die gewohnten Entwicklungspattern beibehalten werden können.

Basierend auf Kotlin Coroutines können Client- und Serveranwendungen sehr einfach entwickelt werden. Hierfür gibt es mit Ktor ein Kotlin-natives Framework, das einen unkomplizierten Einstieg ermöglicht. Ktor ist dabei ein kleines Framework dessen Funktionsumfang auf das limitiert ist, was zur Entwicklung eines Servers oder Clients benötigt wird. Dafür ist eine Einbindung in Kotlin Native möglich, die Build-Artefakte bleiben kompakt und es kann einfacher mit anderen Libraries oder Frameworks kombiniert werden. Außerdem gibt es die non-blocking Verwendung der Webserver-Threads automatisch dazu.

Non-blocking Anwendungen können aber auch über Spring Boot mit Kotlin Coroutines entwickelt werden. Hier muss man sich allerdings explizit für den Non-blocking-Ansatz entscheiden. Im Gegenzug gibt es das komplette Spring-Ökosystem dazu: Dependency Injection, Spring Data und vieles mehr.


Links & Literatur

[1] https://ktor.io/

[2] https://start.ktor.io/

[3] https://projectreactor.io/

[4] https://de.wikipedia.org/wiki/Koroutine

[5] https://kotlinlang.org/docs/coroutines-guide.html

[6] https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html

[7] https://start.spring.io

The post Mit Ktor und Spring Boot Server- und Clientanwendungen entwickeln appeared first on JAX.

]]>
Kotlin ‒ das bessere Java? https://jax.de/blog/kotlin-das-bessere-java/ Mon, 07 Jun 2021 10:36:31 +0000 https://jax.de/?p=83667 Kotlin ist eine Programmiersprache, die im Februar 2016 in Version 1.0 das Licht der Welt erblickt hat. Im Jahr 2019 hat Google Kotlin zum „First-class Citizen“ der Android-Entwicklung erklärt und ihr damit zum Durchbruch verholfen. Doch was macht eigentlich Kotlin besonders und wieso lohnt sich ein Blick darauf auch für alte Java-Hasen, die nichts mit Android zu tun haben? Diese und weitere Fragen behandle ich in diesem Artikel.

The post Kotlin ‒ das bessere Java? appeared first on JAX.

]]>
Eines gleich vorweg: Ich bin kein Kotlin-Fanboy und finde Java als Programmiersprache sehr gelungen. Meine tägliche Arbeit dreht sich vor allem um die Backend-Entwicklung auf der JVM, hauptsächlich in Java. Allerdings bin ich der Meinung, dass man auch öfter mal über den Tellerrand blicken und sich mit neueren Sprachen auf der JVM beschäftigen darf. Dieser Artikel geht davon aus, dass Sie bereits Java „sprechen“, und zeigt interessante Features von Kotlin aus dieser Sicht. Ich konzentriere mich dabei auf die meiner Meinung nach wichtigsten Features von Kotlin: Funktionen, Variablen, null safety, objektorientierte Programmierung, funktionale Programmierung und Interoperabilität mit Java.

Kotlin wird als Open-Source-Software unter der Apache-2.0-Lizenz von der Firma JetBrains entwickelt. Das ist die Firma, die auch die bekannte IDE IntelliJ IDEA vertreibt und entsprechend gut ist dort auch die Unterstützung für Kotlin. Es gibt aber auch Plug-ins für Eclipse und NetBeans. Kotlin wird bereits seit 2010 entwickelt, allerdings erlangte es erst 2016 mit Version 1.0 einige Bekanntheit. Der endgültige Durchbruch, zumindest in der Android-Entwicklung, kam 2019, als Google auf der Google I/O die Android-Entwicklung „Kotlin-first“ deklariert hat.

Kotlin ist eine moderne, statisch typisierte Programmiersprache, die Konzepte aus der objektorientierten und der funktionalen Programmierung vereint. Ein besonderes Augenmerk liegt auf der Kompatibilität mit Java-Code – dies ist auch eines der Differenzierungsmerkmale von Kotlin gegenüber Scala. Entwickler können in Kotlin problemlos Java-Bibliotheken verwenden und müssen dabei auch nicht Datentypen, wie z. B. Listen, konvertieren. Ebenso ist es möglich, aus Java heraus Kotlin-Code zu verwenden. Der Kotlin-Compiler erzeugt Bytecode für die JVM (Java Virtual Machine) – d. h. Programme, die in Kotlin geschrieben sind, können mit einer handelsüblichen Java-Installation ausgeführt werden. Es wird nur eine JVM in mindestens Version 1.6 benötigt.

Kotlin selbst vermarktet sich im Vergleich zu Java als „concise“, also ohne viel so genannten Boilerplate-Code. Kotlin-Code ist um ca. 40 Prozent kürzer als vergleichbarer Java-Code und damit ausdrucksstärker [1]. Außerdem hat Kotlin ein verbessertes Typsystem, das vor allem darauf abzielt, NullPointerExceptions zu vermeiden. Aber schauen wir uns diese Versprechen doch einmal genauer an.

Hello World, in Kotlin

Wir beginnen klassisch mit „Hello World“:

package wjax
 
fun main(args: Array<String>) {
  print("Hello world")
}

Genau wie Java ist Kotlin-Code in Packages verwaltet. Was in obigem Beispiel sofort auffällt: Es ist nicht notwendig, eine Klasse zu erstellen, um die main-Funktion zu definieren. Funktionen werden mit dem Keyword fun deklariert (kurz für „function“). Bei den Parametern wird zuerst der Name des Parameters (args), danach dessen Typ (Array<String>) angegeben. Es ist also genau umgekehrt zu Java. Arrays werden als generische Typen ausgedrückt und nicht, wie in Java, mit eckigen Klammern hinter dem Typ (z. B. String[]).

Im Funktions-Body benötigt man hinter den Statements keine Semikola, sie sind in Kotlin optional. In der Funktion findet sich nun ein Funktionsaufruf von print(…) ohne Objektinstanz. Es ist in Kotlin also möglich, Funktionen ohne zugehörige Objektinstanz aufzurufen. In der Standardbibliothek gibt es mehrere Funktionen, die keiner Klasse angehören. print(…) ist hier eine Abkürzung für System.out.print(…), das einem vermutlich bekannt vorkommt. Führt man diesen Code aus, erscheint auf der Konsole wie erwartet „Hello World“.

Typinferenz und Funktionen

In Listing 1 werden Variablen mit dem Keyword val deklariert. Den Typ der Variablen kann Kotlin automatisch per Typinferenz bestimmen (in diesem Fall Int, das Kotlin-Äquivalent zu Javas int). Wem das nicht gefällt, der kann den Typ getrennt mit einem Doppelpunkt explizit angeben: val operand1: Int = 1. Variablen, die mit val definiert werden, sind automatisch unveränderlich (immutable), ähnlich wie final in Java. Eine weitere Zuweisung würde einen Compilerfehler auslösen. Das ist ein wichtiges Konzept in Kotlin: Man sollte so viel wie möglich unveränderlich machen.

fun main(args: Array<String>) {
  val operand1 = 1
  val operand2 = 2
  val sum = operand1 + operand2
 
  print("$operand1 + $operand2 = $sum")
}

Die Typinferenz funktioniert auch, wenn mehrere Variablen zusammen verwendet werden – im Beispiel bestimmt der Ergebnistyp des +-Operators den Typ der Variablen sum (Int).

Im print(…)-Statement habe ich ein weiteres Feature von Kotlin verwendet, die sogenannte Stringinterpolation, die die Variablen operand1, operand2 und sum in einen String einbettet. Wenn das Programm ausgeführt wird, erscheint auf der Konsole 1 + 2 = 3.

private fun sum(operand1: Int, operand2: Int = 0): Int = operand1 + operand2
 
fun main(args: Array<String>) {
  val result1 = sum(1, 2)
  println("1 + 2 = $result1")
 
  val result2 = sum(1)
  println("1 + 0 = $result2")
}

In der ersten Zeile in Listing 2 ist eine Funktion namens sum deklariert. Der Modifier private gibt an, dass die Funktion nur in derselben Datei verwendet werden kann. Außerdem ist der Rückgabetyp der Funktion mit angegeben, hier Int. Den Rückgabetyp gibt man mit einem Doppelpunkt nach der Parameterdeklaration an. Im Beispiel ist auch die Kurzschreibweise für Funktionen verwendet, die auf geschweifte Klammern verzichtet und direkt mit = hinter der Signatur eingeleitet wird. Diese Schreibweise kann für alle Funktionen verwendet werden, die nur aus einer Zeile Code bestehen. Der Parameter operand2 ist außerdem standardmäßig auf 0 gesetzt, d. h., dass der Aufrufer ihn nicht explizit angeben muss. Optionale Parameter sind sicherlich ein Feature, das sich viele Java-Entwickler schon immer gewünscht haben.

In der main-Funktion sieht man den Aufruf von sum zuerst mit explizit gesetzten Argumenten 1 und 2. Der zweite Aufruf der Funktion sum lässt das zweite Argument weg, sodass der Standardwert des Parameters (0) verwendet wird. Wenn dieser Code ausgeführt wird, zeigt die Konsole

1 + 2 = 3
1 + 0 = 1

Null Safety

Das in meinen Augen beste Feature von Kotlin ist das verbesserte null-Handling. Ein oft gesehener Fehler bei (Java-)Programmen ist die NullPointerException. Sie tritt auf, wenn eine Variable null enthält, der Entwickler den Fall aber nicht behandelt hat und trotzdem auf die Variable zugreift. Kotlin verhindert dies durch eine Erweiterung des Typsystems: nullable Variablen haben einen anderen Typ als Variablen, die nicht null sein können. An einem Beispiel wird das anschaulicher.

fun formatName(firstName: String, midName: String?, lastName: String): String {
  var result = firstName.toLowerCase()
  result += midName.toLowerCase()
  result += lastName.toLowerCase()
 
  return result
}

In Listing 3 ist eine Funktion deklariert, die drei Parameter annimmt: firstName vom Typ String, midName vom Typ String? und lastName vom Typ String. Der Typ String? ist hier kein Tippfehler, sondern Kotlins Syntax für einen nullable String. Endet ein Typ mit einem Fragezeichen, bedeutet das, dass null als Wert erlaubt ist. Steht kein Fragezeichen am Typ, ist null auch kein valider Wert für diesen Typ.

Die Variable result wurde mit dem Keyword var deklariert – das bedeutet, dass die Variable änderbar (mutable, das Gegenteil von immutable) ist.

Listing 3 enthält außerdem einen Fehler, den Kotlin erkennt und so eine NullPointerException verhindert. Wenn der midName null ist, verursacht der Aufruf der toLowerCase()-Funktion eine NullPointerException. Da Kotlin weiß, dass midName null sein kann (durch den Typ String?), wird der Fehler zur Compilezeit erkannt und die Kompilierung abgebrochen.

Um den Code dennoch kompilieren zu können, ist die in Listing 4 enthaltene Änderung notwendig.

fun formatName(firstName: String, midName: String?, lastName: String): String {
  var result = firstName.toLowerCase()
  if (midName != null) {
    result += midName.toLowerCase()
  }
  result += lastName.toLowerCase()
 
  return result
}

Durch das if wird Kotlin nun „bewiesen“, dass der Entwickler den null-Fall behandelt hat, und der Aufruf der Funktion toLowerCase() ist innerhalb des if möglich. Im Typsystem hat die Variable midName in dem if-Block den Typ von String? (nullable) auf String (nicht nullable) geändert. Dieses Konzept bezeichnet Kotlin als Smart Casts, und man findet es auch an anderen Stellen, z. B. bei Typecasts. Der Aufruf der Funktion kann nun so stattfinden:

val name = formatName("Moritz", null, "Kammerer")
val name2 = formatName("Moritz", "Matthias", "Kammerer")
val name3 = formatName(null, "Matthias", "Kammerer")

Zeile 1 setzt den midName auf null, was durch den Typ String? erlaubt ist. Zeile 2 setzt den midName auf Matthias. Zeile 3 kompiliert nicht, da der Typ des ersten Parameters String ist – und null ist bei String nicht als Wert erlaubt.

Das null-Handling von Kotlin ist dabei nicht nur bei Referenztypen erlaubt, sondern auch bei primitiven Typen wie int, boolean etc. Kotlin unterscheidet im Gegensatz zu Java nicht zwischen primitiven und Referenztypen – so ist es auch möglich, auf int, boolean etc. Funktionen aufzurufen (Listing 5).

val count = 1
val enabled = true
val percentage = 0.2
 
println(count.toString())
println(enabled.toString())
println(percentage.toString())

Die Typen der Variablen im Beispiel sind übrigens Int, Boolean und Double – es gibt keine kleingeschriebenen Varianten wie int, boolean oder double wie in Java. Der Compiler kümmert sich um das Autoboxing, sollte es nötig sein.

Sehen wir uns nun an, was Kotlin in Sachen Objektorientierung zu bieten hat.

Objektorientierte Programmierung in Kotlin

Listing 6 definiert eine neue Klasse namens Square. Diese Klasse hat eine Property length vom Typ Int, die immutable ist. Als Property bezeichnet man dabei die Kombination von Feld und dessen Zugriffsfunktionen. Kotlin generiert hier automatisch eine Getter-Funktion sowie einen Konstruktorparameter für length. Würde man statt val ein var verwenden, würde Kotlin noch eine Setter-Funktion generieren. Es ist zudem noch die Funktion area() definiert, die die Property length verwendet, um den Flächeninhalt auszurechnen.

class Square(val length: Int) {
  fun area(): Int = length * length
}

Interessanterweise ist diese Klasse automatisch public – ein fehlender Modifier bedeutet also public, nicht wie bei Java package protected. Außerdem wird die Klasse automatisch als nicht vererbbar markiert (final in Java). Wenn man dies nicht möchte, kann man das Keyword open verwenden. Ein Aufrufer kann diese Klasse nun so verwenden:

val square = Square(4)
 
val length = square.length
val area = square.area()
 
println("Length: $length, Area: $area")

Zeile 1 erstellt eine neue Instanz der Klasse Square mit length = 4. Kotlin hat kein new-Keyword, was ich persönlich etwas schade finde – ich habe des Öfteren schon Java-Code nach new durchsucht, um Objektinstanziierungen zu finden. Klar, hier würde auch die IDE helfen, aber bei Code-Reviews auf z. B. GitHub oder GitLab hat man diese ja nicht unbedingt.

Auf Properties greift man einfach mittels des Punkt-Operators zu, analog zum Feldzugriff in Java. Unter der Haube wird allerdings die Getter-Funktion aufgerufen. Aufrufe von Funktionen sehen genau aus wie in Java.

Listing 7 zeigt eine Vererbungshierarchie: Die Klasse Square erbt von der abstrakten Klasse Shape, die eine abstrakte Funktion namens area() definiert, die Square implementiert.

abstract class Shape {
  abstract fun area(): Int
}
 
class Square(val length: Int): Shape() {
  override fun area(): Int = length * length
}

In Kotlin gibt es kein extends-Keyword, man verwendet den Doppelpunkt. Außerdem ist override ein Keyword, nicht wie in Java eine Annotation. Es ist zudem möglich, mehrere public-Klassen in einer Datei zu definieren und diese Datei beliebig zu benennen. Das kann in manchen Situationen Vorteile haben, könnte aber auch zu Nachteilen wie erschwerter Navigation im Code führen. Die Kotlin Coding Conventions haben dazu eine Empfehlung [2]: Wenn die Klassen semantisch zusammengehören und die Datei nicht zu viele Zeilen hat, ist es okay, die Klassen in eine Datei zu schreiben.

Interessanterweise kennt Kotlin das Keyword static nicht. Es können also keine statischen Funktionen definiert werden. An deren Stelle treten die beiden Konzepte Singletons und Companion Objects. Ein Singleton, in Kotlin object genannt, ist eine Klasse, die nicht instanziiert werden kann und von der immer genau eine Instanz existiert:

object FixedSquare: Shape() {
  override fun area(): Int = 25
}

Singletons werden mit dem Keyword object definiert und Aufrufer müssen (und können) keine Instanzen davon erzeugen. Der Funktionsaufruf sieht aus wie ein Aufruf einer statischen Funktion in Java:

val area = FixedSquare.area()

Will man nun eine Klasse mit einer „statischen“ Funktion ausstatten, sieht das so aus:

class Square(val length: Int): Shape() {
  override fun area(): Int = length * length
 
  companion object {
    fun create() : Square = Square(5)
  }
}

Die statischen Funktionen der Klasse, in diesem Fall nur create(), befinden sich in einem Companion Object und können in einer Weise aufgerufen werden, die wie static in Java aussieht:

val area = Square.create()

Ich persönlich finde die Lösung mit dem Companion Object etwas seltsam, und ich bin damit auch nicht allein [3]. Die Designer begründen die Entscheidung damit, dass es Top-Level-Funktionen gibt und man damit kaum mehr statische Funktionen benötigt. Außerdem ist ein Companion Object ein vollwertiges Objekt, kann also auch z. B. Interfaces implementieren und in einer Referenz gespeichert werden.

Was ich hingegen hervorragend gelöst finde, ist das Verhalten von if, try etc. Diese Kontrollstrukturen sind in Java Statements, keine Expressions. Der Unterschied: Ein Statement ist ein Stück Code, das keinen Rückgabetyp hat, während eine Expression einen Wert liefert. Wie oft haben wir in Java schon Code dieser Art geschrieben:

boolean green = true;
String hex;
if (green) {
  hex = "#00FF00";
} else {
  hex = "#FF0000";
}

Das Problem hierbei ist, dass in Java ein if keinen Wert zurückgeben kann. Java löst dieses Problem speziell für if mit dem ternären Operator ? (z. B. String hex = green ? “#00FF00” : “#FF0000”). In Kotlin braucht es diesen Operator nicht (Listing 8).

val green = true
val hex = if (green) {
  "#00FF00"
} else {
  "#FF0000"
}
 
// Oder kürzer:
val hex = if (green) "#00FF00" else "#FF0000"

Das Konzept ist dabei nicht auf if beschränkt, sondern funktioniert z. B. auch mit try-catch (Listing 9).

val input = "house"
 
val result = try {
  input.toInt()
} catch (e: NumberFormatException) {
  -1
}

Hier wird auf dem String input die Funktion toInt() aufgerufen. Diese Funktion wirft, falls input kein gültiger Integer ist, eine NumberFormatException. Die Exception wird mit dem umschließenden try-catch gefangen, das im Fehlerfall -1 zurückgibt. Weil try-catch eine Expression ist, also einen Wert zurückgibt, wird der Wert (entweder der Integer-Wert des Strings oder -1 im Fehlerfall) der Variablen result zugewiesen. Dieses Feature habe ich in Java schon sehr oft vermisst, und tatsächlich brachte Java 12 so etwas Ähnliches auch mit den Switch-Expressions [4]. Apropos Exceptions: Kotlin hat, im Gegensatz zu Java, keine Checked Exceptions.

In Listing 9 wird die Funktion toInt() auf einem String aufgerufen. Diese Funktion existiert im JDK nicht auf der Klasse String. Woher kommt sie also? Die Lösung des Geheimnisses heißt Extension Functions.

Extension Functions

In Kotlin kann man beliebige Typen, auch wenn sie nicht im eigenen Code definiert sind, durch Funktionen erweitern.

private fun String.isPalindrome(): Boolean {
  return this.reversed() == this
}

In obigem Beispiel wird auf dem Typ String die Funktion isPalindrome() definiert. Diese Funktion gibt true zurück, falls der String ein Palindrom ist. Ein Palindrom ist ein Wort, das man sowohl von vorne als auch von hinten lesen kann, z. B. „anna“ oder „otto“.

Der String, der geprüft werden soll, ist in der Referenz this zu finden. Anders als in Java lassen sich in Kotlin Strings (und alle anderen Objekte) mit dem ==-Operator vergleichen. In Kotlin ruft der ==-Operator equals() auf und vergleicht nicht wie in Java die Referenzen der Objekte. Sollte man den Referenzvergleich in Kotlin brauchen, so existiert der ===-Operator. Die Funktion in obigem Beispiel ist in der längeren Schreibweise geschrieben, mit geschweiften Klammern und einem expliziten return. Da die Funktion nur aus einer Zeile besteht, könnte man aber auch die Kurzschreibweise verwenden. Man kann die Extension Function nun folgendermaßen aufrufen:

import wjax.isPalindrome
 
val palindrom = "anna".isPalindrome()
println(palindrom)

Es sieht so aus, als ob String eine weitere Funktion namens isPalindrome() besitzt. Extension Functions sind ein praktisches Werkzeug, um vorgegebene Typen um weitere Funktionen zu erweitern und damit den Code lesbarer zu machen. Übrigens: Der Aufrufer muss die Extension Function explizit über ein import-Statement importieren, damit er sie nutzen kann.

Functional Programming

Funktionale Konzepte kamen in Java 8 mit den Streams. Kotlin erlaubt es ebenfalls, einen funktionalen Programmierstil zu verwenden (Listing 10).

val names = listOf("Moritz", "Sebastian", "Stephan")
val namesWithS = names
  .map { it.toLowerCase() }
  .filter { it.startsWith("s") }
namesWithS.forEach { name -> println(name) }

Die erste Zeile von Listing 10 erstellt eine neue Liste mit drei Strings, Kotlin inferiert dabei den Typ der Liste als List<String>.

Die map-Funktion transformiert jedes Element in einen String mit Kleinbuchstaben, Lambdas gibt man dabei mit geschweiften Klammern an. Erhält das Lambda nur einen Parameter (in diesem Beispiel der String, der transformiert wird), ist der Name des Parameters nicht notwendig. Möchte man in diesem Fall auf den Parameter zugreifen, kann man den Variablennamen it verwenden.

Nach der map-Funktion selektiert die filter-Funktion nur die Strings, die mit einem S anfangen. Das Resultat dieser ganzen Pipeline wird in der Variablen namesWithS gespeichert. Der Typ dieser Variable ist ebenfalls List<String>. Im Gegensatz zu Java arbeiten die funktionalen Operatoren direkt auf Listen, Sets etc. und nicht auf Streams, die dann zu Listen, Sets etc. umgewandelt werden müssen. Wünscht man dasselbe Verhalten wie Java Streams, vor allem die Lazy Evaluation, dann stehen Sequences bereit.

In der letzten Zeile ist ein Lambda mit einem expliziten Parameternamen name definiert, und die Namen werden auf der Konsole ausgegeben:

sebastian
stephan

Auch weitere funktionale Konstrukte wie zip, reduce und foldLeft lassen sich mit Kotlin über vordefinierte Funktionen verwenden (Listing 11).

val firstNames = listOf("Moritz", "Sebastian", "Stephan")
val lastNames = listOf("Kammerer", "Weber", "Schmidt")
 
firstNames
  .zip(lastNames)
  .forEach { pair -> println(pair.first + " " + pair.second) }

Listing 11 erzeugt mittels der zip-Funktion aus zwei Listen (Typ List<String>) eine Liste vom Typ List<Pair<String, String>>. Kotlin bringt also schon ein paar generische Containertypen wie Pair, Triple etc. mit. Möchte man statt pair.first und pair.second lieber fachlich sprechende Namen verwenden, ist das auch möglich, aber etwas mehr Arbeit:

data class Person(
  val firstName: String,
  val lastName: String
)

In diesem Beispiel verwenden wir ein weiteres tolles Feature von Kotlin, die Data Classes. Diese Klassen erzeugen automatisch Getter (und ggf. Setter), hashCode()-, equals()- und toString()-Funktionen. Auch eine sehr praktische copy()-Funktion wird erzeugt, mit der man eine Kopie der Data Class erstellt und dabei auch einzelne Properties ändern kann. Data Classes sind vergleichbar mit den Records, die in Java 14 Einzug gehalten haben [5]. Eine Data Class kann nun folgendermaßen verwendet werden:

firstNames
  .zip(lastNames)
  .map { Person(it.first, it.second) }
  .forEach { person -> println(person.firstName + " " + person.lastName) }

Die map-Funktion transformiert das Pair<String, String> in eine Person-Instanz, und in der forEach-Funktion kann man nun die fachlich richtigen Namen verwenden. Data Classes sind nicht nur beim funktionalen Programmieren hilfreich, ich z. B. verwende sie sehr oft für DTOs (Data Transfer Objects), Klassen, die nur aus Feldern und Zugriffsfunktionen bestehen.

Die Kotlin-Designer beheben übrigens auch ein Versäumnis der Java-Entwickler: Listen, Maps, Sets etc. sind in Kotlin standardmäßig unveränderlich, haben also keine add-, remove-, set– etc. Funktionen. Trotzdem zeigt auch hier Kotlin seinen pragmatischen Kern, denn es gibt auch veränderbare Listen in Form von MutableList, MutableSet, MutableMap etc. Diese veränderbaren Varianten erben von ihrem unveränderlichen Pendant.

Interoperabilität mit Java

Ein wichtiger Punkt bei Kotlin ist die Zusammenarbeit mit bestehendem Java-Code. Kotlin verzichtet z. B. auf die Einführung eines eigenen Collection Frameworks, sodass Java-Funktionen, die Listen erwarten, auch mit den „Kotlin-Listen“ aufgerufen werden können. Generell ist der Aufruf von Java-Code aus Kotlin kein Problem. An manchen Stellen wendet der Compiler auch etwas Magie an. So sind z. B. die Getter und Setter von Java-Klassen in Kotlin als Properties verwendbar:

public class Person {
  private String firstName;
  private String lastName;
  private LocalDate birthday;
  // Konstruktor, Getter und Setter
}

Diese in Java geschriebene Klasse lässt sich einfach in Kotlin verwenden:

val person = Person("Moritz", "Kammerer", LocalDate.of(1986, 1, 2))
 
println(
  person.firstName + " " + person.lastName + " wurde am " +
  person.birthday + " geboren"
)

Die getFirstName()-Funktion in Java ist nun als firstName Property in Kotlin verfügbar. Es werden sowohl Typen aus der Java-Bibliothek, wie hier im Beispiel LocalDate, unterstützt als auch eigens geschriebener Code in Java. Es ist sogar möglich, Kotlin und Java in einem Projekt zu mischen. Ich finde dieses Feature toll, weil man damit auch bestehende Projekte nach und nach zu Kotlin konvertieren kann (falls man dies möchte) und keine Big-Bang-Migration braucht. Man könnte auch, um Kotlin etwas kennenzulernen, den Produktivcode weiter in Java schreiben, den Testcode aber in Kotlin. IntelliJ bietet auch einen automatischen Java-zu-Kotlin-Konverter an. Dieser erzeugt zwar nicht optimalen Kotlin-Code, ist aber durchaus brauchbar.

Hochinteressant ist auch das null-Handling, wenn man Kotlin- und Java-Code mischt. Kotlin deutet dabei die diversen Nullability-Annotationen (@Nullable, @NotNull etc.) von Java. Wenn diese Annotationen allerdings fehlen, dann versagt die null Safety von Kotlin. In diesem Fall führt Kotlin für Java-Code sogenannte Platform Types ein, z. B. String!. Das Ausrufezeichen bedeutet, dass Kotlin nicht erkennen kann, ob der Typ null sein kann oder nicht. Der Kotlin-Programmierer kann bei Variablen dieses Typs dann so tun, als seien sie nicht nullable, muss aber mit NullPointerExceptions zur Laufzeit rechnen. Ein Beispiel macht dies klarer:

public class Person {
  private String firstName;
  private String middleName; // Nullable!
  private String lastName;
  // Konstruktor, Getter und Setter
}

In dieser in Java definierten Klasse ist middleName per fachlicher Definition nullable. Verwendet man diese Klasse in Kotlin, sucht der Kotlin-Compiler zuerst nach Nullability-Annotationen. Da diese Klasse keine solche Annotationen besitzt, ist der Typ von firstName, middleName und lastName String!, d. h., dass diese Variablen null sein könnten oder auch nicht. Beim Verwenden dieser Java-Klasse in Kotlin sind dann wieder NullPointerExceptions möglich:

val person = Person("Moritz", null, "Kammerer")
println(person.middleName.toLowerCase())

Das obige Listing ruft auf der Variablen middleName die Funktion toLowerCase() auf. Dieser Code kompiliert und lässt sich auch ausführen, führt aber zur Laufzeit zu einer NullPointerException:

java.lang.NullPointerException: person.middleName must not be null

Immerhin hat die NullPointerException eine sinnvolle Fehlermeldung und zeigt genau an, was null war. Auch bei den Platform Types zeigt sich wieder die pragmatische Natur von Kotlin: Die Sprachdesigner hätten ja auch alle Typen, deren Nullability nicht bekannt ist, als nullable annehmen können. Dies hätte aber, wenn man Java-Code in Kotlin verwenden möchte, zu jeder Menge (vermutlich unnötigen) null-Checks geführt und damit die Interoperabilität mit Java um einiges erschwert. Die jetzige Lösung empfinde ich als gut gewählten Trade-off. Viele Java-Bibliotheken und Frameworks setzen auch schon die Nullability-Annotationen ein, so z. B. Spring. Wenn man mit diesen Bibliotheken in Kotlin arbeitet, merkt man nicht, dass sie in Java geschrieben sind – auch die null Safety ist wieder gegeben.

Fazit

Ich finde, dass Kotlin eine sehr gelungene Programmiersprache ist. Als Java-Entwickler fühlt man sich sofort heimisch. Viele Probleme sind pragmatisch gelöst. Kotlin nimmt sich dabei vieler kleinerer „Probleme“ an, die Java hat. In aller Fairness muss man aber sagen, dass Java für diese „Probleme“ entweder gute Workarounds hat (z. B. Codegenerierung über die IDE oder Lombok) oder die Java-Entwickler bereits Lösungen dafür geschaffen haben (Switch Expression, Multiline Strings, Records, NullPointerExceptions).

Ein großer Pluspunkt von Kotlin, der in Java so nicht zu finden ist, ist das null-Handling. Meiner Meinung nach hat Kotlin dafür eine perfekte Lösung geschaffen – zumindest solange man sich in purem Kotlin-Code bewegt. Wenn man Java-Interoperabilität benötigt, um z. B. eigenen Java-Code aufzurufen oder eine der vielen tollen Bibliotheken aus dem Java-Umfeld (wie Spring Boot) zu verwenden, funktioniert das auch gut – man muss aber bei den Platform Types aufpassen.

Als Backend-Entwickler habe ich mehrere Projekte mit Spring Boot und Kotlin umgesetzt, und es hat viel Spaß gemacht – Kotlin ist also keineswegs nur auf Android-Entwicklung beschränkt. Mit Kotlin fühlt man sich mindestens so produktiv wie in Java, und die Standardbibliothek hat viele nützliche kleine Helferlein. Zugegeben, der Kotlin-Compiler ist langsamer als der von Java, aber die Features machen das wieder wett. Aus Sicht des Software-Engineerings macht die Sprache auch vieles richtig: hervorragende IDE-Unterstützung und das Verwenden der Java-üblichen Buildtools Maven und Gradle. Die Unterstützung von statischen Codeanalysetools wie SonarQube lässt allerdings noch zu wünschen übrig.

Kotlin hat noch ein paar mehr tolle Features zu bieten, die ich aus Platzmangel hier nicht mehr beschrieben habe. Wer jetzt Lust auf mehr bekommen hat, dem empfehle ich die Kotlin Koans [6]. Mit diesen kann man anhand von kleinen vorgegebenen Problemstellungen Kotlin lernen, und das, ohne den Browser verlassen zu müssen.

Und um die Frage der Artikelheadline zu beantworten: Java ist und bleibt eine tolle Programmiersprache, die gerade in der letzten Zeit ordentlich den Feature-Hahn aufgedreht hat. Kotlin ist allerdings auch nicht zu verachten und gefällt durch gut durchdachte und an Problemen der echten Welt orientierten Features. Also: Kotlin oder Java? Die Antwort lautet wie immer beim Software-Engineering: „It depends“.

Links & Literatur

[1] https://kotlinlang.org/docs/faq.html

[2] https://kotlinlang.org/docs/coding-conventions.html

[3] https://discuss.kotlinlang.org/t/what-is-the-advantage-of-companion-object-vs-static-keyword/4034

[4] https://openjdk.java.net/jeps/361

[5] https://openjdk.java.net/jeps/359

[6] https://play.kotlinlang.org/koans/overview

The post Kotlin ‒ das bessere Java? appeared first on JAX.

]]>