Dart-Collections: List, Set und Map (HashSet, HashMap)
Zuletzt aktualisiert:
8 Min. Lesezeit

Collections stecken überall in Flutter-Code: Die Produktliste auf dem Bildschirm ist eine List, die ausgewählten Tags sind ein Set, und das JSON vom Server ist eine Map. Der häufigste Fehler von Lernenden: für alles eine List verwenden und die Frage „Ist dieses Element in der Liste?“ jedes Mal durch einen Durchlauf von vorn bis hinten beantworten. Die richtige Collection zu wählen, ist oft dasselbe, wie den richtigen Algorithmus zu wählen.
Dieser Beitrag behandelt die drei grundlegenden Collections, den Unterschied zwischen HashSet und HashMap aus dart:collection und den Standardtypen Set und Map, die Regel für den korrekten Einsatz eigener Klassen in Collections sowie die praktische Syntax, die mit Dart 3 kam. Das vorige Thema war Interfaces und implements; hier lernen wir, die entworfenen Objekte zusammenzuhalten.
Live-Demo: Beispiele für Dart-Collections
Probieren Sie den Einsatz von List, HashSet und HashMap interaktiv aus:
List: geordnet und über den Index erreichbar
Eine List ähnelt Arrays aus anderen Sprachen. Die Elemente bleiben in der Reihenfolge, in der sie hinzugefügt wurden, jedes hat einen Index ab 0, und derselbe Wert darf mehrfach vorkommen.
void main() {
final zahlen = [3, 4, 5]; // List<int>
final namen = <String>['Ahmet', 'Mehmet', 'Selda'];
final quoten = [3.2, 3.3, 3.5]; // List<double>
print(zahlen); // [3, 4, 5]
print(namen); // [Ahmet, Mehmet, Selda]
print(quoten); // [3.2, 3.3, 3.5]
}Sie müssen nicht jedes Mal List<int> schreiben; Dart leitet den Typ aus dem Literal ab. Bei einer leeren Liste geben Sie den Typ aber an (<String>[]), sonst erhalten Sie eine List<dynamic>.
Hinzufügen, Ändern und Entfernen
void main() {
final zahlen = [3, 4, 5];
zahlen.add(32); // Hängt ans Ende: [3, 4, 5, 32]
zahlen.insert(0, 100); // Fügt am Anfang ein: [100, 3, 4, 5, 32]
zahlen.addAll([7, 8]); // Fügt mehrere hinzu
zahlen[0] = 50; // Ändern
zahlen.removeAt(0); // Entfernt nach Index
zahlen.remove(32); // Entfernt nach Wert (den ersten Treffer)
zahlen.removeWhere((z) => z > 6); // Entfernt nach Bedingung
print(zahlen); // [3, 4, 5]
}Häufig genutzte Eigenschaften
void main() {
final zahlen = [10, 20, 30, 40];
print(zahlen.isEmpty); // false
print(zahlen.isNotEmpty); // true
print(zahlen.length); // 4
print(zahlen.first); // 10
print(zahlen.last); // 40
print(zahlen.contains(20)); // true
print(zahlen.indexOf(30)); // 2
for (final zahl in zahlen) {
print('Zahl: $zahl');
}
}first und last werfen bei einer leeren Liste einen StateError. Wo die Liste leer sein kann, sind die mit Dart 3.0 eingeführten firstOrNull und lastOrNull sicherer.
Collection if, for und Spread
Dart erlaubt Bedingungen und Schleifen direkt im Listenliteral. In Flutter nutzen Sie das ständig, wenn Sie Widget-Listen aufbauen:
void main() {
const angemeldet = true;
final basis = ['Start', 'Suche'];
final menue = [
...basis, // Spread: entpackt eine andere Liste
if (angemeldet) 'Profil', // bedingtes Element
for (var i = 1; i <= 2; i++) 'Seite $i',
];
print(menue); // [Start, Suche, Profil, Seite 1, Seite 2]
}Eine Liste von Objekten: Sortieren und Filtern
In echten Apps enthalten Listen meist Objekte eigener Klassen:
class Schueler {
final int nr;
final String name;
final String klasse;
const Schueler(this.nr, this.name, this.klasse);
@override
String toString() => '$nr - $name ($klasse)';
}
void main() {
final schueler = [
const Schueler(33, 'Ahmet', '3. Klasse'),
const Schueler(199, 'Ayse', '4. Klasse'),
const Schueler(22, 'Mehmet', '2. Klasse'),
];
for (final s in schueler) {
print(s);
}
}In einer früheren Fassung dieses Beitrags waren die Felder nullable deklariert, etwa int? nr, und bei jeder Verwendung stand nr!. Ist die Nummer eines Schülers nie leer, holt ein nullable Feld nur den !-Operator und das Risiko eines Laufzeitfehlers in den Code. Ist der Wert immer vorhanden, schreiben Sie final int nr.
Auch das Überschreiben von toString ist eine kleine, aber nützliche Gewohnheit: print(s) gibt jetzt eine lesbare Zeile statt Instance of 'Schueler' aus.
Sortieren
schueler.sort((a, b) => a.nr.compareTo(b.nr));
print(schueler); // [22 - Mehmet (2. Klasse), 33 - Ahmet (3. Klasse), 199 - Ayse (4. Klasse)]
schueler.sort((a, b) => a.name.compareTo(b.name)); // Nach Namesort ändert die Liste an Ort und Stelle und gibt nichts zurück. final sortiert = schueler.sort(...) zu schreiben, ist ein Fehler, den ich im Unterricht sehr oft sehe; der Compiler meldet, dass ein void-Wert nicht verwendet werden kann. Wollen Sie eine sortierte Kopie, ohne das Original anzutasten, kopieren Sie zuerst: final sortiert = [...schueler]..sort(...).
compareTo vergleicht Text nach Zeichencodes, deshalb landen Umlaute wie Ä, Ö oder Ü nicht unbedingt dort, wo man sie alphabetisch erwartet. Brauchen Namenslisten für Nutzer eine sprachgerechte Sortierung, müssen Sie das gesondert behandeln.
Filtern und Umwandeln
void main() {
final schueler = [
const Schueler(33, 'Ahmet', '3. Klasse'),
const Schueler(199, 'Ayse', '4. Klasse'),
const Schueler(22, 'Mehmet', '2. Klasse'),
];
final kleineNummern = schueler.where((s) => s.nr < 100).toList();
final namen = schueler.map((s) => s.name).toList();
final ayseDabei = schueler.any((s) => s.name == 'Ayse');
final allePositiv = schueler.every((s) => s.nr > 0);
final gefunden = schueler.where((s) => s.nr == 22).firstOrNull;
print(kleineNummern); // [33 - Ahmet (3. Klasse), 22 - Mehmet (2. Klasse)]
print(namen); // [Ahmet, Ayse, Mehmet]
print('$ayseDabei $allePositiv'); // true true
print(gefunden?.name); // Mehmet
}where und map liefern ein träges (lazy) Iterable; die Elemente werden erst beim Durchlaufen berechnet. Brauchen Sie Indexzugriff oder verwenden Sie das Ergebnis mehrfach, machen Sie mit .toList() eine konkrete Liste daraus. In Flutter werden Funktionen wie ein Suchfeld genau mit diesem where-Muster geschrieben.
Set: Elemente ohne Duplikate
Ein Set enthält jedes Element nur einmal und beantwortet die Frage „Ist dieses Element enthalten?“ viel schneller als eine Liste. Bei einer List prüft contains die Elemente einzeln; bei einem Set ist diese Abfrage im Durchschnitt in konstanter Zeit erledigt. Warum das so ist, erkläre ich im Beitrag über Big O.
void main() {
final tags = {'dart', 'flutter'}; // Set<String>
final neu = tags.add('mobil'); // true
final doppelt = tags.add('dart'); // false: schon vorhanden
print(tags); // {dart, flutter, mobil}
print('$neu $doppelt'); // true false
final eindeutig = [1, 2, 3, 4, 1].toSet();
print(eindeutig); // {1, 2, 3, 4}
final a = {1, 2, 3};
final b = {2, 3, 4};
print(a.union(b)); // {1, 2, 3, 4}
print(a.intersection(b)); // {2, 3}
print(a.difference(b)); // {1}
}Eine Falle, auf die Sie achten sollten: {} erzeugt kein leeres Set, sondern eine leere Map. Für ein leeres Set geben Sie den Typ an: <String>{}.
Set und HashSet
Das HashSet im Titel dieses Beitrags stammt aus der Bibliothek dart:collection. Ein Literal {...} oder Set() erzeugt standardmäßig ein LinkedHashSet. Der Unterschied:
Set-Literal (LinkedHashSet): Durchläuft die Elemente in Einfügereihenfolge.HashSet: Gibt keine Reihenfolge-Garantie; die Durchlaufreihenfolge kann von der Einfügereihenfolge abweichen.
import 'dart:collection';
void main() {
final geordnet = {5, 3, 8}; // Immer in der Reihenfolge 5, 3, 8
final ungeordnet = HashSet<int>.from([5, 3, 8]); // Keine Reihenfolge-Garantie
print(geordnet);
print(ungeordnet);
}Im Alltag genügt fast immer ein Set-Literal, und seine vorhersehbare Reihenfolge erleichtert das Debuggen. Wählen Sie HashSet, wenn die Reihenfolge wirklich keine Rolle spielt und Sie das im Code ausdrücklich zeigen wollen.
Eigene Klassen im Set: == und hashCode
Ein Set entscheidet mit dem Operator == und hashCode, ob zwei Objekte „gleich“ sind. Standardmäßig gelten zwei getrennte Objekte als verschieden, selbst wenn ihre Felder identisch sind. Sollen Schüler mit derselben Nummer als gleich gelten, überschreiben Sie beides zusammen:
class Schueler {
final int nr;
final String name;
const Schueler(this.nr, this.name);
@override
bool operator ==(Object other) => other is Schueler && other.nr == nr;
@override
int get hashCode => nr.hashCode;
}
void main() {
final schueler = <Schueler>{};
schueler.add(const Schueler(123, 'Ahmet'));
schueler.add(const Schueler(124, 'Mehmet'));
schueler.add(const Schueler(123, 'Anderer Name')); // Gleiche Nummer: nicht hinzugefügt
print('Schüler insgesamt: ${schueler.length}'); // 2
}Die ==-Methode der früheren Fassung war als (other as Schueler).nr geschrieben. Das wirft einen TypeError, sobald mit etwas verglichen wird, das kein Schueler ist; == sollte aber nie werfen, sondern bei einem Objekt anderen Typs einfach false liefern. Das Muster other is Schueler && ... sorgt dafür.
Die Regel ist eindeutig: Sagt ==, dass zwei Objekte gleich sind, müssen auch ihre hashCode-Werte gleich sein. Vergleichen Sie mehrere Felder, können Sie Object.hash(nr, name) verwenden. Diese Gleichheit sollte nur auf Feldern beruhen, die sich nicht ändern (final); ein Objekt, dessen Hash sich nach dem Einfügen in ein Set ändert, wird nicht mehr gefunden.
Map: vom Schlüssel zum Wert
Eine Map verknüpft jeden Wert mit einem eindeutigen Schlüssel. Denken Sie an ein Mehrfamilienhaus: Jede Wohnung hat eine Nummer (Schlüssel) und jemanden, der darin wohnt (Wert).
void main() {
final bestand = {'apfel': 12, 'birne': 0}; // Map<String, int>
bestand['banane'] = 30; // Hinzufügen
bestand['apfel'] = 10; // Ändern
bestand.update('birne', (v) => v + 5); // Vorhandenen Wert ändern
bestand.putIfAbsent('kiwi', () => 8); // Nur hinzufügen, wenn nicht vorhanden
print(bestand['apfel']); // 10
print(bestand['ananas']); // null: Schlüssel fehlt
print(bestand['ananas'] ?? 0); // 0
print(bestand.containsKey('banane')); // true
bestand.remove('banane');
print(bestand); // {apfel: 10, birne: 5, kiwi: 8}
}Ein aus einer Map gelesener Wert ist immer nullable (int?), weil der Schlüssel fehlen kann. Statt ihn mit ! zu erzwingen, ist es sicherer, mit ?? einen Standardwert anzugeben oder vorher mit containsKey zu prüfen.
Eine Person über die Ausweisnummer finden
class Buerger {
final String ausweisNr;
final String vorname;
final String nachname;
const Buerger(this.ausweisNr, this.vorname, this.nachname);
}
void main() {
const personen = [
Buerger('12345678901', 'Ahmet', 'Balaman'),
Buerger('98765432109', 'Mehmet', 'Yilmaz'),
Buerger('55555555555', 'Ayse', 'Demir'),
];
// Die Ausweisnummer ist der Schlüssel, die Person der Wert
final buerger = {for (final p in personen) p.ausweisNr: p};
final gefunden = buerger['12345678901'];
print('${gefunden?.vorname} ${gefunden?.nachname}'); // Ahmet Balaman
}Die frühere Fassung speicherte die Ausweisnummer als int. Werte, die wie Zahlen aussehen, mit denen aber nie gerechnet wird (Ausweis-, Telefonnummern, Postleitzahlen), speichern Sie besser als String: Führende Nullen bleiben erhalten, Sie stoßen an keine Längengrenze, und niemand addiert versehentlich zwei Ausweisnummern. Die Schreibweise {for (...) schluessel: wert} beim Aufbau der Map ist das Map-Gegenstück zu collection for.
Eine Map durchlaufen
for (final MapEntry(key: nr, value: person) in buerger.entries) {
print('Ausweis: $nr');
print('Name: ${person.vorname} ${person.nachname}');
}Dank der Patterns aus Dart 3 können Sie jeden MapEntry gleich am Schleifenanfang in Schlüssel und Wert zerlegen. Das ist sauberer, als die Schlüssel zu durchlaufen und jedes Mal mit buerger[nr] einen nullable Wert zu lesen. Brauchen Sie nur die Werte, nehmen Sie buerger.values, nur die Schlüssel buerger.keys.
Map und HashMap
Die Unterscheidung vom Set gilt auch hier. Ein Literal {...} erzeugt eine LinkedHashMap und durchläuft die Schlüssel in Einfügereihenfolge; HashMap aus dart:collection gibt keine Reihenfolge-Garantie. Im Alltag genügt das Literal.
List, Set und Map im Vergleich
| Eigenschaft | List | Set | Map |
|---|---|---|---|
| Reihenfolge | Einfügereihenfolge | Bei Literalen Einfügereihenfolge, bei HashSet keine Garantie |
Bei Literalen Einfügereihenfolge, bei HashMap keine Garantie |
| Doppelte Werte | Erlaubt | Nicht erlaubt | Schlüssel nein, Werte ja |
| Zugriff | Über Index | Nur „enthalten?“ und Durchlaufen | Über Schlüssel |
| Kosten von „enthalten?“ | Prüft jedes Element | Im Schnitt konstante Zeit | Im Schnitt konstante Zeit für einen Schlüssel |
Die Wahl hängt meist weniger an der Geschwindigkeit als an der Frage „Wonach werde ich die Daten suchen?“:
- List: Alles, bei dem die Reihenfolge zählt. Warenkorb, Nachrichten, auf dem Bildschirm angezeigte Ergebnisse.
- Set: Alles, bei dem Duplikate keinen Sinn ergeben. Ausgewählte Tags, Favoriten-IDs, bereits gesehene Datensätze.
- Map: Alles, was über einen Schlüssel gesucht wird. Einstellungen, ein Cache nach ID, JSON-Daten.
Collections in Flutter
In Flutter wird List am häufigsten verwendet. Ein ListView erzeugt für jedes Element der Liste eine Zeile:
import 'package:flutter/material.dart';
class ProduktListe extends StatelessWidget {
const ProduktListe({super.key});
static const produkte = ['Apfel', 'Birne', 'Banane'];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: produkte.length,
itemBuilder: (context, index) => ListTile(title: Text(produkte[index])),
);
}
}Für den Auswahlzustand ist ein Set wie gemacht. Abzufragen, ob ein Element ausgewählt ist, und die Auswahl umzuschalten, braucht jeweils nur eine Zeile:
final ausgewaehlt = <String>{};
void auswahlUmschalten(String produkt) {
if (!ausgewaehlt.remove(produkt)) {
ausgewaehlt.add(produkt); // remove lieferte false, also war es nicht da: hinzufügen
}
}Das Ergebnis von jsonDecode kommt als Map<String, dynamic> an; beim Lesen der Felder gelten die Regeln aus dem Beitrag über Typumwandlungen.
Häufige Fehler
- Für alles eine List nehmen und sie immer wieder mit
containsdurchsuchen; für eindeutige, häufig gesuchte Daten ein Set wählen. {}für ein leeres Set schreiben; das erzeugt eine Map, verwenden Sie<T>{}.- Das Ergebnis von
sorteiner Variable zuweisen;sortändert die Liste an Ort und Stelle und liefertvoid. - In
==mitasarbeiten; beim Vergleich mit einem Objekt anderen Typs gibt es einen Fehler. ==überschreiben undhashCodevergessen; Set und Map finden das Objekt nicht.- An einen aus einer Map gelesenen Wert
!hängen; fehlt der Schlüssel, stürzt die App ab. firstauf einer möglicherweise leeren Liste verwenden;firstOrNullist die bessere Wahl.
Nächster Schritt
Beim Arbeiten mit Daten läuft nicht immer alles nach Plan: Eine Datei fehlt, JSON kommt in unerwarteter Form, die Netzwerkverbindung bricht ab. Als Nächstes folgt Fehlerbehandlung mit try-catch und async/await.
Ressourcen
Kontakt:
Häufig gestellte Fragen
Gibt es einen Unterschied zwischen HashSet und Set bzw. HashMap und Map?
Set und Map sind abstrakte Schnittstellen; das Literal {} erzeugt standardmäßig ein LinkedHashSet bzw. eine LinkedHashMap, die die Einfügereihenfolge beibehalten. HashSet und HashMap aus dart:collection geben dagegen keine Reihenfolge-Garantie. Im Flutter-Alltag reichen Set- und Map-Literale meist aus.
Warum müssen hashCode und operator == gemeinsam überschrieben werden?
Set und Map prüfen beim Suchen zuerst hashCode und bestätigen dann mit ==. Überschreiben Sie nur eines davon, können Objekte, die gleich sein sollten, als verschieden gelten. Die Regel in Dart ist eindeutig: Sagt ==, dass zwei Objekte gleich sind, muss auch hashCode gleich sein.
Warum liefert where ein Iterable statt einer List?
where gibt ein träges (lazy) Iterable zurück; die Elemente werden erst beim Durchlaufen berechnet. Für Indexzugriff oder um das Ergebnis an ListView.builder zu übergeben, rufen Sie .toList() auf.
Wie erzeuge ich ein leeres Set?
Schreiben Sie <String>{} oder Set<String>(). Ein untypisiertes {} erzeugt eine leere Map; var s = {}; und danach s.add(...) führt zu einem Kompilierfehler.
Verwandte Artikel
Flutter-Kamera ohne erzwungene Ausrichtung: drei Fehler und ihre Lösungen
Gedrehte Vorschau, Einfrieren beim Aufnahmestart, tote Kamera: drei Flutter-Kamerafehler aus Recapday und der Code, der sie behoben hat.
Den besten Flutter-Lehrer finden: Leitfaden für Mobile Apps
Flutter im Einzelunterricht, per Videokurs oder im Bootcamp? So prüfen Sie eine Lehrkraft in der Probestunde und lernen in sinnvoller Reihenfolge.
Fehlerbehandlung in Dart: try-catch und asynchrone Abläufe
Gezieltes try-catch mit on, rethrow, eigene Exceptions, Future, async/await, Timeouts, paralleles Warten und sicheres setState in Flutter mit Dart.