Stand: 2016-04-05

Kotlin – Das bessere Java

Die Programmiersprache Kotlin steigert die Produktivität durch praktische Sprachmerkmale, prinzipielle Verbesserungen und klare Syntax.

Inhalt

Warum Kotlin?

Kotlin steigert die Produktivität indem es Entwicklern Schreibarbeit erspart, den Code verständlicher macht und einige Dinge prinzipiell verbessert. Außerdem erhöht Kotlin die Qualität, weil typische Fehler wie NullPointerExceptions oder ungewollte Vererbung verhindert werden.

Ein paar Vorzüge von Kotlin:

Wie sieht Kotlin aus?

Das folgende Beispiel gibt einen ersten Eindruck, wie Kotlin-Quelltext aussieht:

data class Point(val x: Int, val y: Int) : Shape {
    fun shift(h: Int, v: Int) = Point(x + h, y + v)
}

val a = Point(2, 4)
var b = Point(5, 5)
b = b.shift(1, 2)
println("Position: ${b.x}, ${b.y}")

Erläuterung:

Es dürfen übrigens auch mehrere Klassen mit der Sichtbarkeit public in einer Datei stehen.

Einen schönen Überblick über die Sprache und die Möglichkeit, direkt im Browser Beispiele auszuprobieren, bietet try.kotlinlang.org:

try.kotlinlang.org: Kotlin direkt im Browser ausprobieren

Null-Sicherheit

Der Erfinder des Konstrukts null, Tony Hoare, bezeichnet seine Erfindung heute als “billion-dollar mistake”, weil die Programmierung mit null sehr fehleranfällig ist und hohe Kosten verursacht hat.

Innerhalb von Kotlin-Code kann der Compiler NullPointerExceptions verhindern, denn Typen müssen mit einem Fragezeichen explizit als “nullable” gekennzeichnet werden, wenn sie den Wert null annehmen können sollen:

var a: String = "hello"
a = null // Compiler-Fehler!

var b: String? = "world"
b = null // ok

Wenn der Typ eines Wertes nicht mit ? gekennzeichnet ist, ist ausgeschlossen, dass dieser Wert jemals null wird.

Bevor man auf Methoden oder Felder eines “nullable” Wertes zugreifen kann, muss man zunächst sicherstellen, dass er nicht null ist:

val length = if (b != null) b.length else -1

b.length kann nach der Prüfung direkt aufgerufen werden, weil der Compiler erkennt, dass eine null-Prüfung vorausgegangen ist.

Sichere Aufrufe sind durch ein Fragezeichen hinter einem Ausdruck möglich:

val length: Int? = b?.length

Wenn b nicht null ist, wird der Aufruf length ausgeführt, andernfalls hat der Gesamtausdruck den Wert null, weshalb length hier vom Typ Int? sein muss.

Der obige if/else-Ausdruck kann mit dem “Elvis-Operator” (?:) noch etwas verkürzt werden:

val length: Int = b?.length ?: -1

Wenn b nicht null ist, wird length aufgerufen, ansonsten wird -1 zurückgegeben.

Und wenn es doch mal eine bewährte NullPointerException sein darf, kann !! verwendet werden:

val length: Int = b!!.length

Wenn b == null ist, wird eine NullPointerException geworfen, ansonsten wird der Wert von length zurückgegeben. Das ist das gleiche Verhalten wie bei Java – nur expliziter.

Typableitung

Durch automatisch ermittelte Typen von Variablen oder Methoden bzw. Funktionen wird der Code wesentlich übersichtlicher.

val point = Point(1, 2)

fun add(a: Int, b: Int) = a + b

Wenn es der Klarheit dient, darf man natürlich auch weiterhin den Typ angeben. Die obigen Beispiele könnten auch so geschrieben werden:

val point: Point = Point(1, 2)

fun add(a: Int, b: Int): Int = a + b

Klassen

Konstruktoren und Initialisierer

Der auffälligste Unterschied zu Java-Klassen ist der Primärkonstruktor. Hinter den Klassennamen wird eine Parameterliste geschrieben:

class Point(x: Int, y: Int) {
    ...
}

Die Klassenparameter können zur Initialisierung von Properties oder in einem init-Block verwendet werden.

class Point(x: Int, y: Int) {
    init {
        checkArgument(x > 0)
        checkArgument(y > 0)
        logger.info("Point ($x, $y) created.")
    }
}

Für die Parameter des Primärkonstruktors können auch gleich Properties angelegt und initialisiert werden. Dazu braucht einem Parameter lediglich val (unveränderlich) oder var (veränderlich) vorangestellt zu werden:

class Point(val x: Int, val y: Int)

Weitere Konstruktoren können im Rumpf der Klasse angelegt werden. Dafür verwendet Kotlin das erfreulich selbsterklärende Wort constructor:

class Point(val x: Int, val y: Int) {
    constructor(template: Point) : this(template.x, template.y) {
        logger.info("Point ($x, $y) created.")
    }
}

Instantiiert werden Klassen in Kotlin ohne das Schlüsselwort new:

val p = Point(1, 2)

Nur gewollte Vererbung

Standardmäßig sind alle Klasse in Kotlin final. Das heißt, es kann nicht von ihnen abgeleitet werden, solange sie nicht explizit mit dem Schlüsselwort open deklariert werden:

class A
class B : A() // Compiler-Fehler!

open class C
class D : C() // ok

Diese strikte Vorgabe hilft, Fehler durch falsch angewendete Vererbung zu vermeiden. Klassen sollten explizit für Vererbung entworfen werden; andernfalls sollte Vererbung ausgeschlossen werden.

Datenklassen

Extrem praktisch sind Datenklassen. Sie eignen sich besonders gut für Klassen, die mehr oder weniger Datenstrukturen darstellen. Wenn man der Klassendefinition das Wörtchen data voranstellt, bekommt man Implementierungen für equals, hashCode, toString und eine copy-Methode geschenkt:

data class Point(val x: Int, val y: Int)

Diese eine Zeile entspricht in etwa folgendem Java-Code:

import java.util.Objects;

public final class Point {

  private final int x;
  private final int y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public int getX() {
    return x;
  }

  public int getY() {
    return y;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Point point = (Point) o;
    return x == point.x && y == point.y;
  }

  @Override
  public int hashCode() {
    return Objects.hash(x, y);
  }

  @Override
  public String toString() {
    return "Point(" + x + ", " + y + ")";
  }
}

Eine copy-Methode ist trotz der Länge der Klasse nocht nicht einmal enthalten! In Kotlin ist copy dagegen mit benannten Parametern einfach und praktisch:

val p1 = Point(1, 2)
val p2 = p1.copy(y = 7)    

Singletons und Companion Objects

Anstelle von statischen Elementen, die nicht so richtig zur Objektorientierung passen, gibt es in Kotlin Objektliterale. Elemente, die in Java static sind, sind in Kotlin in einem Objekt angesiedelt. Objekte werden direkt als Code geschrieben und es muss nicht erst eine Klasse definiert und dann instantiiert werden.

Von Objektliteralen gibt es genau diese eine Instanz, weshalb sie auch “Singletons” (Einzelstücke) genannt werden:

object PersonRepository {

    fun add(person: Person) { ... }

    fun findById(id: Long): Person? { ... }
}

Ein Singleton kann auch von einer Klasse erben oder eine Schnittstelle implementieren:

object PersonRepository : Repository<Person> {
    ...
}

“Companion Objects” sind Objekte, die innerhalb einer Klasse definiert werden und die Zugriff auf private Elemente der Klasse haben:

class MyClass {
    companion object {
        fun create(): MyClass = MyClass()
    }
}

Aufgerufen werden die Methoden des Companion Objects mit dem vorangestellten Klassennamen:

val my = MyClass.create()

Von außen sieht das aus, wie der Aufruf einer statischen Methode in Java. Allerdings enthalten Companion Objects keine statischen Elemente, stattdessen handelt es sich um ganz normale Instanzen, die auch Schnittstellen implementieren können.

Generische Typen

Im Gegensatz zu den eingeschränkten Wildcards, die bei generischen Java-Klassen möglich sind, kann in Kotlin Kovarianz und Kontravarianz bei der Deklaration einer Klasse angegeben (declaration-site variance) werden. Dadurch erfährt der Compiler, welche Typparameter nur an Ausgabepositionen und welche nur an Eingabepostionen verwendet werden. Das hat den Vorteil, dass der Compiler nicht vorsichtshalber Aufrufe verbieten muss, die eigentlich in Ordnung wären.

Ob ein Parameter nur an Eingabe- oder Ausgabepositionen steht, wird mit den Varianzannotationen in und out angegeben. Im folgenden Beispiel wird die Verwendung von A auf Ausgabepositionen beschränkt.

interface Producer<out A> {
    fun next(): A
}

Eine weitere kleine Verbesserung gegenüber Java ist, dass keine Raw-Types möglich sind. Ein generischer Typ wie List<T> kann also nicht ohne Typparameter als List deklariert werden. Weiterhin gibt es Typprojektionen und obere Grenzen (upper bounds).

Da Varianz als Thema für diesen Überblick etwas sperrig ist, verweise ich den geneigten Leser auf die Dokumentation zu Generics.

Delegation

Statt mühsam Methoden zu schreiben, die nichts weiter machen, als Werte durchzureichen, kann man in Kotlin die eingebaute Delegation nutzen und dem Compiler die Schreibarbeit überlassen. Das Schlüsselwort by gibt an, durch welches Objekt eine Schnittstelle implementiert werden soll.

In dem folgenden Beispiel gibt es eine Schnittstelle SomeInterface und eine Implementierung Part davon. Die Klasse Something implementiert die Schnittstelle SomeInterface. Als Implementierung dieser Schnittstelle wird das dem Konstruktor übergebene Objekt p verwendet.

interface SomeInterface {
    fun show(a: String): Unit
}

class Part : SomeInterface {
    override fun show(a: String) {
        print(a.toUpperCase())
    }
}

class Something(p: SomeInterface) : SomeInterface by p // Delegation

fun main(args: Array) {
    val part = Part()
    val something = Something(part)
    something.show("hallo")
}

In Java würde man Delegation nach diesem zeilenreichen Muster umsetzen:

public class Something implements SomeInterface {

    private final SomeInterface part;

    public Something(SomeInterface part) {
        this part = part;
    }

    public void show(String a) {
        part.show(a);
    }
}

Kotlin bietet endlich eine saubere und einfache Alternative zur oft ungünstigen (aber bequemen) Vererbung.

Properties

Die in Java verbreiteten Getter und Setter sind ein Irrtum der Geschichte. In Kotlin gibt es an ihrer Stelle sogenannte Properties, die wie Felder angesprochen werden, aber zusätzlich auch Logik enthalten können.

Veränderliche Properties werden mit var deklariert, unveränderliche mit val.

public class Person {
    val name = ...
    var age = ...
}

Interessant wird es, wenn Logik ins Spiel kommt:

val isValid
    get() = this.items.size >= 1

var name: String = "x"
    get() = field
    set(value) {
        field = value.toUpperCase()
    }

Das Schöne dabei ist, dass sich die Schnittstelle einer Klasse nicht ändert, wenn einem Property nachträglich Logik hinzugefügt wird. Das ist auch als Uniform Access Principle bekannt.

Genau wie Methoden können auch Properties abstrakt sein, überschrieben werden oder in Schnittstellen enthalten sein.

abstract class Resource {
    abstract val path: String
}

Delegation kann nicht nur auf Klassenebene sondern auch für einzelne Properties genutzt werden. Ein paar nützliche Delegates liefert die Kotlin-Bibliothek bereits mit. Außerdem kann man beliebige eigene Delegates implementieren.

Besonders praktisch ist die Funktion lazy, die ein Delegate vom Typ Lazy<T> erzeugt. Damit kann die Initialisierung des Properties bis zum ersten tatsächlichen Zugriff verzögert werden.

val entityManagerFactory: EntityManagerFactory by lazy {
  Persistence.createEntityManagerFactory("defaultPersistenceUnit")
}

Ein anderer gängiger Anwendungsfall für delegierte Properties sind Oberservables.

Funktionen

Funktion und Methoden werden mit dem Schlüsselwort fun geschrieben:

fun square(a: Int) = a * a

square(7) // 49

Parameter werden in der Form <name>: <typ> mit Komma getrennt angegeben. Dabei können auch Standardwerte vergeben werden, was so manche Überladung und manchen Builder spart.

fun sum(a: Int = 1, b: Int = 2, c: Int = 3) = a + b + c

sum(b = 8) // 12

Funktionen, die aus einem einzigen Ausdruck bestehen, können mit der oben gezeigten Syntax mit = geschrieben werden. Ansonsten werden geschweifte Klammern und ein explizites return wie bei Java verwendet. Der Rückgabetyp kann optional angegeben werden. Im Gegensatz zu Java können Funktionen auch außerhalb von Objekten und Klassen direkt in eine Datei geschrieben werden.

Eine variable Anzahl von Parametern wird mit dem Schlüsselwort vararg angegeben:

fun sum(vararg numbers: Int): Int = ...

Zur Formulierung domänenspezifischer Sprachen (DSL) ist es möglich, Funktionen mit einem Parameter in Infix-Notation zu schreiben. Das bedeutet, dass der Punkt zwischen Objekt und Methode und die Klammern um den Parameter weggelassen werden. Im Folgenden wird die Erweiterungsfunktion shouldEqual mit dem Schlüsselwort infix definiert:

infix fun Any.shouldEqual(that: Any) {
    assert(this == that)
}

x shouldEqual "a" // entspricht x.shouldEqual("a")

Erweiterungsfunktionen

Sehr praktisch sind Erweiterungsfunktionen, mit denen bestehende Klassen ohne Vererbung ergänzt werden können. Die folgende Funktion erweitert zum Beispiel die Klasse String um die Funktion shorten:

fun String.shorten(length: Int): String {
    if (this.length <= length || this.length < 5)
        return this
    else
        return this.substring(0, length - 3) + " ..."
}

val articleName: String = ...
articleName.shorten(10)

Das Objekt des erweiterten Typs steht innerhalb der Funktion als this bereit.

Lambdaausdrücke

Lambdaausdrücke sind Funktionen, die anderen Funktionen als Parameter übergeben werden. Dadurch lassen sich Abstraktionen ausdrücken, die für kompakten und gut lesbaren Code sorgen.

Um zum Beispiel eine Ressource zu nutzen, die AutoCloseable implementiert, und diese dann wieder zu schließen, könnte man folgende Funktion definieren:

fun <A: AutoCloseable, B> doWithResource(resource: A, action: (resource: A) -> B): B {
    try {
        return action(resource)
    } finally {
        resource.close()
    }
}

Der erste Parameter ist die Ressource, die AutoCloseable implementieren muss. Der zweite Parameter ist die mit dieser Ressource auszuführende Aktion. Genutzt werden kann doWithResource dann so:

doWithResource(StringReader("test")) { reader ->
    println(reader.read())
}

Lambdaausdrücke werden immer in geschweifte Klammern geschrieben. Wenn es nur einen Parameter gibt, und der Name nicht so wichtig ist, kann man einfach it schreiben und die Parameterliste des Lambdaausdrucks weglassen:

someList.filter { it > 3 }

Inline-Funktionen

Normalerweise ist eine Funktion ein Objekt mit ensprechend zusätzlichem Bedarf an Ausführungszeit (für virtuelle Methodenaufrufe) und Speicher. Mit inline markierte Funktionen und ihnen übergebene Lambdaausdrücke werden dort, wo sie aufgerufen werden, in den erzeugten Bytecode eingebettet (Inlining). Dadurch wird zwar die Programmdatei größer, aber für kleine Funktionen und an kritischen Stellen, kann es sich durchaus lohnen.

Durch Inlining sind sogar reified Generics möglich, das heißt, dass die tatsächlichen Typen von Typparametern zur Laufzeit verfügbar sind. In Java bekommt man durch die Typlöschung immer nur Object.

Die folgende Methode gibt den Namen des für T eingesetzten Typs aus:

inline fun <reified T> className() = T::class.qualifiedName

println(className<String>()) // kotlin.String

Operatorüberladung

Operatoren wie +, -, [i] können in Kotlin überladen werden. Dazu wird eine Klassenmethode oder Erweiterungsmethode mit einem der festgelegten Operatornamen benannt und mit dem Schlüsselwort operator versehen:

data class Complex(val r: Double, val i: Double) {
    operator fun plus(other: Complex) = Complex(r + other.r, i + other.i)
}

val a = Complex(1.2, 1.1)
val b = Complex(3.5, 1.3)
val s = a + b  // + auf selbst definierter Klasse

Erfreulicherweise sind die Operatoren, die auf diese Weise selbst definiert werden können, begrenzt, sodass keine Syntaxauswüchse wie bei manchen Scala-Bibliotheken (:~>, %+) zu befürchten sind.

Strings

Mehrzeilige Zeichenketten können in Kotlin direkt geschrieben werden:

val multiLine = """
    Es war einmal eine kleine süße Dirne, die hatte jedermann lieb,
    der sie nur ansah, am allerliebsten aber ihre Großmutter, die
    wußte gar nicht, was sie alles dem Kinde geben sollte. Einmal
    schenkte sie ihm ein Käppchen von rotem Sammet ...
    """

Dabei bleiben alle Zeichen, auch die Einrückung am Zeilenanfang, erhalten. Wenn das nicht gewünscht ist, kann die Methode trimMargin verwendet werden:

val s = """
    |Es war einmal eine kleine süße Dirne, die hatte jedermann lieb,
    |der sie nur ansah, am allerliebsten aber ihre Großmutter, die
    |wußte gar nicht, was sie alles dem Kinde geben sollte. Einmal
    |schenkte sie ihm ein Käppchen von rotem Sammet ...
    """.trimMargin()

Standardmäßig wird | zur Kennzeichnung des Zeilenanfangs verwendet, aber trimMargin kann ein beliebiges anderes Zeichen als Parameter übergeben werden.

Strings können außerdem Ausdrücke enthalten. Einfachen Ausdrücken wird $ vorangestellt, zusammengesetzte Ausdrücke werden nach dem Muster ${AUSDRUCK} geschrieben:

val w = "Frühling"
println("Wort: $w, Länge: ${w.length}")

Einheitliches Typsystem

Referenztypen und primitive Datentypen wie Integer oder Boolean haben in Kotlin als gemeinsame Wurzel die Klasse Any. Diese Vereinheitlichung erspart Sonderfälle wie IntFunction oder LongFunction im JDK. Man kann primitive Datentypen ohne Umwege als Typparameter für generische Typen verwenden. Außerdem kann man auch auf primitiven Datentypen Methoden wie 1.rangeTo(10) aufrufen oder eigene Erweiterungsmethoden schreiben. Im Hintergrund verwendet der Compiler wann immer möglich die primitiven Typen der JVM, um die bestmögliche Performance zu erzielen.

Eine andere Form der Vereinheitlichung ist der Rückgabetyp Unit für Methoden, die eigentlich nichts zurückgeben. Das entspricht in etwa Javas void. Allerdings ist Unit ein ganz normaler Typ, weshalb auch dafür keine Sonderfälle im Code nötig sind. In Java gibt es beispielsweise die Schnittstellen Function<T, R> und Consumer<T>. Letzeres ist aber eigentlich auch nur eine Funktion, nur eben eine ohne Rückgabewert. Dank Unit können in Kotlin beide Fälle mit Function<T, R> ausgedrückt werden.

Alles hat einen Wert

Kontrollstrukturen wie if sind in Kotlin Ausdrücke und haben einen Wert.

val color = if (stock > 0) GREEN else RED

Dass alles ein Ausdruck ist, hat mehrere Vorteile:

When-Ausdrücke

Der Ausdruck when ähnelt Javas switch-Anweisung, bietet allerdings mehr Möglichkeiten.

when (x) {
    0 -> println("Null")                // Vergleich mit Wert
    1, 2 -> println("1 oder 2")         // Vergleich mit mehreren Werten
    in 3..10 -> println("3 bis 10")     // Prüfen, ob in Intervall
    in colors -> println("eine Farbe")  // Prüfen, ob in Collection
    is Double -> println("Kommazahl")   // Typprüfung
    else -> println("etwas anderes")    // Alternative, wenn alles nicht passt
}

Genau wie if ist auch when ein Ausdruck, sodass der Wert direkt einer Variablen oder Konstanten zugewiesen werden kann:

val color = when (stock) {
    0 -> RED
    in 1..9 -> YELLOW
    else -> GREEEN
}

Im Zusammenspiel mit Sealed Classes kann der Compiler prüfen, ob alle möglichen Fälle abgedeckt werden, da die Klassenhierarchie fest steht:

import MeasurementUnit.*

sealed class MeasurementUnit(val value: Double) {
    class Meter(value: Double) : MeasurementUnit(value)
    class Kilogram(value: Double) : MeasurementUnit(value)
    class Watt(value: Double) : MeasurementUnit(value)
}

val value = when (m) {
    is Meter -> m.value
    is Kilogram -> m.value
    // Compiler-Fehler, da 'Watt' oder 'else' fehlen!
}

Nicht zuletzt entfällt bei Kotlin das lästige und fehleranfällige break (das doch schon öfter mal vergessen wurde).

Typkonvertierung

Im letzten when-Beispiel wurden bereits sogenannte “Smart Casts”, automatische Typkonvertierungen, verwendet; da mit is eine Typprüfung durchgeführt wird, kann der Compiler garantieren, dass der geprüfte Wert den fraglichen Typ hat und konvertiert ihn automatisch in diesen Typ. Deshalb kann im obigen Beispiel mit m.value auf eine Eigenschaft von MeasurementUnit zugegriffen werden.

Smart Casts funktionieren mit if analog:

if (x is String) {
    println(x.toUpperCase())  // x ist hier automatisch vom Typ String
}

Explizite Typkonvertierungen sind mit as möglich:

val a: String = x as String
val b: String? = y as String? // falls der Wert null sein kann

Wenn eine solche Umwandlung nicht möglich ist, wird eine Exception geworfen.

Exceptions

Der wichtigste Unterschied zu Java ist, dass es keine Checked Exceptions gibt. Das ist ein wahrer Segen, denn Checked Exceptions werden fast nie sinnvoll eingesetzt und ziehen sich dann durch alle Abstraktionsebenen, die dadurch von einem bestimmten Exception-Typ, also einem Implementierungsdetail, abhängig werden.

Außerdem passen Checked Exceptions denkbar schlecht zu funktionalen Konstrukten, da in einer Kette von Funktionsaufrufen kaum eine Ausnahmebehandlung untergebracht werden kann, ohne die Lesbarkeit zu ruinieren.

Collections

Die Standardbibliothek bringt veränderliche und unveränderliche Collections mit. Dadurch können APIs präziser gestaltet und Fehler vermieden werden.

val names: MutableList<String> = mutableListOf("Pia", "Paul", "Peter")
val numbers: List<Int> = listOf(1, 2, 3)

listOf und mutableListOf sind Funktionen der Standardbibliothek zur komfortablen Erzeugung von Listen. Ähnliche Methoden gibt es auch für andere Collections wie Sets.

Auf die Elemente von Maps wird nach dem Muster instanz[schlüssel] zugegriffen:

val s = hashMapOf(1 to "eins", 2 to "zwei", 3 to "drei")
s[2] = "two"    // Wert setzen
val name = s[2] // Wert lesen

Die hier verwendete to-Methode sollte man übrigens nur an Stellen verwenden, die nicht kritisch für die Performance sind.

Destructuring Declarations

Ein Objekt kann in einzelne Eigenschaften zerlegt werden, wenn es Operatormethoden mit den Namen component1() bis componentN() implementiert. Das ist standardmäßig bei Datenklassen der Fall.

Praktisch ist diese Zerlegung zum Beispiel, wenn man Schlüssel und Werte einer Map verarbeiten will:

for ((key, value) in map) {
    println("key: $key, value: $value")
}

Gleichheit

Strukturelle Gleichheit wird in Kotlin mit dem Operator == und Ungleichheit mit != geprüft.

a == b

Das entspricht a.equals(b) in Java, ist aber besser lesbar.

Die Gleichheit von Referenzen wird mit === und Ungleichheit mit !== geprüft. Wenn a und b im folgenden Beispiel auf dasselbe Objekt zeigen, ist der Ausdruck wahr:

a === b

Import

Die Import-Anweisung sieht in Kotlin im Prinzip so wie in Java aus. Mit ihr können auch direkt Funktionen, Properties von Objekten und Enum-Konstanten importiert werden; eine spezielle Anweisung wie import static in Java ist nicht nötig.

import some.package
import some.package.AnObject.myFunc

Namenskonflikte können durch Umbenennung mit as vermieden werden:

import some.package.Something as Thing

Weniger Spezialfälle

Die Sprachschöpfer haben sich bemüht, den Kern der Sprache klein zu halten und Spezialfälle zu vermeiden. Das zeigt sich an einigen Stellen:

Lesbarkeit

Kotlin ist überaus gut lesbar. Viele Schlüsselwörter sind selbsterklärend:

Syntax ohne Ballast

Förderlich für die Lesbarkeit und Verständlichkeit ist außerdem die von Ballast befreite Syntax. Durch folgende Merkmale wird der Quelltext auf das Wesentliche beschränkt:

Werkzeugunterstützung

Ein wichtiges Ziel bei der Entwicklung von Kotlin war und ist, eine Sprache zu schaffen, die gut von Entwicklungswerkzeugen unterstützt werden kann. Parallel zur Sprache hat JetBrains die Kotlin-Unterstützung für IntelliJ entwickelt. Das Kotlin-Plugin fühlt sich schon ziemlich ausgereift an. Inspections gibt es für Kotlin noch nicht so viele, aber sonst gibt es kaum etwas zu vermissen. Den Umstieg von Java auf Kotlin erleichtert die automatische Java-zu-Kotlin-Konvertierung.

Kotlin-Unterstützung in IntelliJ IDEA

Es gibt auch ein Eclipse-Plugin für Kotlin, das allerdings langsamer als das Pendant für IntelliJ entwickelt wird.

Für die Entwicklung von Kotlin-Programmen können die bekannten und bewährten Build Tools Maven, Gradle oder Ant verwendet werden.

Fazit

Entwickler mit guten Java-Kenntnissen werden Kotlin sehr schnell lernen. Schon nach ein paar Tagen dürfte man mit Kotlin produktiver als mit Java sein. Kotlin ist Java konzeptionell sehr ähnlich und man kann einfach weiter mit den gewohnten Frameworks arbeiten. Durch Kotlin profitiert man durch syntaktische Vereinfachungen und konzeptionelle Verbesserungen wie der Null-Sicherheit oder dem einheitlichen Typsystem.

Angesichts des geringen Lernaufwands und der Möglichkeit weiterhin mit bewährten Java-Technologien zu arbeiten, ist das Risiko der Kotlin-Einführung gering. Der Übergang von Java zu Kotlin kann fließend erfolgen, denn Java- und Kotlin-Dateien können problemlos nebeneinander in einem Projekt existieren. Kotlin ist ein Drop-in Replacement für Java.

Kotlin ist so, wie man sich Java wünschen würde.

Weitere Informationen