Stand: 2024-04-30
Programmieren wie Hemingway
Gute Lesbarkeit einer Programmiersprache trägt maßgeblich zur Verständlichkeit des Codes bei. Wie schlagen sich Java und Kotlin in dieser Disziplin?
Inhalt
Genau wie natürliche Sprachen dienen Programmiersprachen der Kommunikation – nicht nur zwischen Mensch und Maschine, sondern vor allem auch zwischen Menschen. Deshalb ist es wichtig, dass Programmiersprachen leicht verständlich sind, ohne ausschweifend zu sein. Java ist in dieser Hinsicht nicht schlecht, allerdings sieht man Kotlin deutlich an, dass Lesbarkeit beim Entwurf der Sprache einen höheren Stellenwert hatte. Aber bilde dir am besten selbst eine Meinung anhand konkreter Beispiele.
Schleifen
Selbst die altbekannte for-Schleife lässt sich noch etwas verbessern – in Kotlin kann man sie wie einen Satz lesen:
for (item in list)
Java-Programmierer müssen dagegen wissen, was der Doppelpunkt bedeutet. Flüssig lesen kann man den Code nicht:
for (Item item : list)
Falls tatsächlich mal klassisch eine Schleife mit einem hochgezählten Integer-Wert durchlaufen werden soll, ist auch das in Kotlin sehr gut lesbar möglich:
for (i in 0..10) {
println(i)
}
Zum Vergleich das Java-Pendant:
for (int i = 0; i <= 10; i++) {
System.out.println(i);
}
Destructuring Declarations
Objekte können in Kotlin an verschiedenen Stellen im Code mit Destructuring Declarations in ihre Einzelteile zerlegt werden. Im einfachsten Fall kann dies bei einer Zuweisung geschehen:
val (x, y) = point
Mit diesem Konstrukt lässt sich auch einfach über alle Key-Value-Paare einer Map iterieren:
val capitals = mapOf("Germany" to "Berlin", "France" to "Paris")
for ((country, capital) in capitals) {
println("Capital of $country is $capital.")
}
Eine direkte Entsprechung gibt es in Java nicht. Dort müsste man sich mit entrySet()
die Einträge besorgen und diese durchlaufen:
final var capitals = Map.of("Germany", "Berlin", "France", "Paris");
for (var entry : capitals.entrySet()) {
System.out.printf(
"Capital of %s is %s.%n", entry.getKey(), entry.getValue());
}
Gleichheit
Ein typischer Anfängerfehler in Java ist der Vergleich von Objekten mit ==
, was aber nur bei primitiven Datentypen wie Integer zum erwarteten Ergebnis führt, denn bei Objekten wird damit stattdessen die Identität verglichen, also ob zwei Referenzen auf dieselbe Stelle im Speicher zeigen. Stattdessen muss man in Java Objekte mit der equals
-Methode vergleichen:
objectA.equals(objectB)
Vergleiche in Kotlin werden auch bei Objekten mit ==
durchgeführt:
objectA == objectB
Überladene Operatoren
Die Vergleichssyntax ist durch Operatorüberladung möglich, was auch in anderen Fällen gut lesbar ist, wie die Addition von zwei BigInteger-Objekten zeigt:
bigIntegerA + bigIntegerB
Der entsprechende Java-Code wirkt deutlich umständlicher und ist schwerer zu erfassen:
bigIntegerA.plus(bigIntegerB)
Gerade bei längeren Rechnungen wird es durch die ausgeschriebenen Methoden und Klammern schnell unübersichtlich.
Ganz nebenbei sorgt Operatorüberladung für Konsistenz, denn warum sollte etwa die Addition anders aussehen, nur weil die Operanden einen weniger üblichen Datentyp haben? Oder warum sollte man auf das x-te Element einer Liste anders zugreifen als auf das x-te Element eines Arrays? Gleichförmige Syntax sorgt dafür, dass man den Code schneller erfassen kann und verbessert so ebenfalls die Lesbarkeit.
Auch an anderen Stellen tragen Operatoren in Kotlin zur Lesbarkeit bei. Mit dem in
-Operator kann zum Beispiel geprüft werden, ob ein Wert in einem Set enthalten ist:
if (color in trendColors)
Beim Kompilieren wird der Operator in
zu einem Aufruf der contains
-Methode übersetzt.
Wer befürchtet, dass Operatorüberladung zu kryptischem Code führen könnte, sei beruhigt, denn die Designer der Sprache haben das nur für eine überschaubare Menge allgemein bekannter Operatoren erlaubt. Auswüchse wie in Scala, wo es in Bibliotheken Operatoren wie ~~>
gibt, sind in Kotlin ausgeschlossen.
Wertebereiche
Keine Entsprechung in Java gibt es für Kotlins Syntax für Wertebereiche (Range). Damit lässt sich zum Beispiel prüfen, ob ein Wert innerhalb eines bestimmten Bereichs liegt. Ob Wasser in flüssigem Zustand ist, ließe sich etwa so prüfen:
if (temperature in 0..100)
Bedingungsoperator in lesbar
Obwohl der Bedingungsoperator, oder oft auch “ternärer Operator” genannt, von der Lesbarkeit eine Zumutung ist, würden ihn manche Programmierer am liebsten auch in Kotlin sehen – vermutlich haben sie ein Stockholmsyndrom entwickelt! Zum Glück haben die Kotlin-Schöpfer aber auch hier die Lesbarkeit favorisiert, sodass das ohnehin schon vorhandene if
/else
als Ausdruck an seine Stelle tritt:
val color = if (status == Status.ERROR) Color.RED else Color.GREEN
Dieser Code liest sich fast wie ein Satz. Wie soll man dagegen das folgende Java-Pendant lesen?
final var color = status == Status.ERROR ? Color.RED : Color.GREEN;
Spätestens bei Verschachtelungen führt dieser Ansatz zu Kopfschmerzen.
Konstruktoren
Während in Java Konstruktoren genauso heißen müssen wie die Klasse, heißen sie in Kotlin selbsterklärend constructor
. Der sogenannte primäre Konstruktor direkt hinter dem Klassennamen wird im folgenden Beispiel zur Verdeutlichung explizit als constructor
gekennzeichnet – üblich ist aber, dieses Schlüsselwort bei Pirmärkonstruktoren wegzulassen:
class Pixel constructor( // Primärkonstruktor
val red: Short,
val green: Short,
val blue: Short
) {
// Sekundärer Konstruktor
constructor(greyScale: Short): this(greyScale, greyScale, greyScale)
}
Abgesehen von dem selbsterklärenden Schlüsselwort constructor
tragen die Primärkonstruktoren auch schon einiges zur Lesbarkeit bei, indem sie den Code deutlich verkürzen. In vielen Fällen reicht sogar dieser eine Konstruktor, sodass der Code schnell zu überblicken ist.
Bei dem entsprechenden Java-Code, muss man dagegen wissen, dass diese Dinge, die so ähnlich aussehen wie Methoden und genauso heißen wie die Klasse, die Konstruktoren sind:
class Pixel {
final short red;
final short green;
final short blue;
Pixel(short red, short green, short blue) {
this.red = red;
this.green = green;
this.blue = blue;
}
Pixel(short greyScale) {
this(greyScale, greyScale, greyScale);
}
}
Dass hier bei Kotlin weniger Schreibarbeit anfällt, liegt vor allem an der Möglichkeit mit val
(oder var
) direkt ein Property für den Parameter durch den Compiler erstellen zu lassen.
Objekte ausgeschrieben
Warum lassen sich in vielen objektorientierten Sprachen nur Klassen als Vorlagen für Objekte beschreiben, aber keine Objekte? Und was ist, wenn man nur ein einziges Objekt, ein sogenanntes Singleton, haben will?
Es gibt verschiedene Varianten Singletons in Java zu schreiben, die wohl einfachste sieht so aus:
class Counter {
public static final Counter instance = new Counter();
private int value = 0;
// Verhindert, dass von außen weitere Objekte erzeugt werden.
private Counter() {}
public void increment() {
value++;
}
public int value() {
return value;
}
}
In Kotlin können Objekte buchstäblich geschrieben werden:
object Counter {
var value: Int = 0
private set
fun increment() {
value++
}
}
Die Kotlin-Variante enthält deutlich weniger technischen Ballast und ist dadurch schneller zu erfassen.
Vererbung und Implementierung
Ableitung und Vererbung gehören zu den wenigen Fällen, in denen Java-Code verständlicher ist, denn im Code steht wörtlich was passiert:
class Something extends Base implements Contract {}
In Kotlin wird stattdessen ein Doppelpunkt verwendet:
class Something : Base(), Contract
Auch wenn diese Syntax eine gewisse Logik hat, weil sie dem Muster entspricht, nach dem auch sonst Typen in Kotlin angegeben werden, etwa bei Parametern wie x: Int
, ist sie im wörtlichen Sinne nicht lesbar.
Annotationen
Eine Annotation in Java anzulegen, gleicht einem Ausflug in eine andere Welt, denn die Syntax dafür weicht deutlich vom Rest der Sprache ab.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequirePermission {
public String name() default "";
}
Mit einer normalen Schnittstelle hat eine Annotation wenig gemein, weshalb schon das Schlüsselwort Interface
nicht sehr einleuchtend ist. Vielmehr dient eine Annotation hauptsächlich dazu, Metainformation in den Code einzufügen.
Der nächste irritierende Punkt ist die Deklaration von Parametern, die der Annotation mitgegeben werden können, denn dafür wird eine Methodensyntax verwendet: public String name()
, während es später bei der Übergabe des Wertes wie ein benannter Parameter aussieht: @RequirePermission(name = "read")
.
Ebenfalls aus dem Rahmen fällt die Angabe des Standardwerts, aber dafür bietet Java auch sonst keine Syntax.
Kotlin ist in dieser Hinsicht viel verständlicher und intuitiver.
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class RequirePermission(val name: String = "")
Variable Parameterlisten
Variable Parameterlisten werden in Kotlin durch das Schlüsselwort vararg
gekennzeichnet:
fun add(vararg values: Int): Int
Die drei Pünktchen (Ellipse genannt) in Java sind naheliegend, aber nicht so explizit und weniger verständlich. Es ist auch nicht unbedingt offensichtlich, ob sie beim Datentyp oder beim Namen des Parameters stehen sollen, auch wenn man mit etwas Nachdenken darauf kommen kann, dass eine Liste mehrerer Parameter einen anderen Typ als ein einzelner haben muss. Der Compiler akzeptiert allerdings beide Varianten.
int add(int… values)
Erweiterungsfunktionen
Ohne Entsprechung in Java sind Kotlins Erweiterungsfunktionen. Damit können existierenden Klassen von außen Funktionen hinzugefügt werden. Zum Beispiel könnte man damit der Klasse Int
eine isEven
-Funktion geben:
fun Int.isEven(): Boolean = this % 2 == 0
Genutzt werden kann diese Funktion so, als wäre sie Teil der Int
-Klasse:
4.isEven() // true
Das ist vor allem dann praktisch, wenn bestehende Klassen wie Int
nicht geändert werden können.
Letztlich ist das syntaktischer Zucker für eine statische Methode und bietet die gleichen Möglichkeiten – Zugriff auf private Felder der Instanz gibt es also nicht.
So würde man das Problem in Java mit einer statischen Methode lösen:
class IntExtension {
public static boolean isEven(int value) {
return value % 2 == 0;
}
}
Und die Verwendung sähe so aus:
import static IntExtension.isEven;
// ...
isEven(4);
Nebenbei bemerkt bräuchte man in Kotlin zum Importieren eines einzeln (statischen) Elements, wie einer Funktion, nicht das Schlüsselwort static
anzugeben.
Ausdrucksstarke Funktionen
Neben Funktionen, die ihr Ergebnis mit return
zurückgeben, bietet Kotlin eine kürzere Variante, falls der Rumpf nur aus einem Ausdruck besteht:
fun square(x: Int): Int = x * x
Diese Schreibweise ist ähnlich kompakt wie die in der Mathematik. Eine Entsprechung in Java gibt es nicht.
Benannte Parameter
Außerordentlich praktisch sind Kotlins benannte Parameter, die optional mit Standardwerten versehen werden können. Im folgenden Beispiel wird ein Drei-Gänge-Menü definiert, das für jeden Gang einen Parameter mit einem vorgegebenen Wert hat:
class Menu(
val starter: String = "salad",
val mainCourse: String = "baked beans",
val dessert: String = "pudding"
)
Durch die Vorbelegung der Parameter brauchen nicht alle angegeben zu werden. Im Beispiel könnte etwa nur der Nachtisch geändert werden:
Menu(dessert = "ice cream")
Beim Lesen ist sofort klar, welcher Wert geändert wird. Ein weiterer Vorteil ist, dass die Reihenfolge der Parameter keine Rolle spielt und dieser Ansatz daher auch bei Änderungen im Code unempfindlich ist.
Am ehesten ähnelt dem in Java ein Builder, für den aber einige Zeilen Code zu schreiben wären. Die benannten Parameter können in Kotlin auch bei Methoden verwendet werden, wo ein Builder dann nur noch über den Umweg eines zusätzlichen Objekts für lesbaren und flexiblen Code sorgen könnte.
Funktionsnamen in Tests
Die Namen von Testfunktionen sollte möglichst genau beschreiben, welcher Fall getestet wird, weshalb diese Methodennamen oft etwas länger sind. Mit der üblichen Camelcase-Schreibweise ist das oft schwer lesbar:
public void shouldRejectMeatIngredientForVegetarianMeal()
In Tests – und nur dort – darf man in Kotlin Leerzeichen in Methodennamen verwenden, wenn er in Backticks eingefasst wird:
fun `should reject meat ingredient for vegetarian meal`()
Typinferenz
Eine der angenehmsten syntaktischen Verbesserungen der letzten Jahre war die Einführung des Schlüsselwortes var
in Java. Dadurch kann lokal, das heißt innerhalb eines Methodenrumpfes oder eines static
-Blocks, auf eine explizite Typangabe verzichtet werden.
var colors = new HashSet<String>();
Früher musste man in Java stattdessen jeder Variablen explizit einen Typ geben:
Set<String> color = new HashSet<>();
In Kotlin gibt es var
seit Anfang an.
var colors = mutableSetOf<String>()
In diesem Punkt ist Java also fast so angenehm zu nutzen wie Kotlin. Aber wieso nur fast? In Kotlin gibt es noch ein zweites Schlüsselwort für unveränderliche Referenzen, nämlich val
.
val color = "green"
Um das Gleiche in Java zu erreichen, muss man final
voranstellen, was die Schönheit dann doch ein Stück weit beeinträchtigt.
final var color = "green";
Dabei wäre es vorteilhaft, den unveränderlichen Fall bequemer zu machen, damit Code besser nachvollziehbar und weniger fehleranfällig wird. Java fördert hier eher den schlechteren Stil.
Rust-Programmierer müssen zur “Strafe” sogar extra mut
(“mutable”) schreiben, wenn sie eine Variable statt einer Konstante haben wollen (let mut x = 5;
) – das ist das andere Ende des Spektrums.
Typprüfungen
Ob ein Objekt von einem bestimmten Typ ist, lässt sich in Java mit dem instanceof
-Operator ermitteln:
if (x instanceof String)
Das ist sehr schön lesbar und verständlich. Kotlin verwendet zu diesem Zweck den is
-Operator:
if (x is String)
Einen klaren Favoriten kann man hier vielleicht nicht ausmachen, aber zumindest in when
-Ausdrücken macht sich das kürzere is
gut:
when(x) {
is String -> "text"
is Int, Double -> "number"
else -> "???"
}
Casting
Typen von Objekten werden in Kotlin mit dem Schlüsselwort as
umgewandelt:
iceCream as VanillaIceCream
Weniger selbsterklärend muss man in Java den Zieltyp in runde Klammern vor das umzuwandelnde Objekt (bzw. die Referenz darauf) schreiben:
(VanillaIceCream) iceCream
Fazit
Kotlin ist eine überaus gut lesbare Sprache und schlägt Java in dieser Disziplin deutlich. Java wird auch kaum aufschließen, denn dazu müsste die Sprache grundlegend geändert werden, was nicht zu der für Java typischen Beständigkeit passen würde.
Aufgrund der guten Lesbarkeit ist Kotlin auch gut als Lehrsprache an Schulen und Universitäten geeignet.