Aus Linux-Magazin 11/2017

Plattformübergreifend entwickeln mit Kotlin

© satina, 123RF

Die recht junge Programmiersprache Kotlin gewinnt dank angesagter Programmiertechniken, kompaktem Quellcode und einer intelligenten statischen Typableitung zunehmend Liebhaber. Als Java-Alternative gestartet empfiehlt sie sich nun auch für Android- und Webentwickler.

Eigentlich ist die tschechische Firma Jetbrains vor allem für ihre Java-Entwicklungsumgebung Intellij bekannt. Deren Entwickler störten sich jedoch im Laufe der Zeit zunehmend an den Eigenheiten von Java, also schufen sie mit Kotlin [1] kurzerhand ihre eigene Sprache, die sich allerdings an Java orientiert. Obwohl die erste stabile Version 1.0 erst Anfang 2016 erschien, kann Kotlin inzwischen bereits einige prominente Nutzer vorweisen, darunter Unternehmen wie Pinterest, Gradle, Evernote und Uber.

Einen ordentlichen Schub erhielt sie, als Google Kotlin Mitte Mai zu einer so genannten “First-Class”-Sprache für Android erhob [2]. Der Name Kotlin leitet sich übrigens von einer Insel im finnischen Meerbusen ab [3].

Platte Formen

Kotlin ist eine statisch typisierte, objektorientierte Programmiersprache. Sie kommt auch mit bestehenden Java-Bibliotheken und -Frameworks zurecht, da sie zu Java kompatibel ist. Daneben eignet sich Kotlin explizit zum Schreiben von Server-seitigen Anwendungen. Das Spring-Framework [4] unterstützt Kotlin sogar mit speziellen APIs, Jetbrains werkelt mit Ktor an einem eigenen Webframework [5].

Abbildung 1: Die Testumgebung – auf der Kotlin-Homepage zu finden – hält auf der linken Seite zahlreiche Beispielprogramme bereit.

Abbildung 1: Die Testumgebung – auf der Kotlin-Homepage zu finden – hält auf der linken Seite zahlreiche Beispielprogramme bereit.

Android und die Java Virtual Machine sind aber nicht die einzigen Zielplattformen. So existiert mit Kotlin Native [6] ein Backend für den LLVM-Compiler, der Kotlin-Quellcode in native Programme für verschiedene Betriebssysteme umwandelt. Alternativ konvertiert der Kotlin-Compiler den Quellcode in Javascript. Über Funktionen aus der Standardbibliothek beeinflussen Webentwickler in Kotlin bequem die DOM-Objekte einer Seite. Nicht zuletzt lassen sich bestehende Javascript-Bibliotheken wie Jquery oder React-JS einbinden.

Abbildung 2: Unter Unix-ähnlichen Betriebssystemen installieren Entwickler den Kotlin-Compiler bequem über das Tool Sdkman. Es erzeugt standardmäßig Bytecode für die JVM oder – wie hier – ein Jar-Archiv.

Abbildung 2: Unter Unix-ähnlichen Betriebssystemen installieren Entwickler den Kotlin-Compiler bequem über das Tool Sdkman. Es erzeugt standardmäßig Bytecode für die JVM oder – wie hier – ein Jar-Archiv.

Android Studio unterstützt Kotlin ab Version 3.0, Intellij Idea seit Version 15. Der Konkurrent Eclipse lernt die Syntax von Kotlin über ein Plugin. Kleine Codeschnipsel testen Entwickler auch einfach auf der Kotlin-Homepage (Abbildung 1, [7]), Puristen übersetzen ihren Quellcode mit dem Kotlin-Compiler auf der Kommandozeile (Abbildung 2, [8]).

Hallo Welt!

In jedem Kotlin-Programm bildet die Funktion »main()« den Einstiegspunkt, ihre Kollegin »println()« gibt den ihr übergebenen Text aus. Das obligatorische Hallo-Welt-Beispiel sieht damit wie in Listing 1 aus.

Listing 1

Hallo Welt!

01 // Dies ist ein Kommentar
02 fun main(args: Array<String>) {
03         println("Hallo Welt!")
04 }

Entwickler schließen die einzelnen Anweisungen wahlweise mit einem Semikolon ab, müssen dies aber nicht tun. Kotlin stellt eine Standardbibliothek mit vielen nützlichen Funktionen bereit, von denen es einige automatisch importiert. Dazu gehört etwa auch »println()«. Weitere Funktionen holt Kotlin, ähnlich wie Java, per »import« hinzu.

Funktional

Eine neue Funktion definieren Entwickler mit »fun«, gefolgt vom Namen der Funktion, den Argumenten in Klammern und nach einem Doppelpunkt dem Typ des Rückgabewerts (Listing 2). Die Funktion »ggt()« erwartet zwei Argumente, die dann im Rumpf in den Variablen »x« und »y« bereitstehen. Der Typ einer Variablen steht ähnlich wie in Pascal hinter dem Doppelpunkt. In der Funktion »ggt()« nehmen »x« und »y« 32-Bit-Ganzzahlen auf (»Int«).

Listing 2

Funktionen

01 // Berechnet den größten gemeinsamen Teiler
02 fun ggt(x: Int = 0, y: Int = 0): Int {
03         var a: Int = x
04         var b: Int = y
05
06         while (b != 0) {
07                 if (a > b) {
08                         a = a - b
09                 } else {
10                         b = b - a;
11                 }
12         }
13         println("Der ggT von $x und $y ist $a.")
14         return a;
15 }

Optional geben Entwickler Standardwerte vor, im Beispiel erhalten »x« und »y« den Wert »0«. Der Funktionsaufruf darf zudem festlegen, welcher Parameter welchen Wert erhält: »ggt(b = 3)«. Auch primitive Datentypen wie »Int« betrachtet Kotlin als vollwertige Objekte.

Als Resultat gibt »ggt()« eine Ganzzahl zurück. Ohne Angabe des Rückgabetyps liefert die Funktion automatisch den Wert »Unit« vom Typ »Unit« zurück. Besteht der Funktionsrumpf aus einem einzigen Ausdruck, gibt es eine Kurzschreibweise: »fun doppel(a: Int) = a + a«.

Variablen definiert der Entwickler mit »var«, Konstanten mit »val«. In Kotlin gilt die Konvention, möglichst immer alle Variablen als »val« zu definieren. Das betrifft auch die übergebenen Parameter, die der Rumpf von »ggt()« daher für die Berechnung in die Variablen »a« und »b« kopiert. Gibt der Programmierer die Typen nicht an, versucht der Compiler sie beim Übersetzen automatisch abzuleiten. Variableninhalte setzt er zudem in Zeichenketten ein, indem er den Variablennamen im Text ein »$« voranstellt.

Unter Kontrolle

Ergänzend zu »if« kennt Kotlin »when«, das dem Switch-Konstrukt aus anderen Sprachen ähnelt. Wie im Beispiel aus Listing 3 vergleicht es den Inhalt oder den Typ einer Variablen nacheinander mit Vorgaben.

Listing 3

when-Konstrukt

01 fun wasist(obj): String = when (obj) {
02         1       -> "Eins"
03         "Hallo" -> "Ein String mit dem Inhalt Hallo"
04         is Int  -> "Eine Zahl"
05         else    -> "unbekannt"
06 }

Der Operator »..« erzeugt hingegen eine Zahlenfolge, die der Code zum Beispiel in einer »for«-Schleife durchläuft. Das Beispiel in Listing 4 addiert alle Zahlen von 1 bis 100.

Listing 4

..-Operator

01 var x: Int = 0
02 for (i in 1..100) {
03         x = x + i
04 }

Klassisch

In Kotlin besitzen Klassen einen primären und bei Bedarf noch weitere sekundäre Konstruktoren. Die Parameter des primären Konstruktors notieren Entwickler hinter dem Schlüsselwort »constructor« (Listing 5).

Listing 5

Klassen

01 class Auto constructor(t: Int) {
02         // Properties:
03         val tueren: Int = t
04         var farbe: String = "Rot"
05                 get() = field.toUpperCase()
06                 set(value) {
07                         field = value.toLowerCase()
08                 }
09
10         // Primärer Konstruktor:
11         init {
12                 println("Anzahl Türen: ${tueren}.")
13         }
14         // Sekundärer Konstruktor:
15         constructor(t: Int, f: String) : this(t) { farbe = f }
16 }
17
18 fun main(args: Array<String>) {
19         val audi = Auto(5) // Objekt audi erstellen
20         audi.farbe = "Blau"
21         println(audi.farbe)
22 }

Möchte der Entwickler dem Konstruktor keine so genannten Annotationen anheften, darf er den Ausdruck »constructor« auch ganz weglassen und in diesem Fall nur »class Auto(t: Int)« schreiben. Der eigentliche Code des primären Konstruktors wandert in einen mit »init« gekennzeichneten Block.

Funktionen mit dem Namen »constructor()« bilden schließlich die weiteren, sekundären Konstruktoren. Diese müssen in Kotlin zwingend den primären Konstruktor aufrufen. Das geschieht entweder auf dem indirekten Weg, beispielsweise über den Aufruf eines sekundären Konstruktors in der Oberklasse, oder – wie es das in Listing 5 gezeigte Beispiel demonstriert – über »this«. Dieser Ausdruck steht dabei immer für das Objekt selbst. Ein Objekt erstellen Entwickler in Kotlin, ohne das in anderen Programmiersprachen übliche Schlüsselwort »new« verwenden zu müssen.

Properties

Daten speichert ein Objekt in Variablen, die Kotlin als Properties bezeichnet. Die Klasse »Auto« besitzt die zwei Properties »tueren« und »farbe«. Sofern der Konstruktor lediglich die Variablen mit Werten füllt, erlaubt Kotlin die Kurzschreibweise »class Auto(val tueren: Int) { … }«. Das Property »tueren« erhält dabei automatisch den Wert, den der Entwickler dem Objekt bei seiner Instanzierung mit auf den Weg gibt. Speichert eine Klasse nur Daten, lässt sie sich folglich in nur einer Zeile deklarieren:

class Person(val name: String, val ort: String)

Stellt der Programmierer zusätzlich noch ein »data« voran, ergänzt der Compiler automatisch ein paar nützliche Methoden wie »copy()« oder »equal()«.

In der Klasse »Auto« demonstriert das Property »farbe« den Einsatz von Getter- und Setter-Methoden: Weist der Entwickler dem Property später einen Wert zu (etwa via »audi.farbe = “Blau”«), ruft Kotlin automatisch die »set()«-Funktion auf. Analog führt Kotlin »get()« beim Zugriff auf die Variable aus.

Auf diese Weise lässt sich die Farbe noch vor dem Speichern beziehungsweise vor der Rückgabe ändern. Dabei gibt es aber eine Stolperfalle: Würde »set()« direkt dem Property »farbe« einen Wert zuweisen (via »farbe = value.toLowerCase()«), würde die Funktion immer wieder rekursiv aufgerufen. Das Schlüsselwort »field« verhindert dies. Es speichert den Wert in einem so genannten Backing Field.

Ein Objekt kann Kotlin dabei in mehrere Variablen zerlegen, was Entwickler als Destructuring Declarations bezeichnen:

val (plz, ortsname) = berlin

Im hier gezeigten Beispiel entstehen die Variablen »plz« und »ortsname«, welche die Werte aus den Eigenschaften des Objekts »berlin« enthalten.

Generics

Bei Listen und ähnlichen Containerklassen steht bei ihrer Erzeugung noch nicht fest, welche Objekte sie speichern sollen. In diesen Fällen darf der Entwickler für den Typ einen Platzhalter hinterlegen, wie es Listing 6 für das »T« macht. Die letzte Zeile erstellt ein Cache-Objekt, das Zahlen speichert. Ist der Typ eindeutig, lässt sich die Kurzschreibweise »val zahlencache = Cache(35)« verwenden. Genau wie Java bezeichnet Kotlin dieses Konzept als Generics, unterstützt aber nicht die dort möglichen Wildcards.

Listing 6

Generics

01 class Cache<T>(wert: T) {
02         var inhalt = wert
03 }
04 val zahlencache: Cache<Int> = Cache<Int>(35)

Erbfolge

Eine Klasse kann nur dann von einer Kollegin erben, wenn der Entwickler letztere mit »open« kennzeichnet. Auch hier folgen die Kotlin-Macher den Designvorschlägen des Buches “Effective Java” [9]:

open class Punkt(var x: Int, var y: Int)
class Quadrat(x: Int, y: Int, var w: Int) : Punkt (x,y)

Analog lässt sich eine Funktion nur überschreiben, wenn ihr das Schlüsselwort »open« vorangeht und der Programmierer die überschreibende Funktion explizit mit »override« kennzeichnet. Durch das Schlüsselwort »final« lässt sich eine Funktion in weiteren Unterklassen nicht mehr überschreiben (Listing 7).

Listing 7

Funktionen überschreiben

01 open class Punkt {
02         open fun zeichne() {}
03 }
04 class Kreis() : Punkt() {
05         final override fun zeichne() {}
06 }

Angeflanscht

Kotlin erlaubt auch eine Mehrfachvererbung, im Gegenzug fehlen statische Methoden. Eine Klasse lässt sich aber jederzeit um weitere Eigenschaften und Funktionen ergänzen (Extensions). Im Listing 8 erhält »Quadrat« nachträglich eine Methode »verschieben()« und das Property »farbe«.

Listing 8

Extensions

01 fun Quadrat.verschieben(a: Int, b: Int) {
02         this.x=this.x+a
03         this.y=this.y+b
04 }
05 val Quadrat.farbe: String
06         get() = this.toUpperCase()
07
08 val q = Quadrat(1,2,3)
09 q.verschieben(4,5)

Schnittstellen

Als Ausgangspunkt in der Vererbungshierarchie gibt eine Klasse häufig nur einen Satz Funktionen vor, den dann alle abgeleiteten Klassen anbieten. Kotlin unterstützt dies mit Interfaces und abstrakten Klassen. Die Methoden der Letzteren enthalten keine Implementierung. Abstrakte Klassen kennzeichnet ein vorangestelltes »abstract«. Interfaces sind normale Klassen, die keinen Zustand speichern. Sie enthalten nur dann Eigenschaften, wenn der Entwickler ihnen keinen Wert zuweist oder sie eine Getter-Funktion besitzen (Listing 9).

Listing 9

Interfaces

01 interface Figur {
02         val groesse: Int
03         val farbe: String
04                 get() = "Rot"
05         fun eigenschaften() {
06                 println(farbe)
07         }
08 }
09
10 class Rechteck : Figur {
11         override val groesse: Int = 3
12 }

Bisweilen soll eine Klasse die eigentliche Arbeit an ein bestimmtes Objekt abtreten. Kotlin unterstützt diese Delegation, sofern Klasse und Objekt das gleiche Interface implementieren (Listing 10). Durch das Schlüsselwort »by« fügt der Compiler der Klasse »MeineFigur« für jede Funktion aus dem Interface »Figur« eine Funktion hinzu, die ihre gleichnamigen Kolleginnen im Objekt »r« aufrufen.

Listing 10

Delegation

01 class MeineFigur(r: Figur) : Figur by r
02 val r = Rechteck()
03 MeineFigur(r).eigenschaften()

Alleine in der Fabrik

Mit Hilfe von »object« erzeugt das Programm Objekte direkt und setzt so elegant das Entwurfsmuster Singleton um (Listing 11).

Listing 11

Singleton

01 object BlaueMauritius {
02         var farbe: String = "Blau"
03         fun anmalen(f: String) { farbe = f }
04 }
05 BlaueMauritius.anmalen("Gelb")

Bringt der Entwickler eine Objektdeklaration in einer Klasse unter und stellt ihr »companion« voran, lassen sich die Funktionen und Eigenschaften des Objekts von außen nutzen. Damit ist das Factory-Entwurfsmuster schnell implementiert (Listing 12).

Listing 12

Factory

01 class Auto {
02         companion object Factory {
03                 fun neu(): Auto = Auto()
04         }
05 }
06 val bmw = Auto.neu()

Namenlose Übergabe

In Kotlin können Funktionen andere Funktionen als Parameter entgegennehmen und wieder Funktionen zurückliefern (Higher-Order Function):

fun berechne(foo: (Int) -> Int): Int {
        return foo()
}

Die Funktion »berechne()« erwartet im Beispiel eine Funktion, die eine Zahl entgegennimmt und ein Objekt vom Typ »Int« zurückliefert. In Kotlin lässt sich die zu übergebende Funktion zudem in Form des so genannten Lambda-Ausdrucks direkt in den Aufruf schreiben:

sortiere(zahlen, {x: Int, y: Int -> x < y})

Alternativ darf der Entwickler auch eine Kurzschreibweise verwenden:

sortiere(zahlen) {x,y -> x < y}

Das funktioniert allerdings nur dann, wenn der letzte Parameter eine Funktion ist und der Entwickler einen Lambda-Ausdruck übergibt.

Nicht null

In Java und vielen anderen Sprachen lassen sich Variablen auf den Wert »null« setzen. Beim (versehentlichen) Zugriff wirft der Code dann eine Null-Pointer-Exception (kurz NPE). Um solche NPEs zu reduzieren, dürfen in Kotlin Variablen standardmäßig kein »null« enthalten. Mit dem Fragezeichen lässt sich der Compiler jedoch umstimmen:

var x: String? = "Hallo"

Der Aufruf von »x.length()« würde jetzt zu einer NPE führen. Das kann der so genannte Safe Call Operator »?« verhindern:

x?.length()

Wenn »x« einen String enthält, liefert dieser Aufruf seine Länge, andernfalls »null«. Ergänzend gibt es noch den Elvis-Operator »?:«. Das folgende Beispiel liefert den Wert »-1«, wenn »x« den Wert »null« enthält:

val z = x?.length ?: -1

Danach steht »x!!« immer für einen String, der aber unter Umständen leer ist.

Ganz nebenbei

Neu in Kotlin 1.1 sind so genannte Coroutinen. Ähnlich wie Threads führen sie Berechnungen asynchron aus und pausieren diese dann. Anders als Threads sollen selbst mehrere Tausend nebenläufige Coroutinen keine nennenswerte Performance kosten. Zum Verwalten der Coroutinen stellt die Standardbibliothek passende Funktionen bereit.

So lässt sich eine Coroutine schnell mit der Funktion »launch()« anwerfen und via »delay()« vorübergehend schlafen legen, während »async()« und »avail()« das aus Java bekannte Async-Avail-Prinzip umsetzen. Mit Coroutinen erzeugen Entwickler zudem Generatoren. Diese Funktionen merken sich ihren Zustand für den nächsten Aufruf (Listing 13).

Listing 13

Generatoren

01 val natuerlicheZahlen = buildSequence {
02         var a = 1
03
04         while (true) {
05                 yield(a)
06                 a = a + 1
07         }
08 }
09 natuerlicheZahlen.take(10).forEach { x -> print("$x ") }

Die Funktion »buildSequence()« erzeugt hier eine Folge mit der ihr übergebenen Lambda-Funktion. Letztgenannte bildet die Coroutine und zählt im Beispiel einfach eine Variable hoch. Der Aufruf von »yield()« hält die Coroutine an und liefert die aktuelle Zahl zurück. Derzeit gelten die Coroutinen noch als experimentell, was sich aber bereits mit der nächsten Kotlin-Version ändern soll.

Noch mehr

Kotlin unterstützt noch zahlreiche weitere Konzepte. So dürfen Entwickler wie in Java über so genannte Annotationen Meta-Informationen in ihren Code einstreuen. Darüber hinaus kennt Kotlin auch die vor allem von funktionalen Sprachen unterstützte Endrekursion (Tail Recursion). Die erlaubt es, Funktionen und Klassen ineinander zu verschachteln und die Klassen zudem zu versiegeln (Sealed Classes).

Mit passenden Funktionen, Lambda-Ausdrücken und den Kurzschreibweisen lassen sich zudem Dokumente, etwa eine HTML-Seite, in deskriptiver Form notieren. Vor allem Groovy-Entwickler schätzen diese so genannten Builder, die in Kotlin sogar typsicher sind (Listing 14).

Listing 14

Builder

01 val seite = html {
02         head {
03                 title {"Meine Website"}
04         }
05         body {
06                 p  {"Hallo Welt!"}
07         }
08 }

Fazit

Kotlin besteht aus einem bunten Strauß angesagter Programmiertechniken. Dennoch wirkt die Sprache wie aus einem Guss. Mechanismen wie die statische Typableitung sorgen für weniger Fehler, die Kompatibilität zu Javascript und insbesondere Java erlaubt es, bestehende Bibliotheken zu nutzen.

Der Quellcode ist zwar kompakt, an einige Konstrukte müssen sich aber insbesondere Java-Umsteiger erst gewöhnen. Die gute Dokumentation und vor allem die Testumgebung auf der Kotlin-Homepage vereinfachen den Einstieg. Kein Wunder also, dass Kotlin zunehmend mehr Freunde findet.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben