Programmieren in Kotlin

Hier ist eine Reihe von Themenbereichen aufgelistet, die du einzeln auf- und zuklappen kannst (Klick auf die kleine Pfeilspitze).

Die wichtigsten Informationen sind schwarz geschrieben.

Zusatz- oder Hintergrundinformationen sind grau geschrieben.

I. Funktionen (im engeren und im weiteren Sinn)

1. Funktionen definieren

Wenn ich eine Funktion definiere, programmiere ich aus Einzelteilen einen Baustein, den ich später (mithilfe des von mir festgelegten Funktionsbezeichners) als Ganzen verwenden kann. In Kotlin gibt es dafür zwei Möglichkeiten.

a) "Funktionsschreibweise" (nur für Funktionen "im engeren Sinn")

Eine so notierte Funktion berechnet immer anhand des Funktionsterms genau einen Rückgabewert.

Allgemein:

Beispiel:

fun bezeichner(parameter: typ, parameter: typ, ...) = term

fun addieren(x: Int, y: Int) = x + y

fun halbieren(zahl: Double) = zahl / 2.0

Die Parameter aus der Parameterliste sind nur "innerhalb der jeweiligen Funktion", hier also rechts vom =-Zeichen der jeweiligen Funktionsdefinition, gültig.

Beachte, dass – anders als bei Klassen – vor den Parametern nicht val oder var steht.

b) "Blockschreibweise" (auch für "Funktionen im weiteren Sinn")

Eine so notierte Funktion kann einen Rückgabewert erzeugen oder auch nicht. Sie kann außerdem noch allerhand anderes erledigen.

Allgemein:

Beispiele:

fun bezeichner(parameter: typ, ...): typ {
  ... ggf. algorithmus/programm ...
  return rückgabewert/-term
}

fun kreisflächeBerechnen(radius: Double): Double {
   return radius * radius * kotlin.math.PI
}

fun begrüßen(name: String) {
   println("Hallo $name - schön, dass du da bist!")
}

fun rumeiern() {
   println("Was mach ich'n jetz?")
   println("Ich glaub, ich mach nix.")
}

Wenn – mithilfe der return-Anweisung – ein Rückgabewert erzeugt wird, muss dessen Datentyp im Anschluss an die Parameterliste angegeben werden.

Die Parameter aus der Parameterliste sind nur "innerhalb der jeweiligen Funktion", hier also innerhalb der geschweiften Klammern der jeweiligen Funktionsdefinition, gültig.

2. Funktionen verwenden

Nachdem eine Funktion definiert worden ist, kann ich sie in einem Term (allein oder in Kombination) verwenden. Wir sprechen auch davon, dass ich eine Funktion "aufrufe". Dabei benutze ich den Bezeichner der Funktion, gefolgt von der Parameterliste, in der jetzt für jeden Parameter ein passender Übergabewert stehen muss.

Funktionsdefinition:

Verwendung/Aufruf der Funktion:

fun halbieren(zahl: Double): Double {
   return zahl / 2.0
}

halbieren(5.3)
Rückgabewert wird 2.65 sein

Wenn die Funktion einen Rückgabewert erzeugt, hat der Funktionsaufruf im Term den Wert des Rückgabewerts.

Term

Wert des Terms

halbieren(5.3) + 0.35

3.0

halbieren(halbieren(8.0))

2.0

II. Variablen (auch solche mit unveränderlichem Wert)

Eine Variable ermöglicht es, Werte im Arbeitsspeicher zu speichern, um sie zu einem späteren Zeitpunkt wieder abzurufen und damit weiterzuarbeiten. Mit einer Variablen kann ich das über einen von mir selbst festgelegten Bezeichner tun.

So muss ich mich nicht darum kümmern, wie ein geeigneter Speicherplatz gefunden werden kann, über welche Speicheradresse (eine Zahl) darauf zugegriffen werden muss, wie sichergestellt wird, dass kein anderes Programm mir meine Werte überschreibt usw.

1. Variablen deklarieren und initialisieren (= erzeugen und mit einem Startwert belegen)

a) Langform (häufig nicht nötig)

Allgemein:

Beispiel:

var bezeichner: typ = startwert

var betrachtetesTier: String = "Seegurke"

val bezeichner: typ = wert

val plzNaila: Int = 95119

Benutze ich das Schlüsselwort var, kann ich den Wert der Variablen später neu setzen. Benutze ich stattdessen das Schlüsselwort val, so lässt sich der Wert später nicht mehr ändern.

Wenn ich mir nicht sicher bin, kann ich einfach var benutzen, dann kann in der Regel nichts schiefgehen.

b) Kurzform (der häufigere Fall)

Allgemein:

Beispiel:

var bezeichner = startwert

var esRegnetGerade = false

val bezeichner = wert

val erdbeschleunigung = 9.81

Der Datentyp kann weggelassen werden, wenn das System ihn am Wert erkennen kann, der der Variable zugewiesen wird.

c) Besonderer Fall: null erlauben

Wenn es möglich sein soll, dass die Variable überhaupt keinen sinnvollen Wert hat, wenn ich also den Platzhalterwert null als Wert der Variablen zulassen möchte, muss ich die Langform benutzen – und hinter den Datentyp ein Fragezeichen setzen.

Beispiel:

var nameDerSchachweltmeisterin2044: String? = null

var heutigerMesswert: Double? = 35.289

2. Variablen verwenden

Wenn ich einen Variablenbezeichner in einem Term benutze, wird er durch den aktuellen Wert der Variable ersetzt.
Schreibe ich aber nach einem Variablenbezeichner ein =-Zeichen, so muss darauf ein Term folgen. Der Wert dieses Terms wird der Variable dann als neuer Wert zugewiesen. Das geht nur, wenn mit Schlüsselwort var deklariert wurde. Außerdem muss der Datentyp, den der Wert des Terms hat, mit dem Datentyp der Variable übereinstimmen.

Deklaration & Initialisierung:

Verwendung der Variable:

var nachricht = "Hallo"

nachricht = nachricht + "Fritz!"
println(nachricht)

Zeichenkette Hallo Fritz! wird auf die Konsole geschrieben
nachricht = "Hallo Alberta!"
println(nachricht)

Zeichenkette Hallo Alberta! wird auf die Konsole geschrieben

val erdbeschleunigung = 9.81
var zeit = 1
var geschwindigkeit: Double? = null

geschwindigkeit = erdbeschleunigung * zeit
Variablenwert von geschwindigkeit jetzt: 9.81
zeit = 2
geschwindigkeit = erdbeschleunigung * zeit

Variablenwert von geschwindigkeit jetzt: 19.62

III. Algorithmen und deren Grundbausteine (/ Kontrollstrukturen)

1. Programm und Algorithmus

Ein Algorithmus ist eine endliche Folge eindeutiger Anweisungen, die, wenn sie unter gleichen Ausgangsbedingungen ausgeführt wird, zum gleichen Ergebnis führt. Wenn ein Algorithmus in einer Programmiersprache formuliert wird, entsteht ein Programm. Die jeweilige Programmiersprache legt fest, was als eindeutige Anweisung gilt. In Kotlin sind das Definitionen von Variablen, Wertzuweisungen an Variablen, Funktionsaufrufe, Methodenaufrufe ...

2. Grundbausteine von Algorithmen

a) Die Sequenz / Folge von Anweisungen als zusammengehörender "Block"

Wenn mehrere Anweisungen als zusammengehörig gekennzeichnet werden müssen, werden sie in geschweifte Klammern {...} eingeschlossen. Gebraucht wird das bei Funktionsdefinitionen, bedingten Anweisungen, bedingten Wiederholungen ...

Bedingte Anweisungen, bedingte Wiederholungen etc. können selbst auch innerhalb eines solchen Blocks vorkommen, dann gibt es z. B. innerhalb der geschweiften Klammern nach einem if wieder ein if {...}.

Variablen, die innerhalb eines Blocks deklariert werden, gelten in diesem Block – und in allen darin enthaltenen Blöcken -, aber nicht außerhalb.

b) Die bedingte Anweisung / Fallunterscheidung

Unter der gegebenen Bedingung etwas Bestimmtes machen, ansonsten gar nichts machen:

Allgemein:

Beispiel:

if (aussagefunktion) {
  anweisung
  anweisung
  ...
}

val geheimwort = "geheim"
val eingabe = readLine()
if (eingabe == geheimwort) {
  println("Glueckwunsch!!!")
  println("Du hast das Geheimwort erraten.")
}

Unter der gegebenen Bedingung etwas Bestimmtes machen, ansonsten etwas anderes machen:

Allgemein:

Beispiel:

if (aussagefunktion) {
  anweisung
  anweisung
  ...
}
else {

  anweisung
  anweisung
  ...
}

val geheimwort = "geheim"
val eingabe = readLine()
if (eingabe == geheimwort) {
  println("Glueckwunsch!!!")
  println("Du hast das Geheimwort erraten.")
}
else {
  println("Pech gehabt!)
  println("Das war nicht das Geheimwort.")
}

Wenn mehr als zwei Fälle unterschieden werden sollen, kann nach else direkt mit if (...) {...} weitergemacht werden usw.

c) Die bedingte Wiederholung

Wenn die angegebene Bedingung erfüllt ist, etwas Bestimmtes machen, danach wieder von vorn prüfen, ob die Bedingung erfüllt ist, und wenn ja, dasselbe wieder machen und immer so weiter, bis die Bedingung nicht mehr erfüllt ist.

Wenn die Bedingung bei der ersten Prüfung nicht erfüllt ist, wird gar nichts gemacht (wie bei if).

Allgemein:

Beispiel:

while (aussagefunktion) {
  anweisung
  anweisung
  ...
}

val geheimwort = "geheim"
var eingabe = readLine()
while (eingabe != geheimwort) {
  println("Du hast das Geheimwort noch nicht erraten.")
  println("Versuche es noch einmal!")
  eingabe = readLine()
}
println("Richtig, das ist das Geheimwort!")

IV. Objekte und Klassen

Objekte sind Bündel von Variablen bzw. Funktionen, die unter einem einzigen Bezeichner abgespeichert werden können. Die Variablen im Bündel nennen wir Attribute, die Funktionen im Bündel nennen wir Methoden.

Solange es noch einen Bezeichner gibt, unter dem ein Objekt gespeichert ist, bleibt es – mit allen seinen Attributen und Methoden – erhalten. Gibt es keinen solchen Bezeichner mehr, wird das Objekt entsorgt.

Um Objekte zu erzeugen, programmiere ich üblicherweise eine Klasse, in der festgelegt ist, wie Objekte eines bestimmten Typs aussehen sollen. Die Klasse kann dann beliebig viele Objekte ihres Typs erzeugen. "Ihres Typs" ist hier in jeder Hinsicht wörtlich zunehmen: Mit jeder Klasse erzeuge ich auch einen neuen Datentyp. Dieser Datentyp hat denselben Bezeichner wie die Klasse.

1. Datenobjekte und Datenklassen

a) Datenklassen definieren

Datenklassen legen nur Attribute, jedoch keine Methoden fest.

Allgemein:

Beispiel:

class bezeichner(variablendeklaration,...)

class Punkt(var x: Double, var y: Double)

data class bezeichner(
  variablendeklaration,
  ...
)

data class Einhorn(
  val name: String,
  val geburtsjahr: Int,
  var flauschfaktor: Double
)

Die Auflistung der Attribute, die in Klammern auf den Klassenbezeichner folgt, nennen wir Konstruktor. Ob der Konstruktor in einer Zeile steht oder auf mehrere Zeilen aufgeteilt wird, spielt keine Rolle.

Der Konstruktor ähnelt der Parameterliste einer Funktion (und er wird auch genau so verwendet – siehe unten). Anders als vor gewöhnlichen Parametern muss aber vor jedem Attributbezeichner val oder var stehen. Das kennen wir von der Deklaration von Variablen. (Auf eine Initialisierung verzichten wir an dieser Stelle in der Regel – möglich wäre sie aber.)

Wenn ich eine Klasse als data class definiere, bekommt sie automatisch ein paar praktische Methoden zum Ausgeben und Vergleichen der Daten, ohne dass ich diese Methoden eigens festlegen müsste (siehe unten).

b) Datenklassen verwenden => Datenobjekte erzeugen

Klassendefinition:

Erzeugung von Objekten:

class Punkt(var x: Double, var y: Double)

val mittelpunkt = Punkt(3.69, 22.79534)

data class Einhorn(
  val name: String,
  val geburtsjahr: Int,
  var flauschfaktor: Double
)

var chefin = Einhorn("Hen", 223, 70.3)
var vize = Einhorn("Lug", 478, 68.9)

Typischerweise werden neu erzeugte Objekte (wie hier in den Beispielen) direkt einer (neuen oder bereits existierenden) Variablen als Wert zugewiesen. Das muss aber nicht sein: Was in den Beispielen hinter dem =-Zeichen steht, könnte z. B. auch in einem Funktionsaufruf als Übergabewert verwendet werden.

c) Datenobjekte verwenden

Mithilfe der Punktnotation kann ich auf die Attribute von Datenobjekten zugreifen. Ich schreibe vor dem Punkt einen Objektbezeichner (also den Bezeichner einer Variable, als deren Wert ein Objekt gespeichert ist) und nach dem Punkt einen Attributbezeichner (jeweils ohne Leerzeichen dazwischen). Zusammen lässt sich dies genau so verwenden wie ein einfacher Variablenbezeichner.
Die folgenden Beispiele setzen die Beispiele unter IV.1.b) voraus:

Term

Wert des Terms

mittelpunkt.x

3.69

chefin.geburtsjahr < vize.geburtsjahr

true

vize.flauschfaktor + 3

71.9

Wertzuweisung

Wert des Attributs danach

mittelpunkt.x = 18.724

18.724

vize.flauschfaktor = vize.flauschfaktor + 1.1

70.0

2. "Vollständige" Objekte und Klassen

a) "Vollständige" Klassen definieren

Außer Attributen können in einer Klasse auch Methoden festgelegt werden. Dies geschieht in einem Block, der nach dem Konstruktor folgt, dem sogenannten Klassenrumpf.

Allgemein:

Beispiel:

class bezeichner(variablendeklaration,...) {
  ... funktionsdefinitionen ...
}

class Quadrat(var kantenlaenge: Int) {
  fun umfangGeben() = kantenlaenge * 4
  fun flaecheGeben(): Int {
    return kantenlaenge * kantenlaenge
  }
}

Im Klassenrumpf können auch (weitere) Attribute deklariert werden – die müssen dann auch gleich initialisiert werden (normalerweise mit einem festen Wert, der dann für alle Objekte der Klasse gleich ist).

Allgemein:

Beispiel:

class bezeichner(variablendeklaration,...) {
  ... variablendeklarationen-und-initialisierungen ...
  ... funktionsdefinitionen ...
}

class Quadrat(var kantenlaenge: Int) {
  var sichtbar = true
  fun umfangGeben() = kantenlaenge * 4
  fun flaecheGeben(): Int {
    return kantenlaenge * kantenlaenge
  }
}

Wenn gleich bei der Erzeugung eines Objekts bestimmte Aufgaben erfüllt werden sollen, werden diese in einem init-Block angegeben.

Allgemein:

Beispiel:

class bezeichner(variablendeklaration,...) {
  ... variablendeklarationen-und-initialisierungen ...
  init {
    ... anweisungen ...
  }
  ... funktionsdefinitionen ...
}

class Quadrat(var kantenlaenge: Int) {
  var sichtbar = true
  init {
    println("Quadrat wird erzeugt")
  }
  fun umfangGeben() = kantenlaenge * 4
  fun flaecheGeben(): Int {
    return kantenlaenge * kantenlaenge
  }
}

b) "Vollständige" Klassen verwenden => Objekte erzeugen => Objekte verwenden

Das Erzeugen "vollständiger" Objekte geht genauso wie das Erzeugen von Datenobjekten. Um Methoden aufzurufen, verwende ich genauso wie beim Zugriff auf Attribute die Punktnotation (siehe oben).

Klassendefinition:

Erzeugung und Verwendung von Objekten:

class Quadrat(var kantenlaenge: Int) {
  var sichtbar = true
  fun umfangGeben() = kantenlaenge * 4
  fun flaecheGeben(): Int {
    return kantenlaenge * kantenlaenge
  }
}

val drati = Quadrat(14)
drati.flaecheGeben()
Rückgabewert wird 196 sein
drati.sichtbar = false
Wert von dratis Attribut sichtbar jetzt: false

c) Datenkapselung

Jedes Objekt hat die Kontrolle darüber, was mit seinen – in den Attributen gespeicherten – Daten gemacht wird. Methoden des Objekts können dazu immer ohne Punktnotation auf die Attribute des Objekts zugreifen. Andere Programmteile können dann mit Punktnotation auf die Attribute zugreifen, wenn das nicht verboten wurde: Um das zu verbieten, kann man in einer Klassendefinition das Schlüsselwort private (oder das Schlüsselwort protected) vor die Deklaration des Attributs schreiben.

Klassendefinition:

Erzeugung und Verwendung von Objekten:

class Nachrichtensafe(private val geheimzahl: Int) {
  private var textspeicher = ""
  fun nachrichtSetzen(pin: Int, nachricht: String) {
    if (pin == geheimzahl) {
      textspeicher = nachricht
    }
  }
  fun nachrichtGeben(pin: Int): String {
    if (geheimzahl == pin) {
      return textspeicher
    }
    else {
      return "Nix gibt's!"
    }
  }
}

val safe = Nachrichtensafe(1234)
safe.geheimzahl
geht nicht
safe.textspeicher
geht auch nicht
safe.nachrichtSetzen(1234, "Hase")
safe.nachrichtSetzen(5432, "Berg")

bewirkt nichts
safe.nachrichtGeben(7385)
Rückgabewert wird "Nix gibt's!" sein
safe.nachrichtGeben(1234)
Rückgabewert wird "Hase" sein

V. Vererbung

Eine Unterklasse "erbt" alle Attribute und Methoden ihrer Oberklasse. Üblicherweise "erweitert" sie diese durch weitere Attribute bzw. Methoden. Oder sie "überschreibt" bestimmte Methoden, die sie geerbt hat.

1. Vererbung ermöglichen – Oberklassen definieren

a) Gewöhnliche Oberklassen

Eine Klasse, die als Oberklasse dienen soll, wird so definiert, wie oben beschrieben, vor dem Schlüsselwort class muss aber das Schlüsselwort open stehen. Wenn es möglich sein soll, eine Methode der Klasse zu überschreiben, muss auch bei der Definition der entsprechenden Methode das Schlüsselwort open verwendet werden.

Beispiel:

open class Rechteck(var laenge: Int, var breite: Int) {
  open fun umfangGeben() = laenge * 2 + breite * 2
  fun flaecheGeben(): Int {
    return laenge * breite
  }
}

b) Abstrakte Klassen

Wenn eine Klasse nur als Oberklasse dienen soll, selbst aber keine Objekte erzeugen können soll, kommt das Schlüsselwort abstract zum Einsatz – auch bei der Deklaration von Methoden, die noch gar nicht definiert werden, die es aber in Unterklassen auf jeden Fall geben muss:

Beispiel:

abstract class GeometrischeFigur(var mittelpunktX: Double, var mittelpunktY: Double) {
  abstract fun umfangGeben(): Double
  abstract fun flaecheGeben(): Double
  fun verschieben(deltaX: Double, deltaY: Double) {
    mittelpunktX = mittelpunktX + deltaX
    mittelpunktY = mittelpunktY + deltaY
  }
}

2. Unterklassen definieren – Vererbung nutzen

Eine Unterklasse erbt alles, was die Oberklasse kennzeichnet. Objekte, die von ihr erzeugt werden, werden sich (auch) so verhalten, wie man es von Objekten der Oberklasse erwartet – deshalb haben sie (auch) den Datentyp der Oberklasse. Dementsprechend setzt man hinter den Konstruktor einer Unterklasse einen Doppelpunkt (:); danach folgt (wie sonst ein Datentyp) der Bezeichner der Oberklasse, mit der Liste von Übergabewerten, die der Konstruktor der Oberklasse fordert.

Das folgende Beispiel setzt das Beispiel unter V.1.a) voraus:

class Quadrat(kantenlaenge: Int): Rechteck(kantenlaenge, kantenlaenge) {
  override fun umfangGeben() = laenge * 4
}

Beachte im Beispiel oben:

Das nächste Beispiel setzt das Beispiel unter V.1.b) voraus:

class QuadratFigur(var kantenlaenge: Double, mX: Double, mY: Double): GeometrischeFigur(mX, mY) {
  override fun umfangGeben() = kantenlaenge * 4
  override fun flaecheGeben() = kantenlaenge * kantenlaenge
}

Beachte im Beispiel oben:

Zum Erzeugen von Objekten einer Unterklasse gibt es nichts Besonderes anzumerken.

VI. Arrays

Hilfsvorstellung: Ein Array ist wie ein benanntes Regal mit durchnummerierten Regalfächern. In jedem Fach kann ein Objekt abgelegt werden.

Arrays erlauben es, mehrere Werte desselben Datentyps an durchnummerierten Speicherplätzen unter einem einzigen Variablenbezeichner zu speichern. Mit der Kombination aus diesem Variablenbezeichner und einer Speicherplatznummer – dem sogenannten Index – kann auf die einzelnen Werte zugegriffen werden. Dabei kann der Index auch mithilfe einer Variable (vom Typ Int) angegeben werden. So ist es möglich, ein Programm (oder die Nutzerin des Programms) steuern zu lassen, auf welchen Wert zugegriffen werden soll.

1. Arrays erzeugen ...

... mithilfe der arrayOfNulls-Funktion

Die arrayOfNulls-Funktion erzeugt ein "leeres" Array für Objekte des gewünschten Datentyps. "Leer" ist das Array insofern, als an jedem Speicherplatz das null-Objekt gespeichert wird.

Allgemein:

Beispiel:

arrayOfNulls<typ>(anzahlSpeicherplaetze)

arrayOfNulls<Int>(10)

arrayOfNulls<String>(50)

Die Datentypen der in den Beispielen erzeugten Arrays sind Int? und String?.

... mithilfe der arrayOf-Funktion

Die arrayOf-Funktion wird mit einer beliebigen Zahl von Übergabewerten eines beliebigen Datentyps aufgerufen. Sie erzeugt ein Array, in dem genau die übergebenen Werte in der angegebenen Reihenfolge gespeichert sind.

Allgemein:

Beispiel:

arrayOf(uebergabewert, ...)

arrayOf(10, 3015, -67, 88, 1, 240, 431)

arrayOf("Hase", "Igel", "Fuchs", "Rabe", "Eidechse")

... wie andere Objekte auch mithilfe des Konstruktors der Klasse Array

Dem Konstruktor der Klasse Array wird ein ganzzahliger Übergabewert für die gewünschte Größe des Arrays übergeben und zusätzlich eine Funktion, die die Startwerte für die Speicherplätze im Array berechnet.

Allgemein:

Beispiel:

Array(anzahlSpeicherplaetze){_ -> term}

Array(10){_ -> ""}
erzeugt ein Array für 10
Zeichenketten, in dem
an jedem Speicherplatz
die leere Zeichenkette
gespeichert ist.

oder

Array(anzahlSpeicherplaetze){parameter -> term}

Array(70){index -> index + 1}
erzeugt ein Array für 70 Ganz-
zahlen, in dem die Zahlen von
1 bis 70 gespeichert sind.

2. Arrays verwenden

a) Auf einzelne Werte in einem Array zugreifen

Um auf einen bestimmten Wert zuzugreifen, der in einem Array gespeichert ist, schreibe ich

bezeichner[index].

Deklaration & Initialisierung:

Verwendung des Arrays:

val woerter = arrayOfNulls<String>(20)

woerter[0] = "Salatessig"
println(woerter[0])

Zeichenkette Salatessig
wird auf die Konsole geschrieben
var index = 7
woerter[index]?.count()

Der Methodenaufruf wird nicht ausgeführt,
weil am Index 7 null gespeichert ist.

b) Auf viele bzw. alle Werte in einem Array zugreifen (über ein Array iterieren)

Um flexibel auf verschiedene Werte im Array zugreifen zu können, gebe ich den Index mithilfe einer Variable an. Den Wert der Variable verändere ich im Rahmen einer bedingten Wiederholung so, dass jeder gewünschte Index einmal 'an die Reihe kommt'.

Deklaration & Initialisierung:

Iterieren über das Array:

val zahlen = arrayOf(53, 12, 89, 47)

var index = 0
while (index < zahlen.count()) {
  println(zahlen[index])
  index = index + 1
}

Alle Zahlen im Array werden der
Reihe nach auf die Konsole geschrieben.

VII. Weitere (praktische) Kontrollstrukturen

1. Bedingte Anweisung mit when

a) Mehrfach-Fallunterscheidung

Allgemein:

Beispiel:

when {
  bedingung -> anweisung
  bedingung -> anweisung
  ...
  else -> anweisung
}

val geheimwort = "geheim"
val eingabe = readLine()
when {
  eingabe == geheimwort -> println("Glueckwunsch!!!")
  eingabe[0] == geheimwort[0] -> println("1. Zeichen richtig")
  else -> println("Falsch geraten")
}

Die Auflistung der Bedingungen wird von oben nach unten abgearbeitet. Wenn eine der Bedingungen erfüllt ist, werden die folgenden Bedingungen nicht mehr geprüft – es wird also immer nur eine der Anweisungen auf der rechten Seite der "Pfeile" ausgeführt.

b) Mustervergleich

Den Wert eines Terms mit verschiedenen Mustern vergleichen und jeweils entsprechend reagieren.

Allgemein:

Beispiel:

when (term) {
  muster -> anweisung
  muster -> anweisung
  ...
  else -> anweisung
}

println("Was möchten Sie tun?")
println("1. Uhrzeit anzeigen")
println("2. Datum anzeigen")
val eingabe = readLine()
when (eingabe) {
  "1" -> println(java.time.LocalTime.now())
  "2" -> println(java.time.LocalDate.now())
  else -> println("Ungültige Eingabe")
}

Auf der linken Seite eines "Pfeils" können auch, durch Komma getrennt, mehrere Muster angegeben werden.

2. Wiederholung mit for

a) Spezialfall: Verwendung zum "Durchzählen"

Einer Variablen der Reihe nach jeden Wert aus einem Bereich von ganzzahligen Werten zuweisen – und demnach so oft wiederholen, wie es die Anzahl von Werten im Bereich vorgibt.

Allgemein:

Beispiel:

for (bezeichner in ganzzahl..ganzzahl) {
  anweisung
  anweisung
  ...
}

for (zahl in 1..10) {
  println(zahl)
}

Die Zahlen von 1 bis einschließ-
lich 10 werden auf die Konsole
geschrieben.

Beachte, dass in der Klammer eine Variable (im Beispiel zahl) deklariert wird, ohne dass dafür das Schlüsselwort var verwendet wird. Diese Variable kann, wie im Beispiel, in den zu wiederholenden Anweisungen verwendet werden (das muss aber nicht sein, dann liegt eine "klassische Wiederholung mit fester Anzahl" vor).

Das lässt sich auch zum Iterieren über ein Array verwenden:

Deklaration & Initialisierung:

Iterieren über das Array:

val zahlen = arrayOf(3, 5, 8, 7)

for (index in 0..zahlen.count()-1) {
  zahlen[index] = zahlen[index] + 1
}

Im Array stehen jetzt die Werte 4, 6, 9, 8.

Vorsicht bei Verwendung mit Listen etc.: Wenn deren Größe im Rahmen der Wiederholung verändert wird, geschehen ggf. merkwürdige Dinge ...

Auch praktisch: Im Beispiel oben könnte index in 0..zahlen.count()-1 durch index in zahlen.indices ersetzt werden. Warum das geht, erklärt sich aus dem Folgenden ...

b) Allgemein: Verwendung zum Iterieren (ohne "Zählvariable")

Einer Variablen der Reihe nach jeden Wert aus einer Collection (also einem Array, einer Liste o. Ä.) zuweisen.

Allgemein:

Beispiel:

for (bezeichner in collectionbezeichner) {
  anweisung
  anweisung
  ...
}

val namen = arrayOf("Uli", "Ali", "Ulf")
for (name in namen) {
  println(name)
}

Die drei Namen werden auf die Konsole geschrieben.

Hier wird die in der Klammer deklarierte Variable auf jeden Fall in den zu wiederholenden Anweisungen verwendet werden.

Achtung: Wenn ich for so verwende, kann ich mithilfe der in der Klammer deklarierten Variable keine Werte in die Collection schreiben. Es darf auch im Rahmen der Wiederholung gar nichts an der Collection verändert werden!

VIII. null-Sicherheit

... noch im Aufbau ...

IX. Wichtige Operatoren und Datentypen

1. Operatoren

a) Arithmetische Operatoren

Operator:

Bedeutung:

+

plus

-

minus

*

mal

/

geteilt durch (ggf. ganzzahlig ohne Rest)

%

modulo (ergibt den Rest der ganzzahligen Division)

b) Zuweisungsoperator

Operator:

Bedeutung:

=

Weise den Wert des Terms rechts vom Operator der Variablen mit dem Bezeichner links vom Operator zu! (Wertzuweisung)

c) Vergleichsoperatoren (Aussagefunktionen)

Aussagefunktionen sind Funktionen, deren Rückgabewert entweder true oder false ist.

Operator:

Bedeutung:

==

ist gleich

!=

ist nicht gleich

>

ist größer als

<

ist kleiner als

>=

ist größer als oder gleich

<=

ist kleiner als oder gleich

d) Logische Operatoren (Logische Funktionen sind auch Aussagefunktionen)

Operator:

Bedeutung:

&&

UND

||

ODER

!

NICHT

2. Datentypen

a) Vordefinierte Datentypen

Bezeichner:

'Art' der Daten:

Beispiele für Notation der Daten:

Int

Ganzzahl

4  77  -23

Double

Kommazahl

4.321  77.8  -23.08754

String

Zeichenkette

"Hase"  "Gurkensalat"  "Auf geht's!"

Boolean

Wahrheitswert

true  false  

Char

einzelnes Zeichen

'e'  'P'  ';'

Float

Kommazahl (weniger genau)

4.321f  77.8f  -23.08754f

b) Durch Benutzerin/Benutzer erstellte Datentypen

Mit jeder Klasse erzeuge ich auch einen neuen Datentyp. Der Datentyp hat denselben Bezeichner wie die Klasse.