Programmiersprache Kotlin – Nicht nur im Android-Umfeld interessant
Kotlin ist eine relativ neue, statisch typisierte, JVM-Programmiersprache, die von JetBrains [1] entwickelt wird. Die Version 1.0 wurde Anfang 2016 veröffentlicht. Trotzdem ist Kotlin bereits seit längerem verfügbar und wurde auch vor diesem Release produktiv eingesetzt. Vor allem in der Android-Community hat Kotlin einige Anhänger gewinnen können und ist spätestens seit der diesjährigen Google Konferenz „I/O“ in aller Munde, als man erklärte, dass Kotlin nun „offizielle Sprache für Android“ ist. Wir bei der Firma n-design sind ständig gespannt, wenn es um neue Frameworks und Programmiersprachen geht, sodass auch diese Entwicklung mit Interesse verfolgt wird.
Warum ist Kotlin so interessant für Android?
Der Grund ist relativ einfach, denn Kotlin kann zu Java 1.6 Bytecode kompiliert werden. Android unterstützt in den meisten Versionen nicht das neuste JDK 1.8, wodurch Android-Entwickler auf moderne Java-Sprachfeatures wie Lambdas und Streams [2] verzichten müssen. Kotlin jedoch bietet diese Features, und viele weitere, natürlich ebenfalls an, obwohl lediglich Java 6 benötigt wird, um den kompilierten Code auszuführen. JetBrains‘ Idee beim Entwickeln der Sprache war, eine Alternative zu Java zu schaffen, die Probleme behebt, die Produktivität steigert und weiterhin mit Java-Code vollständig interagieren kann. Ich denke, dass jeder Java-Entwickler, der bereits mit JVM-Sprachen wie Groovy und Scala gearbeitet hat, weiß, dass Java oft sehr „geschwätzig“ ist und viel Boilerplate-Code geschrieben werden muss, auch um simple Dinge zu bewerkstelligen. Kotlin setzt hier an und ermöglicht das Schreiben von deutlich kürzerem Sourcecode, wobei Scala erkennbar als Inspiration diente.
Kotlin-Syntax
Ich möchte im Folgenden einige Beispiele zeigen, die demonstrieren sollen, wie Kotlin-Sourcecode sich anfühlt:
Type Inference lokaler Variablen
Die folgenden Zeilen zeigen, wie Variablen in Kotlin deklariert werden:
var myvar1 = 100
var myvar2 = "text"
myvar1
und myvar2
(Int
bzw. String
) werden vom Compiler automatisch bestimmt, sodass sie nicht explizit angegeben werden müssen.TIP: Den beiden Variablen kann beliebig oft ein anderer Wert zugeordnet werden. Möchte man final
Variablen verwenden, so nutzt man val
anstelle von var
. Die einzelnen Zeilen müssen nicht mit ;
terminiert weren.
Data Class
Wenn man sich überlegt, wie man ein POJO Person
in Java entwickeln würde, das Properties wie name
, age
und job
besitzt und natürlich angemessene Implementierungen für toString()
, hashCode()
und equals()
, sowie die notwendigen Getter und Setter beinhaltet, so hat man direkt eine relativ große Datei vor Augen, die zudem einem sehr oft wiederkehrenden Prinzip entspricht.
Pojo in Kotlin
data class Person(val name: String, val age: Int, val job: String)
Null-Sicherheit
Wir alle fürchten NullpointerException
s. Diese treten meistens aufgrund von Programmierfehlern auf, weil nicht klar war, dass bestimmte Werte zur Laufzeit null
sein können.
Kotlin versucht diese Fehlerquelle zu eliminieren, indem zwischen jenen Variablentypen unterschieden wird, die nullable sind und solchen, die es nicht sind. Dies wird über die Syntax verdeutlicht. Wie man sich denken kann, sind Variablentypen die nicht null
referenzieren können der Standard. Veranschaulicht bedeutet das, dass beispielsweise eine Variable vom Typ String
niemals null
enthalten kann. Der entsprechende Typ eines „nullable String“ wird als String?
deklariert.
Dies alleine würde keinen allzu großen Unterschied machen, weshalb man bei der Verwendung von nullable Typen einige Besonderheiten beachten muss:
Beispiel: Arbeiten mit nullable String
val iCanBeNull:String? = null
//...do some hard work with the string...
iCanBeNull?.length
Die letzte Codezeile wird die Länge des String
s ergeben, wenn iCanBeNull
nicht null
referenziert, oder null
im anderen Fall. Das heißt, der Aufruf length
wird nicht auf einem Null-Pointer operieren und somit sind NullpointerException
s unmöglich.
Man muss erwähnen, dass die Sprache durchaus die Möglichkeit offen lässt, solche Aufrufe zu erzwingen und NullpointerException
s in Kauf zu nehmen. Aber sollte man das wirklich tun?!
String-Templates
Ein Feature, das jeder schnell versteht und man sich fragt, warum Java es nicht anbietet:
Verwende Variablen und Expressions direkt in Strings ohne explizite Konkatenation
var insertMe = "Reader"
println("Hello $insertMe") //Prints >>Hello Reader<< on Console
println("Hello ${insertMe.toUpperCase()}") //Prints >>Hello READER<< on Console
Methoden: Default-Wert für Parameter und benannte Argumente
In Java ist es oft notwendig eine Methode mehrfach zu überladen, um verschiedene API-Varianten bereitzustellen, bspw. wenn einer der Parameter nicht zwingend angegeben werden muss. Die Methode existiert dann mit und ohne diesen Parameter. Eine deutlich lesbarere Alternative ist in Kotlin möglich:
Default-Werte
fun myMethod(notOptional: String, optional: String = "defaultValForOptionalString"){
println("myMethod called with $notOptional and $optional")
}
//Possible Calls
myMethod("arg1", "arg2")
myMethod("arg1")
Benannte Argumente
//You choose your own arg sequence
myMethod(notOptional = "arg1", optional = "arg2")
myMethod(optional = "arg2", notOptional = "arg1")
Lambdas
Kotlin besitzt, anders als Java, angemessene Funktionstypen. Seit Java 1.8 ist es, wie wir wissen, möglich mit Lambdas und Streams ansatzweise funktional programmieren zu können. Allerdings muss man hierbei bedenken, dass die Umsetzung dessen ein wenig ungewöhnlich ist, wenn man mit funktionalen Programmiersprachen vertraut ist. Das Problem ist, dass Java keinen Funktionstypen kennt; Funktionen sind kein First Class Citizen innerhalb der Sprache. Stattdessen ist ein Lambda eine Implementierung eines SAM-Typen [3].
In Kotlin wird einer Funktion ein höherer Stellenwert zugeordnet. Es gibt einen Funktionstypen, der beispielweise so aussieht: (Int, Int) → Int
(Eine Methode mit zwei Int
-Parametern, die ein Int
liefert).
anonyme Methode als Lambda
val multiply = { x: Int, y: Int -> x * y } //Typ: (Int, Int) -> Int
val res = multiply(3,5)
Eine Methode kann einfach einer Variablen zugeordnet und bei nächster Gelegenheit genutzt werden. Natürlich können Methoden auch als Parameter übergeben werden. Als syntaktisches Highlight hat man sich hier überlegt, dass es
- nicht notwendig ist runde Klammern anzugeben, falls ein Lambda das einzige Argument der Methode ist.
- nicht notwendig ist, den Methoden-Parameter zu benennen, falls er der einzige Parameter ist. Der implizite Name ist dann „it“.
- nicht notwendig ist, Methoden-Parameter überhaupt zu nutzen. Diese können mit „_“ benannt werden.
Diese Fälle sind nachfolgend dargestellt:
Lambda Syntaktischer Zucker
val ints = 1..4
//a) & b) filter() has only one parameter of type Int -> Boolean
ints.filter { it > 0 }
val map = mapOf(Pair(1,"val1"),Pair(2, "val2"))
//c) Key of Map-Entry is not used!
map.forEach { _, value -> println("$value!") }
Lambda-Beispiel
//Determine Names of all Males in people-collection
val namesOfMaleMembers = people.filter { it.gender == Person.Sex.MALE }
.map { it.name }
Weiteres
Die Beispiele sind nur ein kleiner Ausschnitt der vielen tollen Features, die Kotlin bietet und man sich zu gerne in Java wünschen würde. Ich habe bereits einige Zeit mit Kotlin arbeiten können und bin ein großer Fan und Unterstützer geworden. Der Einstieg in die Sprache kann sehr schnell gelingen, anders als es bei mir persönlich bei Scala der Fall war.
TIP: Einige der oben vorgestellten Sprachkonstrukte sind in der langen Liste der „JDK Enhancement Proposals“ (JPE) ebenfalls in ähnlicher Weise gewünscht, z.B. beschreibt JEP 286: Local-Variable Type Inference, beschrieben von Brian Goetz, die Typinferenz lokaler Variablen. Vielleicht ein Teil von Java 10?
Gibt es bedenkenswerte Features in Kotlin?
Ich denke, diese Frage kann man prinzipiell mit Nein beantworten. Allerdings erlaubt Kotlin gewisse Freiheiten, die mit Bedacht verwendet werden sollten. Es ist beispielsweise so, dass Kotlin es erlaubt, Funktionen und Variablen in Dateien zu definieren, die keiner Klasse zugeordnet sind, wie es in Java immer der Fall wäre. Diese können unqualifiziert an beliebiger Stelle referenziert werden [4]. Ein sinnvoller Anwendungsbereich sind statische Utility-Methoden (z.B. Collections
). Die Frage ist: Wer darf, an welcher Stelle, solche Dinge innerhalb des Projekts definieren? Man sollte sich meiner Meinung nach projektintern absprechen, wie mit solchen Dingen umzugehen ist und bestenfalls eine zentrale Stelle definieren.
Ähnliches gilt für „Extension-Functions“: Dies sind im Prinzip Methoden, die einer Klasse von außen hinzugefügt werden. Beispielsweise könnte man die Klasse String
mit beliebigen Methoden erweitern, obwohl sie eigentlich final
ist und nicht per Vererbung erweiterbar wäre [5]. Genau genommen ist dies der absolut richtige Ansatz, ganz nach dem Open/Closed-Prinzip, welches hierdurch durchgesetzt wird. Für den Aufrufer ist leider nicht direkt ersichtlich, ob eine Member-Funktion oder eine künstliche Erweiterung aufgerufen wird. Allerdings gilt auch hier, dass ein Import notwendig wird, wodurch die Kritik etwas gemildert wird. In meinen Augen ist auch dies trotzdem ein mit Vorsicht zu nutzendes Feature. Ähnlich wie bei Top-Level Funktionen sollte klar festgelegt sein, wo solche Funktionen zu definieren sind.
Ist Kotlin eine ernstzunehmende Alternative zu Java?
Meiner Meinung nach ist Kotlin aktuell die JVM-Sprache, die es schaffen kann, Java ernsthaft Konkurrenz zu bieten. Kotlin ermöglicht das Erstellen von Applikationen für die JVM, Android, den Browser und bald sogar nativer Art. Jüngste Ereignisse im Zusammenhang mit Google und der Bekanntgabe, dass neben Java nun auch Kotlin offiziell zur Programmierung von Android-Apps herangezogen werden kann, hat das Potenzial einen Hype auszulösen und die Sprache damit bekannter zu machen. Bereits viele Unternehmen nutzen Kotlin produktiv, es entsteht eine Community und die erste Konferenz ist für dieses Jahr geplant. Anders als viele andere JVM-Sprachen hat Kotlin den grandiosen Vorteil, dass es zu 100% mit Java interoperabel ist. Konkret bedeutet das, dass die vielen verfügbaren Java-Libraries und Frameworks problemlos genutzt werden können und bestehende Codebasen nicht vollständig migriert werden müssen.
final
, entsprechend „Effective Java, Item 17: Design and document for inheritance or else prohibit it.“