Kotlin ‒ das bessere Java?

Eine Einführung in die moderne Programmiersprache Kotlin
7
Jun

Kotlin ‒ das bessere Java?

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.

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

Alle News der Java-Welt:
Alle News der Java-Welt:

Behind the Tracks

Agile & Culture
Teamwork & Methoden

Data Access & Machine Learning
Speicherung, Processing & mehr

Clouds, Kubernets & Serverless
Alles rund um Cloud

Core Java & JVM Languages
Ausblicke & Best Practices

DevOps & Continuous Delivery
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Web Development & JavaScript
JS & Webtechnologien

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Software-Architektur
Best Practices

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management