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.