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 NullPointerException
s oder ungewollte Vererbung verhindert werden.
Ein paar Vorzüge von Kotlin:
- Vermeidung von Fehlern durch
null
- Weniger Redundanz durch Typableitung
- Kompakterer Code durch Datenklassen
- Komfortable Delegation spart Schreibarbeit und hilft, ungünstige Klassenhierarchien zu vermeiden.
- Benannte Parameter mit Standardwerten sparen Überladungen oder Builder.
- Einheitliches Typsystem vereinfacht die generische Programmierung.
- Kovarianz und Kontravarianz werden direkt unterstützt.
- Erweiterungsfunktionen
- Strings können direkt Variablen enthalten und mehrzeilig sein.
- Keine checked Exceptions
- Alles ist ein Ausdruck.
- Volle Java-Kompatibilität: Java Frameworks können ohne Verrenkungen genutzt werden und in Java-Projekten kann schrittweise Kotlin eingeführt werden.
- Geringer Lernaufwand für Java Entwickler
- Schneller Compiler (im Vergleich zu Scala)
- Gute IDE-Unterstützung
- Schlanke Standardbibliothek (interessant für Android-Apps)
- Kompatibel mit Java 6. Damit kann Kotlin auch für Android oder Altprojekte genutzt werden.
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:
- Die Klasse im Beispiel ist implizit
public
. - Durch das Schlüsselwort
data
werden die Methodenequals
,hashcode
,toString
undcopy
automatisch implementiert. - Statt
extends
oderimplements
wird nur ein Doppelpunkt geschrieben. - Jede Klasse hat einen Primärkonstruktor (
(val x: Int, val y: Int)
), der hinter dem Klassennamen geschrieben wird. val
vor den Konstruktorparametern sorgt dafür, dass der Compiler automatisch unveränderliche Properties anlegt.- Der Typ wird hinter dem Parameternamen angegeben:
x: Int
- Methoden mit einem Ausdruck kann man ohne geschweifte Klammern schreiben:
fun shift(h: Int, v: Int) = Point(x + h, y + v)
. - Der Rückgabetyp (
Point
) der Methode wird vom Compiler automatisch abgeleitet; er kann aber auch explizit angegeben werden. var
kennzeichnet veränderliche undval
unveränderliche Referenzen.- Neue Objekte werden ohne
new
erzeugt:Point(2, 4)
- Strings können direkt Variablen enthalten:
"Position: ${b.x}, ${b.y}"
- Statt Gettern und Settern (
getX
,setX
) werden in Kotlin Properties verwendet:x
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:

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())
}
}
Den Code, um Aufrufe von Methoden der Schnittstelle SomeInterface
an die dem Konstruktor übergebebene Instanz p
zu delegieren, beschränkt sich auf:
class Something(p: SomeInterface) : SomeInterface by p
In Java würde man Delegation dagegen 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:
- Konstanten können direkt mit komplexen Ausdrücken initialisiert werden. Veränderliche Hilfsvariablen werden seltener benötigt.
- Der unaussprechliche Operator
?:
ist überflüssig. - Funktionen mit einem Ausdruck können besonders kompakt geschrieben werden.
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:
- Einheitliches Typsystem statt Ausnahmen für primitive Datentypen
- Annotationen sind normale Klassen statt
@Interface
- Sprachkonstrukte aus Java werden durch Funktionen ersetzt:
- try-with-resource
- Locking
- assert
- Import einzelner Elemente ohne
import static
Unit
als normaler Typ stattvoid
if
/else
als Ausdruck statt?:
Lesbarkeit
Kotlin ist überaus gut lesbar. Viele Schlüsselwörter sind selbsterklärend:
- Konstruktoren heißen einfach
constructor
, - Initialisierungsblöcke
init
, - Companion Objects heißen
companion object
, - eine variable Anzahl von Parametern
vararg
, - Ein- und Ausgabeparameter bei generischen Typen werden mit
in
undout
gekennzeichnet, - Typkonvertierung mit
as
- Typprüfungen mit
is
- Intervalle als Literale:
1..9
- Vergleiche mit
==
stattequals
for
-Schleife mitin
:for (element in collection)
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:
- Typableitung (
val
,var
) - Instantiierung ohne
new
- Lamdaausdrücke mit einem Parameter ohne Deklaration der Parameter (
it
) - kein Semikolon am Zeilenende nötig
- Primärkonstruktor statt extra Konstruktor
- Methoden mit Standardwerten statt Überladungen
- Datenklassen
- weniger Hilfsvariablen, da alles ein Ausdruck ist
- Strings mit eingebetteten Variablen
- automatische Delegation
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.

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.