Stufe 5Collections · Lektion 28 von 34
Array und veränderliche Collections
Unveränderliche Collections sind in Scala der Normalfall. Für Schnittstellen zu Java, für rechenintensive Schleifen und für das schrittweise Aufbauen großer Datenmengen gibt es aber veränderliche Gegenstücke: Array, ArrayBuffer und ListBuffer.
Array
Ein Scala-Array ist ein ganz normales Java-Array. Es hat eine feste Länge, die Elemente lassen sich aber ändern:
val zahlen = Array(5, 3, 8, 1)
zahlen(0) = 50 // Element ändern
zahlen.length // 4
zahlen.sorted // neues Array: Array(1, 3, 8, 50)
zahlen.mkString(", ") // "50, 3, 8, 1"
val leer = Array.ofDim[Double](3) // Array(0.0, 0.0, 0.0)
val matrix = Array.ofDim[Int](3, 3) // 3 × 3, alle 0
matrix(1)(2) = 7Auf Arrays funktionieren alle Methoden, die Sie von List kennen – map, filter, foldLeft und so weiter. Sie liefern jeweils ein neues Array.
Arrays brauchen Sie vor allem, wenn eine Java-Bibliothek eines erwartet oder liefert, wie args: Array[String] in der klassischen main-Methode, und in rechenintensivem Code, der von der kompakten Speicherung profitiert: Ein Array[Int] speichert echte primitive Zahlen, ohne jedes Element als Objekt zu verpacken.
ArrayBuffer: ein Array, das wachsen kann
import scala.collection.mutable.ArrayBuffer
val puffer = ArrayBuffer[String]()
puffer += "eins"
puffer ++= List("zwei", "drei")
puffer.insert(0, "start")
puffer.remove(1) // entfernt "eins"
puffer(0) // "start"
puffer.toList // List(start, zwei, drei)Ein ArrayBuffer verhält sich wie Javas ArrayList: schneller Zugriff per Index, schnelles Anhängen am Ende. Einfügen am Anfang oder in der Mitte ist dagegen teuer, weil alle folgenden Elemente verschoben werden.
ListBuffer: Listen effizient aufbauen
ListBuffer ist darauf spezialisiert, eine Liste Stück für Stück aufzubauen und am Ende in eine unveränderliche List zu verwandeln:
import scala.collection.mutable.ListBuffer
def primzahlenBis(n: Int): List[Int] =
val ergebnis = ListBuffer[Int]()
for k <- 2 to n do
if ergebnis.forall(p => k % p != 0) then ergebnis += k
ergebnis.toList
primzahlenBis(30) // List(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)toList kostet bei einem ListBuffer nur einen konstanten Aufwand – er wird dabei nicht kopiert. Genau dafür ist er gemacht.
Veränderlich lokal, unveränderlich nach außen
Die übliche Faustregel lautet: Veränderliche Collections dürfen innerhalb einer Methode verwendet werden, wenn das die Lösung einfacher oder schneller macht. Nach außen gibt die Methode aber eine unveränderliche Sammlung zurück – so wie primzahlenBis oben. Für Aufrufer ist die Funktion damit genauso sicher, als wäre sie rein funktional geschrieben.
Gefährlich wird es, wenn veränderliche Collections über Methodengrenzen oder gar zwischen Threads geteilt werden. Dann kann jede Stelle im Programm den Inhalt verändern, und Fehler werden schwer nachvollziehbar.
Umwandeln zwischen den Welten
val a = Array(1, 2, 3)
a.toList // List(1, 2, 3)
a.toVector // Vector(1, 2, 3)
a.to(ArrayBuffer) // ArrayBuffer(1, 2, 3)
List(1, 2, 3).toArray // Array(1, 2, 3)Das allgemeine to(…) nimmt als Argument das Begleitobjekt der gewünschten Collection und funktioniert mit jedem Typ.
Welche veränderliche Collection wofür?
| Bedarf | Werkzeug |
|---|---|
| feste Größe, Java-Schnittstelle, Zahlenkolonnen | Array |
| wachsende Liste mit Indexzugriff | ArrayBuffer |
Liste Stück für Stück aufbauen, am Ende List | ListBuffer |
| Warteschlange (vorne raus, hinten rein) | mutable.Queue |
| Stapel | mutable.Stack |
| Menge bzw. Zuordnung | mutable.Set, mutable.Map |