Die Programmiersprache Kotlin kennenlernen

Kotlin ist eine weit verbreitete Programmiersprache von Android-Entwicklern. Dieses Thema dient als Kotlin-Absturzkurs, der einen schnellen Einstieg ermöglicht.

Variablendeklaration

Kotlin verwendet zwei verschiedene Schlüsselwörter zum Deklarieren von Variablen: val und var.

  • Verwenden Sie val für eine Variable, deren Wert sich nie ändert. Sie können einer Variablen, die mit val deklariert wurde, keinen Wert neu zuweisen.
  • Verwenden Sie var für eine Variable, deren Wert sich ändern kann.

Im folgenden Beispiel ist count eine Variable vom Typ Int, der ein Anfangswert von 10 zugewiesen ist:

var count: Int = 10

Int ist ein Typ, der eine Ganzzahl darstellt. Dies ist einer der vielen numerischen Typen, die in Kotlin dargestellt werden können. Ähnlich wie andere Sprachen können Sie abhängig von Ihren numerischen Daten auch Byte, Short, Long, Float und Double verwenden.

Das Keyword var bedeutet, dass Sie count bei Bedarf Werte zuweisen können. Beispielsweise können Sie den Wert von count von 10 in 15 ändern:

var count: Int = 10
count = 15

Einige Werte sollten jedoch nicht geändert werden. Sehen wir uns ein String namens languageName an. Wenn languageName immer den Wert „Kotlin“ enthalten soll, können Sie languageName mit dem Schlüsselwort val deklarieren:

val languageName: String = "Kotlin"

Mit diesen Keywords können Sie deutlich machen, was geändert werden kann. Nutzen Sie sie nach Bedarf zu Ihrem Vorteil. Wenn ein Variablenverweis neu zuweisbar sein muss, deklarieren Sie ihn als var. Andernfalls verwenden Sie val.

Inferenz eingeben

Ausgehend vom vorherigen Beispiel kann der Kotlin-Compiler den Typ anhand des Typs des zugewiesenen Werts ableiten, wenn Sie languageName einen Anfangswert zuweisen.

Da der Wert von "Kotlin" vom Typ String ist, leitet der Compiler ab, dass languageName ebenfalls ein String ist. Kotlin ist eine statisch typisierte Sprache. Das bedeutet, dass der Typ bei der Kompilierung aufgelöst wird und sich nie ändert.

Im folgenden Beispiel wird languageName als String abgeleitet. Sie können also keine Funktionen aufrufen, die nicht Teil der String-Klasse sind:

val languageName = "Kotlin"
val upperCaseName = languageName.toUpperCase()

// Fails to compile
languageName.inc()

toUpperCase() ist eine Funktion, die nur für Variablen des Typs String aufgerufen werden kann. Da der Kotlin-Compiler languageName als String abgeleitet hat, können Sie toUpperCase() bedenkenlos aufrufen. inc() ist jedoch eine Int-Operatorfunktion und kann daher nicht für eine String aufgerufen werden. Der Kotlin-Ansatz zur Typinferenz bietet sowohl Präzision als auch Typsicherheit.

Null-Sicherheit

In einigen Sprachen kann eine Referenztypvariable deklariert werden, ohne einen anfänglichen expliziten Wert bereitzustellen. In diesen Fällen enthalten die Variablen normalerweise einen Nullwert. Kotlin-Variablen dürfen standardmäßig keine Nullwerte enthalten. Das bedeutet, dass das folgende Snippet ungültig ist:

// Fails to compile
val languageName: String = null

Damit eine Variable einen Nullwert enthält, muss sie vom Typ Nullwerte sein. Sie können eine Variable als Null-Zulässigkeit festlegen, indem Sie ihrem Typ wie im folgenden Beispiel das Suffix ? anhängen:

val languageName: String? = null

Mit dem Typ String? können Sie languageName entweder einen String-Wert oder null zuweisen.

Sie müssen mit Variablen, für die Nullwerte zulässig sind, vorsichtig umgehen, da Sie sonst ein gefürchtetes NullPointerException riskieren. Wenn Sie in Java beispielsweise versuchen, eine Methode für einen Nullwert aufzurufen, stürzt das Programm ab.

Kotlin bietet eine Reihe von Mechanismen für die sichere Arbeit mit Variablen, für die Nullwerte zulässig sind. Weitere Informationen finden Sie unter Gängige Kotlin-Muster in Android: Nullability.

Bedingungen

Kotlin bietet mehrere Mechanismen zur Implementierung bedingter Logik. Die gebräuchlichste ist eine if-else-Anweisung. Wenn ein in Klammern gesetzter Ausdruck neben einem if-Schlüsselwort true ergibt, wird der Code innerhalb dieses Zweigs (d.h. der direkt darauf folgende Code, der in geschweifte Klammern gesetzt ist) ausgeführt. Andernfalls wird der Code im else-Zweig ausgeführt.

if (count == 42) {
    println("I have the answer.")
} else {
    println("The answer eludes me.")
}

Mit else if können mehrere Bedingungen dargestellt werden. Auf diese Weise können Sie eine detailliertere, komplexere Logik innerhalb einer einzelnen bedingten Anweisung darstellen, wie im folgenden Beispiel gezeigt:

if (count == 42) {
    println("I have the answer.")
} else if (count > 35) {
    println("The answer is close.")
} else {
    println("The answer eludes me.")
}

Bedingte Anweisungen sind nützlich, um eine zustandsorientierte Logik darzustellen. Es kann jedoch vorkommen, dass Sie sich beim Schreiben wiederholen. Im obigen Beispiel geben Sie einfach ein String in jedem Zweig aus. Kotlin bietet bedingte Ausdrücke, um diese Wiederholung zu vermeiden. Das letzte Beispiel kann so umgeschrieben werden:

val answerString: String = if (count == 42) {
    "I have the answer."
} else if (count > 35) {
    "The answer is close."
} else {
    "The answer eludes me."
}

println(answerString)

Implizit gibt jeder bedingte Zweig das Ergebnis des Ausdrucks in der letzten Zeile zurück, sodass Sie kein return-Schlüsselwort verwenden müssen. Da das Ergebnis aller drei Zweige vom Typ String ist, ist das Ergebnis des if-else-Ausdrucks auch vom Typ String. In diesem Beispiel wird answerString ein Anfangswert aus dem Ergebnis des if-else-Ausdrucks zugewiesen. Mithilfe der Typinferenz kann die explizite Typdeklaration für answerString weggelassen werden. Zur besseren Übersichtlichkeit ist es jedoch oft eine gute Idee, sie aufzunehmen.

Wenn die Komplexität Ihrer bedingten Anweisung zunimmt, können Sie Ihren if-else-Ausdruck durch einen when-Ausdruck ersetzen, wie im folgenden Beispiel gezeigt:

val answerString = when {
    count == 42 -> "I have the answer."
    count > 35 -> "The answer is close."
    else -> "The answer eludes me."
}

println(answerString)

Jeder Zweig in einem when-Ausdruck wird durch eine Bedingung, einen Pfeil (->) und ein Ergebnis dargestellt. Wenn die Bedingung auf der linken Seite des Pfeils als „true“ ausgewertet wird, wird das Ergebnis des Ausdrucks auf der rechten Seite zurückgegeben. Die Ausführung erfolgt nicht von einem Zweig zum nächsten. Der Code im Ausdrucksbeispiel when entspricht funktional dem Code im vorherigen Beispiel, ist aber wahrscheinlich einfacher zu lesen.

Die Bedingungen von Kotlin heben eine der leistungsstärkeren Funktionen hervor, das intelligente Streamen. Anstatt den Operator „safe-call“ oder den assertion-Operator „not-null“ für die Arbeit mit Werten, für die Nullwerte zulässig sind, zu verwenden, können Sie wie im folgenden Beispiel mithilfe einer bedingten Anweisung prüfen, ob eine Variable einen Verweis auf einen Nullwert enthält:

val languageName: String? = null
if (languageName != null) {
    // No need to write languageName?.toUpperCase()
    println(languageName.toUpperCase())
}

Innerhalb des bedingten Zweigs kann languageName als NULL-Wert behandelt werden, das keine Nullwerte zulässt. Kotlin ist intelligent genug, um zu erkennen, dass die Bedingung zum Ausführen des Zweigs darin besteht, dass languageName keinen Nullwert enthält. Daher müssen Sie languageName in diesem Zweig nicht als Nullwerte behandeln. Dieses Smart Casting funktioniert für Nullprüfungen, Typprüfungen oder alle Bedingungen, die einen Vertrag erfüllen.

Funktionen

Sie können einen oder mehrere Ausdrücke in einer Funktion gruppieren. Anstatt immer die gleiche Reihe von Ausdrücken jedes Mal zu wiederholen, wenn Sie ein Ergebnis benötigen, können Sie die Ausdrücke in eine Funktion verpacken und diese Funktion aufrufen.

Um eine Funktion zu deklarieren, verwenden Sie das Schlüsselwort fun, gefolgt vom Funktionsnamen. Definieren Sie als Nächstes die Arten von Eingaben, die Ihre Funktion annimmt, falls vorhanden, und deklarieren Sie den Ausgabetyp, der zurückgegeben wird. Im Hauptteil einer Funktion definieren Sie Ausdrücke, die beim Aufrufen Ihrer Funktion aufgerufen werden.

Aufbauend auf vorherigen Beispielen finden Sie hier eine vollständige Kotlin-Funktion:

fun generateAnswerString(): String {
    val answerString = if (count == 42) {
        "I have the answer."
    } else {
        "The answer eludes me"
    }

    return answerString
}

Die Funktion im Beispiel oben hat den Namen generateAnswerString. Es nimmt keine Eingaben entgegen. Es wird ein Ergebnis vom Typ String ausgegeben. Verwenden Sie zum Aufrufen einer Funktion deren Namen, gefolgt vom Aufrufoperator (()). Im folgenden Beispiel wird die Variable answerString mit dem Ergebnis von generateAnswerString() initialisiert.

val answerString = generateAnswerString()

Funktionen können Argumente als Eingabe verwenden, wie im folgenden Beispiel gezeigt:

fun generateAnswerString(countThreshold: Int): String {
    val answerString = if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }

    return answerString
}

Beim Deklarieren einer Funktion können Sie eine beliebige Anzahl von Argumenten und deren Typen angeben. Im Beispiel oben verwendet generateAnswerString() ein Argument namens countThreshold vom Typ Int. Innerhalb der Funktion können Sie mithilfe seines Namens auf das Argument verweisen.

Wenn Sie diese Funktion aufrufen, müssen Sie in den Klammern des Funktionsaufrufs ein Argument einfügen:

val answerString = generateAnswerString(42)

Funktionsdeklarationen vereinfachen

generateAnswerString() ist eine ziemlich einfache Funktion. Die Funktion deklariert eine Variable und wird sofort zurückgegeben. Wenn das Ergebnis eines einzelnen Ausdrucks von einer Funktion zurückgegeben wird, können Sie die Deklarierung einer lokalen Variable überspringen. Dazu wird direkt das Ergebnis des in der Funktion enthaltenen if-else-Ausdrucks zurückgegeben, wie im folgenden Beispiel gezeigt:

fun generateAnswerString(countThreshold: Int): String {
    return if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }
}

Sie können das Rückgabe-Keyword auch durch den Zuweisungsoperator ersetzen:

fun generateAnswerString(countThreshold: Int): String = if (count > countThreshold) {
        "I have the answer"
    } else {
        "The answer eludes me"
    }

Anonyme Funktionen

Nicht jede Funktion benötigt einen Namen. Einige Funktionen werden direkter anhand ihrer Ein- und Ausgaben identifiziert. Diese Funktionen werden als anonyme Funktionen bezeichnet. Sie können auf eine anonyme Funktion verweisen und damit später die anonyme Funktion aufrufen. Sie können die Referenz auch wie bei anderen Referenztypen an Ihre Anwendung übergeben.

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

Wie benannte Funktionen können auch anonyme Funktionen eine beliebige Anzahl von Ausdrücken enthalten. Der zurückgegebene Wert der Funktion ist das Ergebnis des endgültigen Ausdrucks.

Im obigen Beispiel enthält stringLengthFunc einen Verweis auf eine anonyme Funktion, die einen String als Eingabe verwendet und die Länge der Eingabe-String als Ausgabe vom Typ Int zurückgibt. Aus diesem Grund wird der Funktionstyp als (String) -> Int bezeichnet. Dieser Code ruft die Funktion jedoch nicht auf. Um das Ergebnis der Funktion abzurufen, müssen Sie sie wie eine benannte Funktion aufrufen. Sie müssen eine String angeben, wenn Sie stringLengthFunc aufrufen, wie im folgenden Beispiel gezeigt:

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

val stringLength: Int = stringLengthFunc("Android")

Funktionen höherer Ordnung

Eine Funktion kann eine andere Funktion als Argument annehmen. Funktionen, die andere Funktionen als Argumente verwenden, werden als Funktionen höherer Ordnung bezeichnet. Dieses Muster ist nützlich, um zwischen Komponenten auf die gleiche Weise zu kommunizieren wie eine Callback-Schnittstelle in Java.

Hier ein Beispiel für eine Funktion höherer Ordnung:

fun stringMapper(str: String, mapper: (String) -> Int): Int {
    // Invoke function
    return mapper(str)
}

Die stringMapper()-Funktion verwendet eine String zusammen mit einer Funktion, die einen Int-Wert aus einem String ableitet, das Sie an sie übergeben.

Sie können stringMapper() aufrufen, indem Sie einen String und eine Funktion übergeben, die den anderen Eingabeparameter erfüllt. Dabei handelt es sich um eine Funktion, die ein String als Eingabe annimmt und eine Int ausgibt, wie im folgenden Beispiel gezeigt:

stringMapper("Android", { input ->
    input.length
})

Wenn die anonyme Funktion der last-Parameter ist, der für eine Funktion definiert ist, können Sie sie außerhalb der Klammern übergeben, die zum Aufrufen der Funktion verwendet werden, wie im folgenden Beispiel gezeigt:

stringMapper("Android") { input ->
    input.length
}

Anonyme Funktionen finden Sie in der gesamten Kotlin-Standardbibliothek. Weitere Informationen finden Sie unter Höhere Ordnungsfunktionen und Lambdas.

Kurse

Alle bisher genannten Typen sind in die Programmiersprache Kotlin integriert. Wenn Sie einen eigenen benutzerdefinierten Typ hinzufügen möchten, können Sie eine Klasse mit dem Schlüsselwort class definieren, wie im folgenden Beispiel gezeigt:

class Car

Properties

Klassen stellen den Zustand mithilfe von Attributen dar. Eine Property ist eine Variable auf Klassenebene, die einen Getter, einen Setter und ein Sicherungsfeld enthalten kann. Da ein Auto Räder zum Antreiben benötigt, können Sie eine Liste von Wheel-Objekten als Attribut von Car hinzufügen, wie im folgenden Beispiel gezeigt:

class Car {
    val wheels = listOf<Wheel>()
}

Beachten Sie, dass wheels ein public val ist. Das bedeutet, dass wheels von außerhalb der Car-Klasse aufgerufen und nicht neu zugewiesen werden kann. Wenn Sie eine Instanz von Car abrufen möchten, müssen Sie zuerst deren Konstruktor aufrufen. Von dort aus können Sie auf alle zugänglichen Eigenschaften zugreifen.

val car = Car() // construct a Car
val wheels = car.wheels // retrieve the wheels value from the Car

Wenn Sie die Räder anpassen möchten, können Sie einen benutzerdefinierten Konstruktor definieren, der angibt, wie Ihre Klasseneigenschaften initialisiert werden:

class Car(val wheels: List<Wheel>)

Im Beispiel oben verwendet der Klassenkonstruktor ein List<Wheel> als Konstruktorargument und verwendet dieses Argument zum Initialisieren des Attributs wheels.

Klassenfunktionen und Datenkapselung

Klassen verwenden Funktionen, um das Verhalten zu modellieren. Funktionen können den Status ändern, sodass Sie nur die Daten verfügbar machen können, die Sie freigeben möchten. Diese Zugriffssteuerung ist Teil eines größeren objektorientierten Konzepts, das als Kapselung bezeichnet wird.

Im folgenden Beispiel wird das Attribut doorLock außerhalb der Car-Klasse privat gehalten. Um das Auto zu entriegeln, müssen Sie die Funktion unlockDoor() aufrufen und dabei einen gültigen Schlüssel übergeben, wie im folgenden Beispiel gezeigt:

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

Wenn Sie anpassen möchten, wie auf eine Eigenschaft verwiesen wird, können Sie einen benutzerdefinierten Getter und Setter bereitstellen. Wenn Sie beispielsweise den Getter eines Attributs verfügbar machen und gleichzeitig den Zugriff auf seinen Setter einschränken möchten, können Sie diesen Setter als private festlegen:

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    var gallonsOfFuelInTank: Int = 15
        private set

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

Mit einer Kombination aus Attributen und Funktionen können Sie Klassen erstellen, die alle Arten von Objekten modellieren.

Interoperabilität

Eine der wichtigsten Funktionen von Kotlin ist die flexible Interoperabilität mit Java. Da Kotlin-Code bis auf JVM-Bytecode kompiliert wird, kann Ihr Kotlin-Code direkt in Java-Code aufgerufen werden und umgekehrt. Das bedeutet, dass Sie vorhandene Java-Bibliotheken direkt von Kotlin aus nutzen können. Darüber hinaus sind die meisten Android-APIs in Java geschrieben und können direkt aus Kotlin aufgerufen werden.

Nächste Schritte

Kotlin ist eine flexible, pragmatische Sprache mit zunehmendem Support und zunehmender Dynamik. Probieren Sie es einfach aus, falls Sie dies noch nicht getan haben. Weitere Informationen finden Sie in der offiziellen Kotlin-Dokumentation und in der Anleitung zur Anwendung gängiger Kotlin-Muster in Ihren Android-Apps.