Aus Linux-Magazin 07/2021

Was Kotlin im Vergleich mit anderen Sprachen zu bieten hat

© undrey / 123RF.com

Dieser Artikel möchte Kotlin vorstellen, eine vielseitige Sprache zur funktionalen und objektorientierten Programmierung. Ein Streifzug führt unter anderem an Immutables und Callbacks sowie verschiedenen Varianten von Listenern vorbei.

Die erste stabile Version von Kotlin erschien im Februar 2016. Im Jahr 2017 erklärte die Firma Google Kotlin zu einer offiziellen Sprache für die Entwicklung von Apps für das Betriebssystem Android, im Mai 2019 zur bevorzugten Sprache für diesen Zweck.

IntelliJ IDEA ist eine IDE von Jetbrains, die sich für die Entwicklung von Kotlin-Programmen eignet. Mit Android Studio von Google, einem Ableger von IntelliJ IDEA, lassen sich Android-Apps in Java oder Kotlin schreiben.

Laut Google verwendete im Mai 2019 bereits über die Hälfte der Android-Entwickler Kotlin. Um dieselbe Aufgabe zu lösen, braucht Kotlin weniger Code als die bisher bevorzugte Sprache Java. Kotlin bietet zahlreiche Möglichkeiten, Android-Apps übersichtlicher und sicherer zu entwickeln. Dabei kann der Kotlin-Programmierer direkt auf Java-Bibliotheken zugreifen, was es unter anderem ermöglicht, vorhandene Java-Projekte schrittweise und zügig auf Kotlin umzurüsten. Zudem wurden die Java-Bibliotheken für Kotlin um viele Funktionen erweitert.

Die Listings in diesem Artikel beinhalten nur die relevanten Codezeilen. Bei Interesse können Sie die vollständigen Projekte [1] von der Linux-Magazin-Webseite herunterladen.

Immutables

Variablen verweisen in Kotlin auf Werte einfacher Datentypen oder auf Objekte komplexer Datentypen. Um Fehlermöglichkeiten zu minimieren, lassen sich die Bedingungen kontrollieren, unter denen man auf Variablen zugreifen kann. In diesem Zusammenhang sind unter anderem die Stichworte Scope und Immutable wichtig.

Der Scope (Gültigkeitsbereich) einer Variablen sollte möglichst klein sein. Bereits die IDE bemerkt, wenn ein Entwickler lesend oder schreibend auf eine Variable außerhalb ihres Gültigkeitsbereichs zugreifen will. Sie verhindert dann die Übersetzung und Ausführung des Programms.

Viele Variablen erhalten einmalig einen Wert zugewiesen und sollen ihn für den restlichen Verlauf des Programms behalten. Möchten Sie die versehentliche Änderung einer solchen Variable verhindern, deklarieren Sie sie als Immutable, also als unveränderliche Variable. In der Folge bemerkt bereits die IDE Versuche, auf eine solche Variable schreibend zuzugreifen, und verhindert die Übersetzung.

Viele Programmiersprachen setzen das Konzept der Immutables um, neben Kotlin zum Beispiel C# oder Javascript. Kam es jedoch in einer Sprache nachträglich dazu, ergeben sich häufig Probleme. Visual Studio, die Standard-IDE für C#, schützt auf diese Weise beispielsweise nur Klassen-Member. In Javascript lassen sich Variablen aufgrund der Abwärtskompatibilität auf mehrere Arten im Programm einführen, sodass auch ein unkontrollierter Zugriff möglich ist.

In der jungen Sprache Kotlin wurde das Konzept dagegen von Beginn an umgesetzt, weshalb es dort solche Probleme nicht gibt. Ein Beispiel soll verdeutlichen, wie das Konzept in den drei genannten Sprachen realisiert wird. Dafür soll eine GUI für Android-Apps eine Schaltfläche und ein Ausgabefeld enthalten. Nach Betätigung der Schaltfläche gibt die Software die Werte der veränderlichen Variablen »a« und der unveränderlichen Variablen »b« aus (Listing 1).

Listing 1

Immutable in Kotlin

B.buZahl.setOnClickListener {
 var a = 42
 a = 43
 val b = 5
 // b = 53
 B.tvAusgabe.text = "%d %d".format(a, b)
}

Der Versuch, die letztgenannte Variable auf einen anderen Wert verweisen zu lassen, führt zu einem Fehler und ist daher auskommentiert. Die Verbindung zwischen GUI und Code wird mithilfe des ViewBindings hergestellt. Das Betätigen des View-Buttons »buZahl« führt zur Ausgabe in der TextView »tvAusgabe«. Veränderliche Variablen deklariert man in Kotlin mit »var«, unveränderliche mit »val«. Einen Versuch, die unveränderliche Variable »b« auf einen anderen Wert verweisen zu lassen, lässt die IDE nicht zu.

Listing 2 zeigt das Erstellen einer Forms-GUI für Windows in C#. Das Betätigen der Schaltfläche »CmdZahl« führt zur Ausgabe im Label »LblAusgabe«. Die Eigenschaft der Unveränderlichkeit wird mithilfe des Modifizierers »readonly« eingestellt. Den Versuch, den unveränderlichen Klassen-Member »b« auf einen anderen Wert verweisen zu lassen, toleriert die IDE nicht.

Listing 2

Immutable in C#

public partial class Form1 : Form
{
 private int a = 42;
 private readonly int b = 52;
 [...]
 private void CmdZahl_Click(object sender, EventArgs e)
 {
 a = 43;
 // b = 53;
 LblAusgabe.Text = a + " " + b;
 }
}

Der Javascript-Code in Listing 3 stammt aus einer Browser-GUI für beliebige Betriebssysteme, die ein Formular beinhaltet. Das Betätigen der Schaltfläche mit der ID »idButton« führt zur Ausgabe im deaktivierten Eingabefeld mit der ID »idAusgabe«. Der Versuch, die unveränderliche Variable »b« auf einen anderen Wert verweisen zu lassen, führt zu einem Abbruch des Programms.

Listing 3

Immutable in Javascript

<script>
 function geklickt()
 {
 let a = 42;
 a = 43;
 const b = 52;
 // b = 53;
 document.getElementById("idAusgabe")
 .value = a + " " + b;
 }
</script>
[...]
<form>
 <p><input id="idButton" type="button" value="Zahl"></p>
 <p><input id="idAusgabe" size="6" disabled></p>
</form>
<script>
 document.getElementById("idButton")
 .addEventListener("click", geklickt);
</script>

Veränderliche Variablen deklariert man in Javascript mit »let«, unveränderliche mit »const«. Variablen könnten aber auch weiterhin mit »var« deklariert werden oder einfach ohne Deklaration im Programm erscheinen. Das würde einen unkontrollierten Zugriff ermöglichen.

Callbacks

Die Flexibilität von Funktionen und Methoden lässt sich mithilfe von Callbacks (deutsch: Rückruffunktionen) erhöhen. Eine solche Funktion übergibt man als Parameter an eine eigene oder vordefinierte Funktion. Die benannte oder anonyme Rückruffunktion wird innerhalb der anderen Funktion meist mehrfach aufgerufen. Es gibt zum Beispiel zahlreiche vordefinierte Sortiermethoden für unterschiedliche Datenstrukturen, denen man eine selbst definierte Rückruffunktion übergeben kann, in der das Vergleichskriterium für die Sortierung der Elemente der Datenstruktur formuliert wird.

Dieses Konzept verdeutlichen die folgende Beispiele für C++, Python, PHP und Kotlin. Die Ausgabefunktion »ausgabe()« nimmt eine Berechnung für eine Reihe von Werten vor und stellt die Ergebnisse dar. Sie erhält zum einen die benannte Funktion »haelfte()« übergeben, die jeweils die Hälfte eines Werts berechnet, und zum anderen eine anonyme Funktion, die jeweils ein Viertel eines Werts ermittelt. Nicht die Komplexität der Rückruffunktion steht hier im Vordergrund, sondern der Callback-Mechanismus.

C++ (Listing 4) listet die Rückruffunktion als Parameter der Ausgabefunktion auf, mit Datentyp und eigener Parameterliste. Beim mehrmaligen Aufruf der Rückruffunktion in der Ausgabefunktion wird jeweils der aktuelle Parameter angegeben – in diesem Fall der aktuelle Wert der Schleifenvariablen. Der Aufruf der Ausgabefunktion gibt zum einen den Namen der benannten Funktion an, und zwar ohne Parameter, zum anderen die Definition der anonymen Funktion. Diese wird mit einer Capture List (Erfassungsliste) in rechteckigen Klammern eingeleitet, in der man der anonymen Funktion Variablen oder Referenzen von außen bekannt machen kann. Es folgt die Parameterliste in runden Klammern. Als Letztes werden in geschweiften Klammern die Anweisungen der Funktion definiert.

Listing 4

callback.cpp

#include <iostream>
#include <iomanip>
using namespace std;
double haelfte(const int& x)
{
 return x / 2.0;
}
void ausgabe(const int& von, const int& bis, const int& schritt, double fkt(const int& x))
{
 cout << fixed << setprecision(2);
 for(double i = von; i < bis; i += schritt)
 cout << fkt(i) << " ";
 cout << endl;
}
int main()
{
 ausgabe(5, 14, 2, haelfte);
 ausgabe(5, 14, 2, [](const int& x){return x/4.0;});
}

In Python (Listing 5) besitzt der Parameter der Ausgabefunktion weder einen Datentyp noch eine eigene Parameterliste. Die Definition der anonymen Funktion wird mit dem Schlüsselwort »lambda« eingeleitet; es folgen die Parameterliste, ein Doppelpunkt und die Anweisung, die die Funktion bildet. In Python muss dieser Lambda-Ausdruck in derselben Zeile stehen und darf keine Mehrfachanweisungen, Ausgaben oder Schleifen umfassen.

Listing 5

callback.py

def haelfte(x):
 return x/2
def ausgabe(von, bis, schritt, fkt):
 for i in range(von, bis, schritt):
 print(fkt(i), end=" ")
 print()
ausgabe(5, 14, 2, haelfte)
ausgabe(5, 14, 2, lambda x: x/4)

In PHP (Listing 6) besitzt der Parameter der Ausgabefunktion ebenfalls keinen Datentyp und keine eigene Parameterliste. Der Name der benannten Funktion wird beim Aufruf der Ausgabefunktion in einer Zeichenkette übergeben. Das Schlüsselwort »function« leitet die Definition der anonymen Funktion ein. Es folgen die Parameterliste in runden Klammern sowie die Anweisungen der Funktion in geschweiften Klammern. Diese werden wie in C++ wie die Anweisungen einer benannten Funktion notiert.

Listing 6

callback.php

<?php
  function haelfte($x)
  {
    return $x / 2;
  }
  function ausgabe($von, $bis, $schritt,
 $fkt)
  {
    for($i = $von; $i < $bis; $i += $schritt)
      echo $fkt($i) . " ";
    echo "<br>";
  }
?>
[...]
<?php
  ausgabe(5, 14, 2, "haelfte");
  ausgabe(5, 14, 2, function($x) { return $x / 4; });
?>

Kotlin (Listing 7) listet die Rückruffunktion als Parameter der Ausgabefunktion sehr genau auf, inklusive des Namens der Funktion, eines Doppelpunkts, der eigenen Parameterliste in runden Klammern, des Operators »->« und des Datentyps des Rückgabewerts der Funktion. Der Name der benannten Funktion wird beim Aufruf der Ausgabefunktion durch den Operator »::« eingeleitet. Die Definition der anonymen Funktion steht in geschweiften Klammern. Sie beginnt mit dem Parameter, gefolgt von einem Doppelpunkt, dem Operator »->« und den Anweisungen der Funktion. Der Rückgabewert wird nicht wie in C++ und PHP mithilfe eines Schlüsselworts wie etwa »return« zurückgeliefert, sondern nur als Ausdruck.

Listing 7

callback.kt

fun main() {
    ausgabe(5, 14, 2, ::haelfte)
    ausgabe(5, 14, 2) { x: Int -> x / 4.0 }
}
fun haelfte(x:Int):Double {
    return x / 2.0
}
fun ausgabe(von:Int, bis:Int, schritt:Int, fkt:(Int) -> Double) {
    for(i in von until bis step schritt)
        print(" " + fkt(i))
    println()
}

Bildet die Definition der anonymen Funktion den letzten Parameter der Ausgabefunktion, empfiehlt der IDE-Editor eine Auslagerung hinter die runden Klammern. Im Beispiel wird mit einer weiteren Standard-IDE für Kotlin gearbeitet: IntelliJ IDEA.

Listener

In Listing 1 wird beim Aufruf der Methode »setOnClickListener()« ein Listener-Objekt definiert, das auf das Ereignis “Betätigen der Schaltfläche »buZahl«” reagiert. Dabei wird eine kompakte Variante mit einer anonymen Funktion genutzt, wie sie in Kotlin häufig zum Einsatz kommt. Ein Listener bemerkt ein Ereignis und reagiert mithilfe von Code darauf. Ein Objekt, das als Listener dient, muss man zuvor für das Ereignis registrieren. Das gilt sprachunabhängig für alle drei Varianten des ersten Beispiels.

Im Folgenden geht es um verschiedene Möglichkeiten, Listener-Objekte in Kotlin anzulegen. Wie hängen sie zusammen und wie leitet sich die kompakte Abart her? Dazu gilt es, einen Streifzug durch viele Sprachelemente von Kotlin zu unternehmen: Interfaces, Klassen, Objekte als Instanzen von Klassen, benannte Singleton-Objekte, anonyme Singleton-Objekte mit und ohne Verweis, anonyme Funktionen sowie Single-Abstract-Method-Interfaces.

Als Beispiel dient eine GUI für Android-Apps im Android-Studio-Projekt »Listener«. Sie umfasst insgesamt zehn Button-Views mit den Bezeichnungen »buEins« bis »buZehn« sowie die TextView »tvAusgabe«. Die Verbindung zwischen GUI und Code stellt auch hier das ViewBinding her.

Das Interface

Ein Interface ist eine Vorschrift für den Aufbau einer Klasse oder eines Objekts. Die Methoden eines Interfaces beinhalten keinen Code. Um das Interface bei der Definition einer Klasse oder eines Objekts zu verwenden, muss man es implementieren. Seine Methoden müssen also in der Klasse oder dem Objekt vollständig definiert werden. Ein Objekt, das als Listener für das Ereignis “Antippen eines Buttons” reagiert, muss das Interface »View.OnClickListener« implementieren. Es schreibt vor, die Methode »onClick()« zu implementieren.

Nach Antippen des Buttons »buEins« wird in der TextView eine »1« ausgegeben, für den Button »buZwei« entsprechend eine »2«. Für beide Ereignisse dient das aktuelle Objekt der Klasse »MainActivity« als Listener. Der Vorteil: Die Behandlung mehrerer ähnlicher Ereignisse kann gemeinsam in der aktuellen Klasse stattfinden (Listing 8).

Listing 8

Variante mit Interface

[...]
class MainActivity: AppCompatActivity(), View.OnClickListener {
  [...]
  override fun onCreate(savedInstanceState: Bundle?) {
  [...]
    B.buEins.setOnClickListener(this)
    B.buZwei.setOnClickListener(this)
    [...]
  }
  override fun onClick(v: View?) {
    when (v?.id) {
      B.buEins.id -> B.tvAusgabe.text = "1"
      B.buZwei.id -> B.tvAusgabe.text = "2"
    }
  }
}
[...]

Das Listener-Objekt ist das aktuelle Objekt der Klasse »MainActivity«, auf das man mithilfe des Schlüsselworts »this« verweist. Der Parameter der Methode »onClick()« verweist auf dasjenige Objekt, bei dem das Ereignis ausgelöst wurde, also auf den Button »buEins« respektive »buZwei«. Mithilfe einer Verzweigung wird ermittelt, welcher Button das Ereignis ausgelöst hat.

Nach Antippen des Buttons »buDrei« ändert sich die Aufschrift des Buttons in den Text “Hallo”. Für dieses Ereignis dient ein neu erstelltes Objekt der eigenen Klasse »MeineListenerKlasse« als Listener. Die Behandlung mehrerer ähnlicher Ereignisse kann gemeinsam in einer eigenen Klasse stattfinden (Listing 9). Diesmal muss die eigene Klasse das Interface »View.OnClickListener« mit der Methode »onClick()« implementieren (Listing 10).

Listing 9

Variante mit eigener Klasse

[...]
class MainActivity: AppCompatActivity() [...] {
  override fun onCreate(savedInstanceState: Bundle?) {
    [...]
    val mDrei = MeineListenerKlasse()
    buDrei.setOnClickListener(mDrei)
  }
}
[...]

Listing 10

Datei MeineListenerKlasse.kt

[...]
class MeineListenerKlasse: View.OnClickListener {
  override fun onClick(v: View?) {
    val bu = v as Button
    bu.text = "Hallo"
  }
}
[...]

Benanntes Singleton-Objekt

Von einer Klasse lassen sich beliebig viele Objekte erzeugen. Singleton-Objekte dagegen sind einzigartig und ermöglichen einen eindeutigen Zugriff. Die Behandlung mehrerer ähnlicher Ereignisse kann gemeinsam in einem Singleton-Objekt stattfinden, ohne eine eigene Klasse zu definieren. Nach Antippen des Buttons »buVier« ändert sich die Aufschrift des Buttons in den Text “Hallo”.

Für dieses Ereignis dient ein benanntes Singleton-Objekt mit dem Namen »MeinListenerObjekt« als Listener (Listing 11). Auf das definierte Singleton-Objekt greift man über dessen Namen zu. Es muss das Interface »View.OnClickListener« mit der Methode »onClick()« implementieren (Listing 12)

Listing 11

Mit benanntem Singleton-Objekt

[...]
class MainActivity:AppCompatActivity() [...] {
  override fun onCreate(savedInstanceState: Bundle?) {
    [...]
    buVier.setOnClickListener(MeinListenerObjekt)
  }
}
[...]

Listing 12

Datei MeinListenerObjekt.kt

[...]
object MeinListenerObjekt: View.OnClickListener {
  override fun onClick(v: View?) {
    val bu = v as Button
    bu.text = "Hallo"
  }
}
[...]

Anonymes Singleton-Objekt

Nach Antippen des Buttons »buFuenf« wird in der TextView eine Fünf ausgegeben. Für dieses Ereignis dient ein anonymes Singleton-Objekt als Listener, auf das man einen Verweis einrichtet. Über diesen Verweis lässt sich später auf das Singleton-Objekt zugreifen (Listing 13).

Listing 13

Anonymes Singleton-Objekt mit Verweis

[...]
class MainActivity: AppCompatActivity() [...] {
  override fun onCreate(savedInstanceState: Bundle?) {
    [...]
    val mFuenf = object: View.OnClickListener {
      override fun onClick(v: View?) {
        tvAusgabe.text = "5"
      }
    }
    buFuenf.setOnClickListener(mFuenf)
  }
}

Das anonyme Singleton-Objekt wird hier innerhalb der Methode »onCreate()« definiert. Es muss das Interface »View.OnClickListener« mit der Methode »onClick()« implementieren. Die Variable »mFuenf« verweist auf das anonyme Singleton-Objekt. Nach Antippen des Buttons »buFuenf« wird über die Variable »mFuenf« auf das Singleton-Objekt als Listener verwiesen.

Anonym ohne Verweis

Nach Antippen des Buttons »buSechs« wird in der TextView eine Sechs ausgegeben. Für dieses Ereignis dient ein anonymes Singleton-Objekt als Listener, auf das diesmal kein Verweis eingerichtet wird. Der Code verkürzt sich weiter (Listing 14).

Listing 14

Anonymes Singleton-Objekt ohne Verweis

[...]
class MainActivity:AppCompatActivity() [...] {
  override fun onCreate(savedInstanceState: Bundle?) {
    [...]
    buSechs.setOnClickListener(object: View.OnClickListener {
      override fun onClick(v: View?) {
        tvAusgabe.text = "6"
      }
    }
  }
}

Zum Erzeugen eines anonymen Singleton-Objekts als Listener-Objekt notiert man den vollständigen Code des Objekts inklusive des Schlüsselworts »object« als Parameter der Methode »setOnClickListener()« in den runden Klammern. Das anonyme Objekt muss das Interface »View.OnClickListener« mit der Methode »onClick()« implementieren.

SAM-Interface

Interfaces schreiben häufig die Implementierung mehrerer Methoden vor. Es gibt jedoch funktionelle Interfaces, auch SAM-Interfaces (Single Abstract Method) genannt, die nur das Implementieren einer einzigen Methode vorschreiben.

Bei SAM-Interfaces kann in Kotlin eine anonyme Funktion als Listener dienen. Das erspart viel Code, unter anderem den Namen des Interfaces und den Namen der Methode. Ergeben sich die Datentypen der Parameter der anonymen Funktion aus dem Zusammenhang, lässt man sie ebenfalls weg; der Code wird noch kompakter.

Nach Antippen des Buttons »buSieben« ändert sich die Aufschrift des Buttons in den Text “Hallo” (Listing 15). Bei einem solchen Verweis auf ein Listener-Objekt darf man die runden Klammern beim Methodenaufruf weglassen.

Listing 15

Variante mit anonymer Funktion

[...]
class MainActivity:AppCompatActivity() [...] {
  override fun onCreate(savedInstanceState: Bundle?) {
    [...]
    buSieben.setOnClickListener { v ->
      val bu = v as Button
      bu.text = "Hallo"
    }
  }
}

Ungenutzte Parameter

Werden Parameter einer anonymen Funktion nicht in der Funktion genutzt, kann man statt ihres Namens jeweils das Zeichen »_« (den Unterstrich) notieren. Nach dem Antippen des Buttons »buAcht« wird in der TextView eine Acht ausgegeben (Listing 16).

Listing 16

Variante mit nicht genutztem Parameter

[...]
class MainActivity:AppCompatActivity() [...] {
  override fun onCreate(savedInstanceState: Bundle?) {
    [...]
    buAcht.setOnClickListener { _ ->
      tvAusgabe.text = "8"
    }
  }
}

Der Parameter <C>it<C>

Besitzt eine anonyme Funktion nur einen einzigen Parameter, kann man diesen zusammen mit dem Operator »->« weglassen. Er steht über das Schlüsselwort »it« dennoch in der Funktion zur Verfügung. Nach Antippen des Buttons »buNeun« wird die Aufschrift des Buttons in den Text “Hallo” geändert (Listing 17).

Listing 17

Variante it und kompakt

[...]
class MainActivity:AppCompatActivity() [...] {
  override fun onCreate(savedInstanceState: Bundle?) {
    [...]
    buNeun.setOnClickListener {
      val bu = it as Button
      bu.text = "Hallo"
    }
  }
}

Besitzt eine anonyme Funktion nur einen einzigen Parameter und wird dieser in der Funktion nicht benötigt, lässt sich häufig die kompakte Variante nutzen. Nach Antippen des Buttons »buZehn« wird in der TextView der Text “10” ausgegeben (Listing 18).

Listing 18

Kompakte Variante

[...]
class MainActivity:AppCompatActivity() [...] {
  override fun onCreate(savedInstanceState: Bundle?) {
    [...]
    buZehn.setOnClickListener {
      tvAusgabe.text = "10"
    }
  }
}

Ausblick

Die vorstehenden Beispiele haben exemplarisch einige Möglichkeiten der Sprache Kotlin demonstriert, die zeigen, warum sich die noch junge, aber ausdrucksstarke Sprache einer wachsenden Beliebtheit erfreut. Freilich ließen sich an dieser Stelle nicht alle Eigenschaften ins rechte Licht rücken. So hat Kotlin auch für die asynchrone Programmierung einiges zu bieten (Coroutinen) und ist besonders absturzsicher, weil es hilft, Null-Pointer-Exceptions zu vermeiden. Ein guter Nachweis seiner Leistungsfähigkeit sind nicht zuletzt die vielen sehr bekannten Android-Apps, die mit Kotlin entwickelt wurden, darunter Kindle, Netflix, Evernote, WordPress, Amex und viele andere mehr (jcb/jlu).

Infos

  1. Downloads zum Artikel: https://theisweb.de/LinuxMagazin2107
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 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