Listen

Eine List ist ein Behälter, also ein Objekt, das andere Objekte enthält.

Behälter werden auch als Sammlungen bezeichnet. Wenn wir einen grundlegenden Behälter für die Beispiele in diesem Buch benötigen, verwenden wir normalerweise eine List.

Lists sind Teil des Standard-Kotlin-Pakets, daher benötigen sie keinen import.

Das folgende Beispiel erstellt eine List, die mit Ints gefüllt ist, indem die Standardbibliotheksfunktion listOf() mit Initialisierungswerten aufgerufen wird:

// Lists/Lists.kt
import atomictest.eq

fun main() {
  val ints = listOf(99, 3, 5, 7, 11, 13)
  ints eq "[99, 3, 5, 7, 11, 13]"   // [1]

  // Select each element in the List:
  var result = ""
  for (i in ints) {                 // [2]
    result += "$i "
  }
  result eq "99 3 5 7 11 13"

  // "Indexing" into the List:
  ints[4] eq 11                     // [3]
}
  • [1] Eine List verwendet eckige Klammern, um sich selbst darzustellen.
  • [2] for-Schleifen funktionieren gut mit Lists: for(i in ints) bedeutet, dass i jeden Wert in ints erhält. Sie deklarieren val i nicht und geben auch nicht seinen Typ an; Kotlin erkennt aus dem Kontext, dass i ein for-Schleifen-Identifikator ist.
  • [3] Eckige Klammern indexieren in eine List. Eine List behält ihre Elemente in der Initialisierungsreihenfolge bei, und Sie wählen sie einzeln nach Nummer aus. Wie in den meisten Programmiersprachen beginnt Kotlin das Indexieren beim Element Null, was in diesem Fall den Wert 99 ergibt. Somit ergibt ein Index von 4 den Wert 11.

Das Vergessen, dass das Indexieren bei Null beginnt, führt zum sogenannten Eins-zu-viel-Fehler. In einer Sprache wie Kotlin wählen wir oft nicht Elemente einzeln aus, sondern iterieren stattdessen durch einen gesamten Container mit in. Dies eliminiert Eins-zu-viel-Fehler.

Wenn Sie einen Index über das letzte Element in einer List hinaus verwenden, wirft Kotlin eine ArrayIndexOutOfBoundsException:

// Lists/OutOfBounds.kt
import atomictest.*

fun main() {
  val ints = listOf(1, 2, 3)
  capture {
    ints[3]
  } contains
    listOf("ArrayIndexOutOfBoundsException")
}

Eine List kann alle verschiedenen Typen halten. Hier ist eine List von Doubles und eine List von Strings:

// Lists/ListUsefulFunction.kt
import atomictest.eq

fun main() {
  val doubles =
    listOf(1.1, 2.2, 3.3, 4.4)
  doubles.sum() eq 11.0

  val strings = listOf("Twas", "Brillig",
    "And", "Slithy", "Toves")
  strings eq listOf("Twas", "Brillig",
    "And", "Slithy", "Toves")
  strings.sorted() eq listOf("And",
    "Brillig", "Slithy", "Toves", "Twas")
  strings.reversed() eq listOf("Toves",
    "Slithy", "And", "Brillig", "Twas")
  strings.first() eq "Twas"
  strings.takeLast(2) eq
    listOf("Slithy", "Toves")
}

Dies zeigt einige der Operationen von List. Beachten Sie den Namen “sorted” anstelle von “sort”. Wenn Sie sorted() aufrufen, erzeugt es eine neue List, die die gleichen Elemente wie die alte in sortierter Reihenfolge enthält—aber es lässt die ursprüngliche List unverändert. Es “sort” zu nennen, impliziert, dass die ursprüngliche List direkt verändert wird (auch bekannt als sortiert an Ort und Stelle). In Kotlin sieht man häufig diese Tendenz, “das ursprüngliche Objekt unverändert zu lassen und ein neues Objekt zu erzeugen.” reversed() erzeugt ebenfalls eine neue List.

Parametrisierte Typen

Wir betrachten es als gute Praxis, Typinferenz zu verwenden—es neigt dazu, den Code sauberer und leichter lesbar zu machen. Manchmal jedoch beschwert sich Kotlin, dass es nicht herausfinden kann, welchen Typ es verwenden soll, und in anderen Fällen macht Explizitheit den Code verständlicher. So teilen wir Kotlin mit, welchen Typ eine List enthält:

// Lists/ParameterizedTypes.kt
import atomictest.eq

fun main() {
  // Type is inferred:
  val numbers = listOf(1, 2, 3)
  val strings =
    listOf("one", "two", "three")
  // Exactly the same, but explicitly typed:
  val numbers2: List<Int> = listOf(1, 2, 3)
  val strings2: List<String> =
    listOf("one", "two", "three")
  numbers eq numbers2
  strings eq strings2
}

Kotlin verwendet die Initialisierungswerte, um abzuleiten, dass numbers eine List von Ints enthält, während strings eine List von Strings enthält.

numbers2 und strings2 sind explizit typisierte Versionen von numbers und strings, erstellt durch das Hinzufügen der Typdeklarationen List<Int> und List<String>. Sie haben Winkelklammern noch nicht gesehen - sie kennzeichnen einen Typparameter, der es Ihnen ermöglicht zu sagen: “Dieser Container enthält ‘Parameter’-Objekte.” Wir sprechen List<Int> als “List von Int” aus.

Typparameter sind nützlich für Komponenten, die keine Container sind, aber man sieht sie oft bei containerähnlichen Objekten.

Rückgabewerte können ebenfalls Typparameter haben:

// Lists/ParameterizedReturn.kt
package lists
import atomictest.eq

// Return type is inferred:
fun inferred(p: Char, q: Char) =
  listOf(p, q)

// Explicit return type:
fun explicit(p: Char, q: Char): List<Char> =
  listOf(p, q)

fun main() {
  inferred('a', 'b') eq "[a, b]"
  explicit('y', 'z') eq "[y, z]"
}

Kotlin leitet den Rückgabetyp für inferred() ab, während explicit() den Rückgabetyp der Funktion angibt. Man kann nicht einfach sagen, dass es eine List zurückgibt; Kotlin wird beanstanden, also muss man auch den Typ-Parameter angeben. Wenn Sie den Rückgabetyp einer Funktion angeben, setzt Kotlin Ihre Absicht durch.

Schreibgeschützte und veränderbare Listen

Wenn Sie nicht ausdrücklich sagen, dass Sie eine veränderbare List möchten, erhalten Sie keine. listOf() erzeugt eine schreibgeschützte List, die keine veränderbaren Funktionen hat.

Wenn Sie eine List schrittweise erstellen (das heißt, Sie haben nicht alle Elemente zum Zeitpunkt der Erstellung), verwenden Sie mutableListOf(). Dies erzeugt eine MutableList, die verändert werden kann:

// Lists/MutableList.kt
import atomictest.eq

fun main() {
  val list = mutableListOf<Int>()

  list.add(1)
  list.addAll(listOf(2, 3))

  list += 4
  list += listOf(5, 6)

  list eq listOf(1, 2, 3, 4, 5, 6)
}

Da list keine anfänglichen Elemente hat, müssen wir Kotlin mitteilen, welchen Typ es hat, indem wir die <Int>-Spezifikation im Aufruf von mutableListOf() angeben. Sie können Elemente zu einer MutableList mit add() und addAll() hinzufügen oder den Operator += verwenden, der entweder ein einzelnes Element oder eine andere Sammlung hinzufügt.

Eine MutableList kann als List behandelt werden, in diesem Fall kann sie nicht geändert werden. Sie können jedoch eine schreibgeschützte List nicht als MutableList behandeln:

// Lists/MutListIsList.kt
package lists
import atomictest.eq

fun makeList(): List<Int> =
  mutableListOf(1, 2, 3)

fun main() {
  // makeList() produces a read-only List:
  val list = makeList()
  // list.add(3) // Unresolved reference: add
  list eq listOf(1, 2, 3)
}

list fehlt es an Mutationsfunktionen, obwohl es ursprünglich mit mutableListOf() innerhalb von makeList() erstellt wurde. Beachten Sie, dass der Ergebnistyp von makeList() List<Int> ist. Das ursprüngliche Objekt ist immer noch eine MutableList, wird aber durch die Linse einer List betrachtet.

Eine List ist schreibgeschützt—Sie können ihren Inhalt lesen, aber nicht schreiben. Wenn die zugrunde liegende Implementierung eine MutableList ist und Sie eine veränderbare Referenz auf diese Implementierung beibehalten, können Sie sie weiterhin über diese veränderbare Referenz modifizieren, und alle schreibgeschützten Referenzen werden diese Änderungen sehen. Dies ist ein weiteres Beispiel für Aliasing, eingeführt in Einschränken der Sichtbarkeit:

// Lists/MultipleListRefs.kt
import atomictest.eq

fun main() {
  val first = mutableListOf(1)
  val second: List<Int> = first
  second eq listOf(1)
  first.add(2)
  // second sees the change:
  second eq listOf(1, 2)
}

first ist eine unveränderliche Referenz (val) auf das veränderliche Objekt, das von mutableListOf(1) erzeugt wird. Wenn second auf first aliasiert wird, wird es zu einer Ansicht desselben Objekts. second ist schreibgeschützt, weil List<Int> keine Änderungsfunktionen beinhaltet. Ohne die explizite List<Int> Typdeklaration würde Kotlin annehmen, dass second ebenfalls eine Referenz auf ein veränderliches Objekt ist.

Wir können dem Objekt ein Element (2) hinzufügen, weil first eine Referenz auf eine veränderliche Liste ist. Beachten Sie, dass second diese Änderungen beobachtet—es kann die Liste nicht ändern, obwohl die Liste über first geändert wird.

Das += Rätsel

Der += Operator kann den Anschein erwecken, dass eine unveränderliche Liste tatsächlich veränderlich ist:

// Lists/ApparentlyMutableList.kt
import atomictest.eq

fun main() {
  var list = listOf('X') // Immutable
  list += 'Y' // Appears to be mutable
  list eq "[X, Y]"
}

listOf() erzeugt eine unveränderliche List, aber list += 'Y' scheint diese List zu ändern. Verstößt += irgendwie gegen die Unveränderlichkeit?

Dies passiert nur, weil list ein var ist. Hier ist ein detaillierteres Beispiel, das die verschiedenen Kombinationen von veränderlichen/unveränderlichen Lists mit val/var zeigt:

// Lists/PlusAssignPuzzle.kt
import atomictest.eq

fun main() {
    // Mutable List assigned to a 'val'/'var':
    val list1 = mutableListOf('A') // or 'var'
    list1 += 'A' // Is the same as:
    list1.plusAssign('A')               // [1]

    // Immutable List assigned to a 'val':
    val list2 = listOf('B')
    // list2 += 'B' // Is the same as:
    // list2 = list2 + 'B'              // [2]

    // Immutable List assigned to a 'var':
    var list3 = listOf('C')
    list3 += 'C' // Is the same as:
    val newList = list3 + 'C'           // [3]
    list3 = newList                     // [4]

    list1 eq "[A, A, A]"
    list2 eq "[B]"
    list3 eq "[C, C, C]"
}
  • [1] list1 bezieht sich auf ein veränderbares Objekt, das daher vor Ort modifiziert werden kann. Der Compiler übersetzt += zum Aufruf von plusAssign(). Es spielt keine Rolle, ob list1 ein val oder ein var ist, da list1 nach der Erstellung niemals neu zugewiesen wird—es verweist immer auf die gleiche veränderbare Liste. Wenn man es zu einem var macht, weist IntelliJ darauf hin, dass es sich nie ändert und schlägt vor, es zu einem val zu machen.
  • [2] Dies versucht, eine neue List zu erstellen, indem list2 und 'B' kombiniert werden, aber es kann diese neue List nicht list2 neu zuweisen, da list2 ein val ist. Ohne die Möglichkeit, diese Neuzuweisung durchzuführen, kann += nicht kompiliert werden.
  • [3] Erstellt newList ohne die bestehende unveränderliche List zu modifizieren, auf die list3 verweist.
  • [4] Da list3 ein var ist, weist der Compiler newList zurück in list3 zu. Der vorherige Inhalt von list3 wird dann vergessen, und es erscheint, als ob list3 verändert wurde. Tatsächlich wurde das alte list3 verworfen und durch das neu erstellte newList ersetzt, was die Illusion erzeugt, dass list3 veränderbar ist.

Dieses Verhalten von += tritt auch bei anderen Sammlungen auf. Die daraus resultierende Verwirrung ist ein weiterer Grund, val gegenüber var für Ihre Bezeichner zu bevorzugen.

Übungen und Lösungen finden Sie unter www.AtomicKotlin.com.