Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting
Mit Einblicken von Nova Trent (Junior Dev) und Jamal Hassan (Backend Developer)

Schwierigkeit: 🟢 Einsteiger | 🟡 Mittel
Lesezeit: 25 Minuten
Voraussetzungen: Java OOP abgeschlossen (Klassen, Vererbung, Interfaces, Exceptions)
Kurs: Java Erweiterte Techniken – Tag 1 von 10


📖 Java Erweiterte Techniken – Alle Tage

TagThemaLevel
→ 1Collections – Listen🟢
2Collections – Sets & Maps🟢
3Generics🟡
4Lambda-Ausdrücke🟡
5Functional Interfaces🟡
6Stream-API🟡
7File I/O🟡
8Annotations & Multithreading Basics🟡
9Multithreading – Synchronisation🔴
10Netzwerkprogrammierung🔴

📍 Du bist hier: Tag 1


⚡ Das Wichtigste in 30 Sekunden

Dein Problem: Arrays sind statisch. Du weißt vorher nicht immer, wie viele Elemente du brauchst. Elemente einfügen oder löschen? Ein Albtraum.

Die Lösung: Das Java Collections Framework – dynamische Datenstrukturen, die mit deinen Anforderungen wachsen.

Heute lernst du:

  • ✅ Warum Collections Arrays in den meisten Fällen überlegen sind
  • ✅ Wie ArrayList und LinkedList funktionieren – und wann du welche nutzt
  • ✅ Immutable Lists mit List.of() erstellen
  • ✅ Verschiedene Wege, durch Listen zu iterieren

Für wen ist dieser Artikel?

  • 🌱 Anfänger: Du lernst Collections von Grund auf
  • 🌿 Erfahrene: Du vertiefst Best Practices und Performance-Unterschiede
  • 🌳 Profis: Im Bonus findest du Big-O Analyse und Edge Cases

Zeit-Investment: 25 Minuten Lesen + 30-60 Minuten Praxis


👋 Elyndra: „Arrays haben mir auch mal gereicht…“

Hi! 👋

Elyndra hier. Ich erinnere mich noch gut an meine ersten Java-Projekte. Arrays waren meine besten Freunde. String[] namen = new String[10]; – simpel, direkt, fertig.

Bis ich eine Benutzerliste bauen musste, bei der User sich registrieren und abmelden konnten.

Kennst du das? Du deklarierst ein Array mit 10 Plätzen. Dann kommen 11 User. Oder du willst den dritten User löschen und musst alle nachfolgenden Elemente verschieben. Manuell. Mit einer Schleife.

Ngl, das war der Moment, als ich dachte: „Es muss doch einen besseren Weg geben.“

Gibt es. Und heute zeige ich dir genau diesen Weg.

Lass uns das gemeinsam angehen! 🚀


🖼️ Das Konzept auf einen Blick

Listen

Abbildung 1: Das komplette Java Collections Framework – Interfaces, Klassen und Legacy-Komponenten


🟢 GRUNDLAGEN

Was ist das Collections Framework?

Das Java Collections Framework ist eine Sammlung von Interfaces und Klassen, die dir dynamische Datenstrukturen bieten. Stell es dir vor wie einen Werkzeugkasten – statt nur einem Schraubenzieher (Array) hast du jetzt Schraubenzieher, Zangen, Hämmer und Sägen.

Die vier Haupttypen:

TypBeschreibungBeispiel
ListGeordnete Sammlung mit Index-ZugriffEinkaufsliste, Playlist
SetKeine Duplikate erlaubtUnique User-IDs, Tags
MapKey-Value-PaareTelefonbuch, Konfiguration
QueueFirst-In-First-OutWarteschlange, Task-Queue

Heute fokussieren wir uns auf List – die vielseitigste und am häufigsten verwendete Collection.


Modern vs. Legacy – Was sollst du verwenden?

💬 Nova fragt: „Ich hab in älterem Code Vector und Stack gesehen. Soll ich die auch lernen?“

Kurze Antwort: Nein. Diese Klassen sind Legacy aus Java 1.0 – also über 25 Jahre alt.

🟢 MODERN – Diese verwenden:

KlasseVerwendungWarum?
ArrayListStandard für ListenSchnell, flexibel, 90% aller Fälle
LinkedListWenn Deque-Operationen nötigaddFirst/addLast in O(1)
ArrayDequeStack oder QueueSchneller als Stack-Klasse
HashSetStandard für SetsO(1) für add/contains
HashMapStandard für MapsO(1) für put/get
TreeSet/TreeMapSortierte CollectionsAutomatische Sortierung

🔴 LEGACY – Diese vermeiden:

KlasseProblemAlternative
VectorSynchronized (langsam), veraltetArrayList + Collections.synchronizedList()
StackErbt von Vector, Design-FehlerArrayDeque
HashtableSynchronized (langsam), kein nullHashMap oder ConcurrentHashMap
EnumerationVeraltet, weniger MethodenIterator

Warum sind die Legacy-Klassen langsamer?

Sie sind synchronized – jede Operation ist thread-safe, ob du es brauchst oder nicht. Das kostet Performance. In 95% der Fälle brauchst du keine Thread-Sicherheit, und wenn doch, gibt es bessere Lösungen wie ConcurrentHashMap.

💬 Jamal: „Real talk: Wenn du in Production-Code Vector oder Stack siehst, ist das ein Zeichen dass der Code alt ist. Bei Refactoring würde ich die ersetzen.“


Warum brauche ich Collections statt Arrays?

💬 Nova fragt: „Elyndra, ehrlich – ich hab bisher immer Arrays benutzt. Warum soll ich das ändern?“

Gute Frage, Nova. Lass mich dir zeigen, warum:

Das Array-Problem:

// Array mit fester Größe
String[] teilnehmer = new String[3];
teilnehmer[0] = "Anna";
teilnehmer[1] = "Ben";
teilnehmer[2] = "Clara";

// Jetzt will sich "David" anmelden...
// Upps. Array ist voll. Was jetzt?

// Option 1: Neues, größeres Array erstellen
String[] neuesTeilnehmer = new String[4];
System.arraycopy(teilnehmer, 0, neuesTeilnehmer, 0, 3);
neuesTeilnehmer[3] = "David";
teilnehmer = neuesTeilnehmer;

// Das ist... mühsam. 😅

Die Collections-Lösung:

// ArrayList wächst automatisch
List<String> teilnehmer = new ArrayList<>();
teilnehmer.add("Anna");
teilnehmer.add("Ben");
teilnehmer.add("Clara");
teilnehmer.add("David");  // Einfach hinzufügen. Fertig.

// Jemand verlässt den Kurs?
teilnehmer.remove("Ben");  // Eine Zeile. Done.

Was macht dieser Code?

Die ArrayList kümmert sich intern um das Größenmanagement. Du fügst Elemente hinzu, entfernst sie – die Liste passt sich automatisch an.

Der Vergleich auf einen Blick:

EigenschaftArrayArrayList
GrößeFest bei ErstellungDynamisch
Element hinzufügenManuelles Kopierenadd()
Element entfernenManuelles Verschiebenremove()
SuchenSchleife schreibencontains(), indexOf()
Primitive Typen✅ Direkt❌ Nur Wrapper (Integer, Double…)

💬 Nova: „Oh Mann! Warum hat mir das niemand früher gesagt?!“

Keine Sorge – jeder hat mal mit Arrays angefangen. Das ist völlig normal.


Das Collection Interface

Bevor wir tiefer in Listen eintauchen, ein kurzer Blick auf die gemeinsame Basis: das Collection Interface.

public interface Collection<E> extends Iterable<E> {
    // Größe und Status
    int size();
    boolean isEmpty();
    
    // Elemente hinzufügen/entfernen
    boolean add(E element);
    boolean remove(Object o);
    void clear();
    
    // Suchen
    boolean contains(Object o);
    
    // Konvertierung
    Object[] toArray();
    <T> T[] toArray(T[] a);
    
    // Bulk-Operationen
    boolean addAll(Collection<? extends E> c);
    boolean removeAll(Collection<?> c);
    boolean retainAll(Collection<?> c);
}

Was macht dieser Code?

Das Collection Interface definiert die Grundoperationen, die ALLE Collections können – egal ob List, Set oder Queue. Das <E> ist ein Generic – ein Platzhalter für den Typ, den du speichern willst. Dazu mehr an Tag 3!

Wichtig zu verstehen:

Das E steht für „Element“ – eine Konvention. Wenn du List<String> schreibst, wird E zu String. Bei List<Integer> wird E zu Integer. So weiß der Compiler, welche Typen erlaubt sind.


Das List Interface

List erweitert Collection um Index-basierte Operationen:

public interface List<E> extends Collection<E> {
    // Index-Zugriff
    E get(int index);
    E set(int index, E element);
    
    // Position-basiertes Einfügen/Entfernen
    void add(int index, E element);
    E remove(int index);
    
    // Suche mit Index
    int indexOf(Object o);
    int lastIndexOf(Object o);
    
    // Sublist
    List<E> subList(int fromIndex, int toIndex);
    
    // Spezielle Iteratoren
    ListIterator<E> listIterator();
    ListIterator<E> listIterator(int index);
}

Das Besondere an Listen:

Listen sind geordnet – die Reihenfolge, in der du Elemente hinzufügst, bleibt erhalten. Und du kannst über den Index auf jedes Element zugreifen, genau wie bei Arrays.


Deine erste ArrayList

Zeit für Praxis! Die ArrayList ist die am häufigsten verwendete List-Implementierung.

import java.util.ArrayList;
import java.util.List;

public class MeineErsteListe {
    public static void main(String[] args) {
        // Liste erstellen
        List<String> einkaufsliste = new ArrayList<>();
        
        // Elemente hinzufügen
        einkaufsliste.add("Milch");
        einkaufsliste.add("Brot");
        einkaufsliste.add("Käse");
        einkaufsliste.add("Äpfel");
        
        // Liste ausgeben
        System.out.println("Einkaufsliste: " + einkaufsliste);
        // Ausgabe: Einkaufsliste: [Milch, Brot, Käse, Äpfel]
        
        // Größe abfragen
        System.out.println("Anzahl: " + einkaufsliste.size());
        // Ausgabe: Anzahl: 4
        
        // Element an Position 1 abrufen (0-basiert!)
        System.out.println("Zweites Element: " + einkaufsliste.get(1));
        // Ausgabe: Zweites Element: Brot
        
        // Prüfen ob Element vorhanden
        System.out.println("Haben wir Milch? " + einkaufsliste.contains("Milch"));
        // Ausgabe: Haben wir Milch? true
        
        // Element entfernen
        einkaufsliste.remove("Brot");
        System.out.println("Nach Entfernen: " + einkaufsliste);
        // Ausgabe: Nach Entfernen: [Milch, Käse, Äpfel]
        
        // Element an bestimmter Position einfügen
        einkaufsliste.add(1, "Butter");
        System.out.println("Mit Butter: " + einkaufsliste);
        // Ausgabe: Mit Butter: [Milch, Butter, Käse, Äpfel]
    }
}

Was macht dieser Code?

Wir erstellen eine ArrayList für Strings und führen die wichtigsten Operationen durch: hinzufügen, abrufen, prüfen, entfernen, einfügen.

Wie funktioniert das im Detail?

Die Deklaration: List<String> einkaufsliste = new ArrayList<>();

Hier passiert etwas Wichtiges: Wir deklarieren die Variable als List<String> (Interface), erstellen aber ein ArrayList-Objekt (Implementierung).

Warum nicht einfach ArrayList<String> einkaufsliste = new ArrayList<>();?

Das ist einer der wichtigsten OOP-Grundsätze: Programmiere gegen das Interface, nicht die Implementierung.

💬 Nova fragt: „Häh?! Was bringt mir das? Funktioniert doch beides?!“

Stell dir vor, du hast eine Methode:

// ❌ Mit konkreter Klasse - unflexibel
public void verarbeite(ArrayList<String> daten) { ... }

// ✅ Mit Interface - flexibel
public void verarbeite(List<String> daten) { ... }

Die zweite Variante akzeptiert jede List-Implementierung: ArrayList, LinkedList, oder sogar deine eigene Custom-List. Du bindest dich nicht an eine konkrete Implementierung.

Praktisches Beispiel:

List<String> namen = new ArrayList<>();  // Heute: ArrayList

// Später merkst du: LinkedList wäre besser für dein Problem
List<String> namen = new LinkedList<>();  // Nur EINE Zeile ändern!

// Der ganze REST deines Codes funktioniert weiter!
// Alle Methoden die List<String> erwarten, funktionieren mit beiden.

💬 Jamal: „In Production hab ich das oft erlebt: Projekt startet mit ArrayList, dann merkt jemand dass eine Queue besser wäre. Wer gegen das Interface programmiert hat, ändert eine Zeile. Wer ArrayList überall hingeschrieben hat, refactored zwei Tage.“

Das ist Polymorphismus in Aktion – ein Kernkonzept aus der OOP.

Der Diamond-Operator: <>

Seit Java 7 musst du den Typ rechts nicht wiederholen. Der Compiler leitet ihn von links ab. Statt new ArrayList<String>() reicht new ArrayList<>().

Die Methoden:

  • add(element) – Fügt am Ende hinzu
  • add(index, element) – Fügt an Position ein, verschiebt Rest nach rechts
  • get(index) – Gibt Element an Position zurück (0-basiert!)
  • remove(element) – Entfernt erstes Vorkommen
  • remove(index) – Entfernt Element an Position
  • contains(element) – Prüft ob vorhanden
  • size() – Gibt Anzahl zurück

In der Praxis bedeutet das:

Du kannst Listen fast wie Arrays verwenden – aber mit eingebauter Flexibilität. Kein manuelles Größenmanagement mehr!

💡 Neu hier? Was ist ein Interface?

Ein Interface ist wie ein Vertrag. Es sagt: „Jede Klasse, die mich implementiert, MUSS diese Methoden haben.“ List ist das Interface, ArrayList und LinkedList sind Implementierungen, die diesen Vertrag erfüllen.

Beispiel: Wenn eine Methode List<String> erwartet, kannst du JEDE List-Implementierung übergeben.

→ Mehr zu Interfaces in unserem OOP-Kurs


ArrayList vs. LinkedList – Wann was?

Java bietet zwei Haupt-Implementierungen von List. Die Wahl hat echte Performance-Auswirkungen.

ArrayList – Das dynamische Array

List<String> arrayList = new ArrayList<>();

Wie funktioniert ArrayList intern?

Eine ArrayList ist im Kern ein Array, das sich automatisch vergrößert. Wenn du Elemente hinzufügst und das interne Array voll ist, erstellt Java ein neues, größeres Array und kopiert alle Elemente um.

Abbildung 2: ArrayList vergrößert ihr internes Array automatisch

Stärken:

  • ✅ Schneller Zugriff per Index: get(5) ist sofort (O(1))
  • ✅ Speichereffizient (keine Overhead pro Element)
  • ✅ Cache-freundlich (zusammenhängender Speicher)

Schwächen:

  • ❌ Einfügen/Entfernen in der Mitte: Alle folgenden Elemente müssen verschoben werden
  • ❌ Vergrößerung: Komplettes Kopieren nötig

LinkedList – Die verkettete Liste

List<String> linkedList = new LinkedList<>();

Wie funktioniert LinkedList intern?

Eine LinkedList besteht aus Knoten (Nodes), die über Referenzen verbunden sind. Jeder Knoten kennt seinen Vorgänger und Nachfolger.

Abbildung 3: LinkedList als doppelt verkettete Knotenstruktur

Stärken:

  • ✅ Schnelles Einfügen/Entfernen am Anfang/Ende: O(1)
  • ✅ Kein Kopieren bei Größenänderung

Schwächen:

  • ❌ Langsamer Index-Zugriff: get(500) muss 500 Knoten durchlaufen (O(n))
  • ❌ Mehr Speicher pro Element (Referenzen zu prev/next)

Die Entscheidungshilfe

SzenarioBeste WahlWarum?
Häufiger Zugriff per IndexArrayListO(1) vs O(n)
Hauptsächlich am Ende hinzufügenArrayListAmortisiert O(1), cache-freundlich
Häufiges Einfügen/Entfernen am AnfangLinkedListO(1) vs O(n)
Häufiges Einfügen/Entfernen in der MitteKommt drauf anBei bekannter Position: LinkedList
Speicher ist kritischArrayListWeniger Overhead
Als Queue/Deque nutzenLinkedListImplementiert Deque Interface

💬 Jamal: „Real talk: In 90% der Fälle ist ArrayList die richtige Wahl. Ich hab in drei Jahren Production-Code vielleicht fünfmal bewusst LinkedList gewählt. Meistens als Queue.“


Immutable Lists mit List.of()

Seit Java 9 gibt es eine elegante Möglichkeit, unveränderliche Listen zu erstellen:

// Immutable List erstellen
List<String> wochentage = List.of("Montag", "Dienstag", "Mittwoch", 
                                   "Donnerstag", "Freitag");

// Das geht:
String tag = wochentage.get(2);  // "Mittwoch"
int anzahl = wochentage.size();  // 5

// Das geht NICHT:
wochentage.add("Samstag");     // UnsupportedOperationException!
wochentage.remove("Montag");   // UnsupportedOperationException!
wochentage.set(0, "Monday");   // UnsupportedOperationException!

Was macht dieser Code?

List.of() erstellt eine unveränderliche (immutable) Liste. Nach der Erstellung kannst du sie nur lesen, nicht mehr verändern.

Warum ist das nützlich?

  1. Thread-Sicherheit: Immutable Objects können bedenkenlos zwischen Threads geteilt werden
  2. Sicherheit: Keine versehentlichen Änderungen möglich
  3. Klarheit: Der Code zeigt: „Diese Liste ist fix“

Wichtig zu verstehen:

List.of() erlaubt auch keine null-Werte! Das ist anders als bei ArrayList:

// Das funktioniert:
List<String> mitNull = new ArrayList<>();
mitNull.add(null);  // OK

// Das nicht:
List<String> ohneNull = List.of("A", null, "B");  // NullPointerException!

💬 Nova: „Moment, Moment… warum sollte ich absichtlich eine Liste wollen, die ich nicht ändern kann?“

Gute Frage! Stell dir vor, du hast eine Konfiguration oder Konstanten. Du willst sicher sein, dass niemand – auch nicht versehentlich – diese Werte ändert. Oder du gibst eine Liste aus einer Methode zurück und willst garantieren, dass der Aufrufer sie nicht manipuliert.


Durch Listen iterieren

Es gibt mehrere Wege, durch eine Liste zu gehen. Jeder hat seinen Platz.

1. For-Each Loop (empfohlen für die meisten Fälle)

List<String> namen = List.of("Anna", "Ben", "Clara");

for (String name : namen) {
    System.out.println("Hallo, " + name + "!");
}

Vorteile: Sauber, lesbar, wenig Fehleranfällig
Nachteile: Kein Zugriff auf den Index, keine Modifikation während Iteration


2. Klassische For-Schleife (wenn du den Index brauchst)

List<String> namen = new ArrayList<>(List.of("Anna", "Ben", "Clara"));

for (int i = 0; i < namen.size(); i++) {
    System.out.println((i + 1) + ". " + namen.get(i));
}
// Ausgabe:
// 1. Anna
// 2. Ben
// 3. Clara

Vorteile: Zugriff auf Index
Nachteile: Mehr Code, leicht Off-by-One-Fehler


3. Iterator (wenn du während der Iteration entfernen willst)

List<String> namen = new ArrayList<>(List.of("Anna", "Ben", "Clara", "Bob"));

Iterator<String> iterator = namen.iterator();
while (iterator.hasNext()) {
    String name = iterator.next();
    if (name.startsWith("B")) {
        iterator.remove();  // Sicher entfernen während Iteration!
    }
}
System.out.println(namen);  // [Anna, Clara]

Vorteile: Sicheres Entfernen während Iteration
Nachteile: Etwas mehr Boilerplate

⚠️ Wichtig: Wenn du mit for-each iterierst und gleichzeitig list.remove() aufrufst, bekommst du eine ConcurrentModificationException! Der Iterator ist der sichere Weg.


4. ListIterator (für bidirektionale Iteration)

List<String> namen = new ArrayList<>(List.of("Anna", "Ben", "Clara"));

ListIterator<String> listIterator = namen.listIterator(namen.size()); // Start am Ende

// Rückwärts iterieren
while (listIterator.hasPrevious()) {
    System.out.println(listIterator.previous());
}
// Ausgabe:
// Clara
// Ben
// Anna

Vorteile: Vorwärts UND rückwärts, Zugriff auf Index, Ersetzen möglich
Nachteile: Selten nötig


Wrapper-Klassen – Primitive in Collections

Collections können keine primitiven Typen speichern – nur Objekte. Für int, double, boolean usw. gibt es Wrapper-Klassen:

PrimitivWrapper-Klasse
intInteger
doubleDouble
booleanBoolean
charCharacter
longLong
floatFloat
byteByte
shortShort

Autoboxing und Unboxing:

Java konvertiert automatisch zwischen primitiven Typen und ihren Wrappern:

List<Integer> zahlen = new ArrayList<>();

// Autoboxing: int → Integer
zahlen.add(42);        // Java macht daraus: zahlen.add(Integer.valueOf(42))
zahlen.add(17);

// Unboxing: Integer → int
int erste = zahlen.get(0);  // Java macht daraus: zahlen.get(0).intValue()

System.out.println("Summe: " + (zahlen.get(0) + zahlen.get(1)));  // 59

Was passiert hier wirklich?

Wenn du zahlen.add(42) schreibst, ruft Java im Hintergrund Integer.valueOf(42) auf, um den primitiven int in ein Integer-Objekt zu verpacken. Beim Auslesen passiert das Gegenteil.

Vorsicht bei null:

List<Integer> zahlen = new ArrayList<>();
zahlen.add(null);  // Erlaubt!

int wert = zahlen.get(0);  // NullPointerException! 
// Weil: null.intValue() geht nicht

💬 Jamal: „Das ist ein klassischer Bug in Production. Immer auf null prüfen, wenn du mit Wrapper-Klassen arbeitest. Oder gleich OptionalInt verwenden – dazu später mehr.“


🟡 PROFESSIONALS

💡 Du hast die Grundlagen drauf? Hier geht’s um Best Practices und Praxis-Tipps.

Best Practices

1. Programmiere gegen das Interface

// ✅ GUT: Interface als Typ
List<String> namen = new ArrayList<>();

// ❌ VERMEIDEN: Konkrete Klasse als Typ
ArrayList<String> namen = new ArrayList<>();

Warum? Wenn du später auf LinkedList wechseln willst, musst du nur die rechte Seite ändern. Alle Methoden, die List<String> erwarten, funktionieren weiterhin.


2. Initiale Kapazität bei bekannter Größe

// Wenn du ungefähr weißt, wie viele Elemente kommen:
List<String> grosseListe = new ArrayList<>(10000);

// Statt:
List<String> grosseListe = new ArrayList<>();  // Startet mit Kapazität 10

Warum? Jede Vergrößerung der ArrayList bedeutet: neues Array erstellen, alles kopieren. Bei 10.000 Elementen mit Standardkapazität passiert das mehrmals.

💬 Jamal: „In einem Projekt hatten wir einen Import-Job, der 50.000 Datensätze lud. Einfach new ArrayList<>(50000) statt new ArrayList<>() hat die Laufzeit um 30% reduziert. Lowkey ein Game-Changer.“


3. Verwende List.of() für Konstanten

// ✅ GUT: Immutable für Konstanten
private static final List<String> ERLAUBTE_STATUS = 
    List.of("AKTIV", "PAUSIERT", "BEENDET");

// ❌ VERMEIDEN: Mutable für Konstanten
private static final List<String> ERLAUBTE_STATUS = 
    new ArrayList<>(Arrays.asList("AKTIV", "PAUSIERT", "BEENDET"));
// Jemand könnte versehentlich ERLAUBTE_STATUS.add("GELOESCHT") aufrufen!

4. Defensive Kopien erstellen

Wenn du eine Liste aus einer Methode zurückgibst und nicht willst, dass der Aufrufer deine interne Liste verändert:

public class Kurs {
    private List<String> teilnehmer = new ArrayList<>();
    
    // ✅ GUT: Defensive Kopie
    public List<String> getTeilnehmer() {
        return List.copyOf(teilnehmer);  // Immutable Kopie
    }
    
    // ❌ GEFÄHRLICH: Direkte Referenz
    public List<String> getTeilnehmerUnsafe() {
        return teilnehmer;  // Aufrufer kann DEINE Liste ändern!
    }
}

Häufige Patterns

Pattern 1: Liste filtern

List<Integer> zahlen = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));

// Alle geraden Zahlen entfernen (mit Iterator)
Iterator<Integer> iter = zahlen.iterator();
while (iter.hasNext()) {
    if (iter.next() % 2 == 0) {
        iter.remove();
    }
}
// zahlen: [1, 3, 5, 7, 9]

// Oder eleganter mit removeIf (seit Java 8):
zahlen.removeIf(n -> n % 2 == 0);

Pattern 2: Liste transformieren

List<String> namen = List.of("anna", "ben", "clara");

// Neue Liste mit transformierten Werten
List<String> grossNamen = new ArrayList<>();
for (String name : namen) {
    grossNamen.add(name.toUpperCase());
}
// grossNamen: [ANNA, BEN, CLARA]

// Mit Streams (ab Tag 6 mehr dazu):
List<String> grossNamen = namen.stream()
    .map(String::toUpperCase)
    .toList();

Pattern 3: Null-sichere Operationen

public void verarbeite(List<String> eingabe) {
    // ✅ Null-Check am Anfang
    if (eingabe == null || eingabe.isEmpty()) {
        return;
    }
    
    // Jetzt sicher arbeiten
    for (String item : eingabe) {
        // ...
    }
}

// Oder mit Objects.requireNonNullElse (Java 9+):
List<String> sicher = Objects.requireNonNullElse(eingabe, List.of());

Stolperfallen vermeiden

Stolperfalle 1: ConcurrentModificationException

// ❌ DAS GEHT SCHIEF:
List<String> namen = new ArrayList<>(List.of("Anna", "Ben", "Bob", "Clara"));
for (String name : namen) {
    if (name.startsWith("B")) {
        namen.remove(name);  // BOOM! ConcurrentModificationException
    }
}

// ✅ RICHTIG: Mit Iterator
Iterator<String> iter = namen.iterator();
while (iter.hasNext()) {
    if (iter.next().startsWith("B")) {
        iter.remove();  // OK!
    }
}

// ✅ ODER: Mit removeIf
namen.removeIf(name -> name.startsWith("B"));

Stolperfalle 2: remove() mit Index vs. Objekt bei Integer-Listen

List<Integer> zahlen = new ArrayList<>(List.of(10, 20, 30, 40));

zahlen.remove(1);      // Entfernt Element an INDEX 1 → [10, 30, 40]
zahlen.remove((Integer) 30);  // Entfernt das OBJEKT 30 → [10, 40]

Bei Integer-Listen ist remove(1) mehrdeutig! Java wählt die spezifischere Methode – und das ist remove(int index). Wenn du ein Integer-Objekt entfernen willst, musst du casten.


Stolperfalle 3: subList() erstellt keine Kopie

List<String> original = new ArrayList<>(List.of("A", "B", "C", "D", "E"));
List<String> sub = original.subList(1, 4);  // [B, C, D]

sub.set(0, "X");  // Ändert auch original!
System.out.println(original);  // [A, X, C, D, E]

original.add("F");  // Ändert Struktur
sub.get(0);  // ConcurrentModificationException!

subList() erstellt eine View auf die Original-Liste, keine Kopie. Änderungen in der Subliste wirken sich auf das Original aus – und strukturelle Änderungen am Original machen die Subliste kaputt.


🔵 BONUS

💡 Für Neugierige und Profis: Performance-Details, Edge Cases und fortgeschrittene Patterns.

Big-O Analyse

OperationArrayListLinkedList
get(index)O(1)O(n)
add(element) (am Ende)O(1) amortisiertO(1)
add(index, element) (am Anfang)O(n)O(1)
add(index, element) (in der Mitte)O(n)O(n)*
remove(index)O(n)O(n)*
contains(element)O(n)O(n)
iterator.remove()O(n)O(1)

*LinkedList ist O(1) für die eigentliche Operation, aber O(n) um die Position zu finden.

Was bedeutet das praktisch?

Bei einer Liste mit 1 Million Elementen:

  • ArrayList.get(500000) → Sofort (ein Speicherzugriff)
  • LinkedList.get(500000) → 500.000 Sprünge durch Referenzen

💬 Jamal: „Ich hab mal einen Junior-Code reviewed, der in einer Schleife linkedList.get(i) aufgerufen hat. Bei 10.000 Elementen: 5 Sekunden Laufzeit. Nach Umstellung auf ArrayList: 2 Millisekunden. Performance matters.“


Memory Layout

Die internen Strukturen von ArrayList und LinkedList unterscheiden sich fundamental:

ArrayList: Zusammenhängender Speicherbereich – das macht sie cache-freundlich. Moderne CPUs laden Daten in Cache-Lines (typisch 64 Bytes). Bei ArrayList liegen die Referenzen nebeneinander, mehrere passen in eine Cache-Line.

LinkedList: Jeder Node liegt irgendwo im Heap. Beim Durchlaufen springt die CPU ständig zu verschiedenen Speicheradressen – das verursacht Cache-Misses und kostet Performance.

💡 Siehe Abbildungen 2 und 3 oben für die visuelle Darstellung.


Wann LinkedList tatsächlich schneller ist

Ein echtes Beispiel, wo LinkedList gewinnt:

// Szenario: Playlist, bei der oft am Anfang eingefügt wird
public class Playlist {
    private final Deque<String> songs = new LinkedList<>();
    
    // O(1) bei LinkedList, O(n) bei ArrayList
    public void alsNaechstesSpielen(String song) {
        songs.addFirst(song);
    }
    
    // O(1) bei LinkedList
    public String naechsterSong() {
        return songs.pollFirst();
    }
}

Wenn du als Queue/Deque nutzt (FIFO/LIFO-Operationen am Anfang/Ende), ist LinkedList die bessere Wahl.


List.copyOf() vs. Konstruktor-Kopie

List<String> original = new ArrayList<>(List.of("A", "B", "C"));

// Methode 1: Konstruktor-Kopie (mutable)
List<String> kopie1 = new ArrayList<>(original);
kopie1.add("D");  // OK

// Methode 2: List.copyOf() (immutable, Java 10+)
List<String> kopie2 = List.copyOf(original);
kopie2.add("D");  // UnsupportedOperationException!

// Wichtig: Beide sind UNABHÄNGIG vom Original
original.add("X");
System.out.println(kopie1);  // [A, B, C, D] - unverändert
System.out.println(kopie2);  // [A, B, C] - unverändert

Edge Case: equals() und hashCode() bei eigenen Objekten

Wenn du eigene Objekte in Listen speicherst und contains(), remove() oder indexOf() nutzen willst, MUSST du equals() überschreiben:

public class Produkt {
    private String id;
    private String name;
    
    // Konstruktor, Getter...
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Produkt produkt = (Produkt) o;
        return Objects.equals(id, produkt.id);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

Ohne equals() vergleicht Java nur Referenzen – zwei Produkte mit gleicher ID wären dann „ungleich“, wenn sie verschiedene Objekte sind.


💬 Real Talk: Die Sache mit ArrayList und LinkedList

Büro von Java Fleet, 15:30 Uhr. Nova sitzt mit gerunzelter Stirn vor ihrem Monitor.


Nova: „Elyndra, ich hab da was gelesen… LinkedList soll für Einfügen schneller sein. Soll ich die jetzt überall nutzen?“

Elyndra: „Lass mich raten – Stack Overflow?“

Nova: „…Ja. 😅“

Jamal: (dreht sich von seinem Schreibtisch um) „Die Antwort ist wahrscheinlich von 2008. Damals war das anders.“

Elyndra: „Jamal hat recht. Die Theorie sagt: LinkedList ist O(1) für Einfügen am Anfang. Aber die Praxis…“

Jamal: „In der Praxis verlierst du bei LinkedList durch Cache-Misses. Moderne CPUs sind optimiert für zusammenhängende Speicherbereiche. ArrayList liegt zusammenhängend im Speicher. LinkedList ist überall verstreut.“

Nova: „Okay, aber wenn ich ständig am Anfang einfüge?“

Elyndra: „Dann kann LinkedList schneller sein. Aber frag dich: Wie oft passiert das wirklich? In den meisten Anwendungen fügst du am Ende hinzu und iterierst durch.“

Jamal: „Real talk: Ich hab in drei Jahren Production-Code vielleicht fünfmal bewusst LinkedList gewählt. Und das war als Queue, nicht als List.“

Nova: „Also ArrayList als Default?“

Elyndra: „Genau. ArrayList als Default, LinkedList wenn du einen spezifischen Grund hast – und den kannst du benennen.“

Nova: „Das ist ja eigentlich simpler als ich dachte!“

Jamal: (lächelt kurz) „Die einfachen Antworten sind meistens die richtigen.“


💡 Praxis-Tipps

Für Einsteiger 🌱

  1. Starte immer mit ArrayList: In 90% der Fälle ist sie die richtige Wahl
  2. Nutze das Interface als Typ: List<String> statt ArrayList<String>
  3. Vergiss size() nicht: Listen sind 0-indiziert, das letzte Element ist bei size() - 1

Für den Alltag 🌿

  1. Setze initiale Kapazität: Bei bekannter ungefährer Größe new ArrayList<>(expectedSize)
  2. Nutze removeIf() statt Iterator: Sauberer und weniger fehleranfällig
  3. Defensive Kopien: Wenn du interne Listen nach außen gibst, nutze List.copyOf()

Für Profis 🌳

  1. Benchmark vor Optimierung: Vermutungen über Performance sind oft falsch
  2. Consider Arrays.asList() für feste Listen: Erstellt eine Fixed-Size-Liste backed by Array
  3. Für Thread-Sicherheit: Collections.synchronizedList() oder CopyOnWriteArrayList

🛠️ Tools & Ressourcen

Für Einsteiger 🌱

ToolWarum?Link
JShellSchnell Collections ausprobierenIn JDK enthalten
Oracle Java TutorialOffizielle Grundlagendocs.oracle.com

Für den Alltag 🌿

ToolWarum?Link
IntelliJ IDEAZeigt Performance-Warnungenjetbrains.com
BaeldungPraktische Java-Artikelbaeldung.com

Für Profis 🌳

ToolWarum?Link
JMHMicro-Benchmarkingopenjdk.org/projects/code-tools/jmh
Java AlmanacAPI-Vergleich zwischen Versionenjavaalmanac.io

❓ FAQ (Häufige Fragen)

Frage 1: Muss ich immer den Typ angeben bei List<String>?

Antwort: Ja, seit Java 5 sind Generics Standard. Ohne Typangabe (List statt List<String>) bekommst du eine „raw type“ Warnung. Der Compiler kann dann keine Typfehler erkennen. Immer den Typ angeben!

Frage 2: Was ist der Unterschied zwischen List.of() und Arrays.asList()?

Antwort: List.of() (Java 9+) erstellt eine komplett immutable Liste – keine Änderungen möglich, kein null erlaubt. Arrays.asList() erstellt eine Liste mit fester Größe – set() funktioniert, aber add() und remove() nicht. Außerdem ist sie backed by the array, Änderungen wirken sich aus.

Frage 3: Kann ich primitive Typen in Listen speichern?

Antwort: Nicht direkt. Du brauchst Wrapper-Klassen: List<Integer> statt List<int>. Java macht Autoboxing, aber sei vorsichtig mit null-Werten – die können bei Unboxing zu NullPointerException führen.

Frage 4: Wie sortiere ich eine Liste?

Antwort: Mit Collections.sort(list) oder seit Java 8 mit list.sort(comparator). Für natürliche Sortierung: list.sort(Comparator.naturalOrder()). Für eigene Sortierung: list.sort(Comparator.comparing(Person::getName)).

Frage 5: ArrayList oder Vector?

Antwort: ArrayList. Vector ist legacy und synchronized, was in den meisten Fällen unnötig und langsamer ist. Wenn du Thread-Sicherheit brauchst, nutze Collections.synchronizedList() oder CopyOnWriteArrayList.

Frage 6: Gibt es Listen mit primitiven Typen ohne Wrapper-Overhead?

Antwort: Nicht in der Standard-Bibliothek. Aber Libraries wie Eclipse Collections oder Trove bieten primitive Collections. Für die meisten Anwendungen ist der Wrapper-Overhead aber vernachlässigbar.

Frage 7: Wer entscheidet eigentlich, welche List-Implementierung bei Java Fleet verwendet wird? 🤔

Antwort: Gute Frage! Meistens Elyndra oder Jamal – die haben ein Auge für Performance. Aber manchmal, ganz selten, taucht ein mysteriöser Commit auf mit einem Kommentar, der alles auf den Punkt bringt. Die Signatur? Nur ein „B.“ …Niemand weiß, wer das ist. Wenn du mehr über die Geheimnisse von Java Fleet erfahren willst, such mal nach „behind the code“ oder „in my feels“. 😉

Frage 8: Warum soll ich List als Typ nutzen statt ArrayList?

Antwort: Das Liskov Substitution Principle! Wenn du gegen das Interface programmierst, kannst du die Implementierung austauschen, ohne den Rest des Codes zu ändern. Methoden, die List<String> akzeptieren, funktionieren mit JEDER List-Implementierung.


🎁 Cheat Sheet

🟢 Basics (Zum Nachschlagen)

// Liste erstellen
List<String> liste = new ArrayList<>();
List<String> immutable = List.of("A", "B", "C");

// Grundoperationen
liste.add("Element");           // Am Ende hinzufügen
liste.add(0, "Erstes");         // An Position einfügen
liste.get(0);                   // Element abrufen
liste.set(0, "Neu");            // Element ersetzen
liste.remove("Element");        // Nach Wert entfernen
liste.remove(0);                // Nach Index entfernen
liste.size();                   // Größe
liste.isEmpty();                // Leer?
liste.contains("X");            // Enthält?
liste.indexOf("X");             // Position finden
liste.clear();                  // Alles löschen

🟡 Patterns (Für den Alltag)

// Iteration
for (String s : liste) { }                    // For-each
liste.forEach(s -> System.out.println(s));    // Lambda
liste.forEach(System.out::println);           // Method Reference

// Filtern
liste.removeIf(s -> s.startsWith("X"));

// Sortieren
liste.sort(Comparator.naturalOrder());
liste.sort(Comparator.comparing(String::length));

// Kopieren
List<String> kopie = new ArrayList<>(liste);  // Mutable Kopie
List<String> kopie = List.copyOf(liste);      // Immutable Kopie

🔵 Advanced (Für Profis)

// Initiale Kapazität
List<String> gross = new ArrayList<>(10000);

// Thread-sicher
List<String> sync = Collections.synchronizedList(new ArrayList<>());

// Sublist (Vorsicht: View, keine Kopie!)
List<String> teil = liste.subList(1, 4);

// In Array konvertieren
String[] array = liste.toArray(new String[0]);
String[] array = liste.toArray(String[]::new);  // Java 11+

🎨 Challenge für dich!

Wähle dein Level:

🟢 Level 1 – Einsteiger

  • [ ] Erstelle eine ToDo-Liste mit ArrayList
  • [ ] Implementiere Hinzufügen, Anzeigen, Entfernen
  • [ ] Speichere die erledigten Tasks in einer zweiten Liste

Geschätzte Zeit: 15-30 Minuten

🟡 Level 2 – Fortgeschritten

  • [ ] Erweitere die ToDo-Liste um Prioritäten (HIGH, MEDIUM, LOW)
  • [ ] Sortiere die Liste nach Priorität
  • [ ] Implementiere eine Undo-Funktion mit einer zweiten Liste

Geschätzte Zeit: 30-60 Minuten

🔵 Level 3 – Profi

  • [ ] Implementiere eine eigene SimpleList-Klasse (ohne ArrayList zu nutzen)
  • [ ] Unterstütze: add, get, remove, size, contains
  • [ ] Schreibe Unit-Tests für alle Methoden
  • [ ] Bonus: Implementiere das Iterable-Interface

Geschätzte Zeit: 1-2 Stunden

Teile dein Ergebnis! 🎉

Java Erweiterte Techniken - Tag 1

Listen – Upgrade von Arrays

Frage 1 von 10

Was ist der Hauptvorteil von ArrayList gegenüber Arrays?

Frage 2 von 10

Welches Interface implementieren ArrayList und LinkedList?

Frage 3 von 10

Wann sollte man LinkedList statt ArrayList verwenden?

Frage 4 von 10

Wie fügt man ein Element zu einer ArrayList hinzu?

Frage 5 von 10

Was ist der Unterschied zwischen ArrayList und Vector?

Frage 6 von 10

Wie iteriert man über eine Liste mit for-each?

Frage 7 von 10

Was macht list.remove(0)?

Frage 8 von 10

Welche Methode gibt die Größe einer Liste zurück?

Frage 9 von 10

Was ist CopyOnWriteArrayList?

Frage 10 von 10

Wie sortiert man eine ArrayList?


📦 Downloads

Alle Code-Beispiele zum Herunterladen:

ProjektFür wen?Download
tag01-listen-starter.zip🟢 Einsteiger⬇️ Download
tag01-listen-complete.zip🟡 Musterlösung⬇️ Download

Quick Start:

# 1. ZIP entpacken
unzip tag01-listen-starter.zip

# 2. In NetBeans öffnen
# Datei → Projekt öffnen → Ordner auswählen

# 3. Main-Klasse ausführen
# Rechtsklick auf Main.java → Datei ausführen

🔗 Weiterführende Links

🇩🇪 Deutsch

RessourceBeschreibung
Rheinwerk OpenBook: Java SE 8Kostenloses Standardwerk, Kapitel Datenstrukturen
Javabeginners: SammlungenEinsteigerfreundliche Erklärungen
StudySmarter: Java CollectionsÜbersicht mit Lernkarten
IONOS: Java ListMethoden und Anwendungen erklärt

🇬🇧 Englisch

RessourceBeschreibungLevel
Oracle Java Tutorials: CollectionsOffizielle Dokumentation🟢
W3Schools: Java ArrayListInteraktive Beispiele🟢
Programiz: Java LinkedListVisualisierungen & Erklärungen🟢
GeeksforGeeks: LinkedListDetaillierte Code-Beispiele🟡
Baeldung: Java ListDeep Dive für Profis🟡

🛠️ Tools & Extensions

ToolBeschreibung
JShellREPL für schnelles Testen
IntelliJ IDEA CommunityIDE mit Collection-Hints

📧 Offizielle Dokumentation


💬 Geschafft! 🎉

Was du heute gelernt hast:

✅ Das Java Collections Framework und seine Struktur
✅ ArrayList und LinkedList – und wann du welche nutzt
✅ Immutable Lists mit List.of() erstellen
✅ Verschiedene Iterationsarten durch Listen
✅ Wrapper-Klassen und Autoboxing
✅ Best Practices für den Produktiveinsatz

Egal ob du heute zum ersten Mal von Collections gehört hast oder dein Wissen vertieft hast – du hast etwas Neues gelernt. Das zählt!

Fragen? Schreib uns:

  • Elyndra: elyndra.valen@java-developer.online
  • Nova: nova.trent@java-developer.online
  • Jamal: jamal.hassan@java-developer.online

Nächster Teil: Sets & Maps – Keine Duplikate, schnelles Finden 🚀

Keep learning, keep growing! 💚


Tags: #Java #Collections #ArrayList #LinkedList #JavaGrundlagen #Tutorial

© 2025 Java Fleet Systems Consulting | java-developer.online

Autoren

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.

  • Jamal Hassan

    💻 Luca Santoro – Der IT-Ninja

    IT-Support & DevOps Assistant | 29 Jahre | „Ich löse Dinge, bevor sie eskalieren.“

    Wenn bei Java Fleet der Drucker spinnt, das WLAN streikt oder der neue Mitarbeiter kein VPN-Zugriff hat – ist Luca schon unterwegs.
    Er ist der stille Systemretter, der dafür sorgt, dass alle anderen arbeiten können.
    Luca ist nicht laut, nicht hektisch, nicht übertrieben heroisch.
    Er ist einfach da – und das rechtzeitig.

    Seit 2023 unterstützt er das Team im Bereich IT-Support, Netzwerkmanagement und DevOps-Automatisierung.
    Er ist das, was man in der IT selten findet: zuverlässig, gelassen und serviceorientiert – mit technischem Tiefgang und menschlicher Geduld.

    💻 Die Tech-Seite

    Luca denkt in Systemen, nicht in Symptomen.
    Er betreut Hardware, richtet Arbeitsplätze ein, pflegt Benutzerkonten, überwacht Backups und unterstützt das DevOps-Team bei kleineren Deployments.
    Er arbeitet mit Linux, Docker, Active Directory, Ansible und klassischen Office-Netzwerkstrukturen.

    Was ihn besonders macht: Er versteht, dass IT-Support nicht nur Technik ist, sondern Kommunikation.
    Er erklärt, was er tut – klar, freundlich, ohne Fachjargon.
    Und er schreibt sich jeden Fehler auf, um ihn beim nächsten Mal schneller zu beheben.

    „Ich bin kein Feuerwehrmann. Ich bin der, der Rauchmelder installiert.“

    Wenn Franz-Martin über Stabilität redet, meint er oft Systeme, die Luca im Hintergrund pflegt.
    Er hat ein gutes Auge für Details, liebt klare Strukturen und hält Ordnung, wo Chaos droht.

    🌿 Die menschliche Seite

    Luca hat italienisch-deutsche Wurzeln, liebt guten Espresso und hat die entspannte Art eines Menschen, der Probleme ernst nimmt, aber nie dramatisch macht.
    Er ist höflich, hilfsbereit und humorvoll – der Typ Kollege, der leise lacht, wenn etwas schiefläuft, und sagt:

    „Kein Stress. Ich schau’s mir kurz an.“

    Er trägt oft ein leichtes Headset, hört Musik beim Arbeiten und findet in kleinen Routinen seinen Flow.
    Wenn andere nach Feierabend den Laptop zuklappen, prüft er noch schnell den Serverstatus – „nur zur Sicherheit“.

    Cassian sagt: „Er ist die Ruhe im System.“
    Kat nennt ihn „unseren stillen Lifesaver“.
    Und Franz-Martin beschreibt ihn mit einem Augenzwinkern:

    „Luca ist der Grund, warum der Kaffeeautomat läuft – und unser Git-Server auch.“

    🧠 Seine Rolle im Team

    Luca ist das stille Fundament der Java Fleet – derjenige, der sicherstellt, dass alle Systeme, Geräte und Prozesse ineinandergreifen.
    Er ist kein Entwickler im klassischen Sinne, aber ohne ihn gäbe es keine funktionierende Umgebung für die, die entwickeln.
    Seine Arbeit bleibt meist unsichtbar, doch seine Wirkung ist täglich spürbar.

    Er ist der Ansprechpartner, wenn Probleme auftauchen – und noch lieber: bevor sie auftauchen.
    Er dokumentiert, strukturiert, denkt voraus – und erinnert das Team daran, dass Zuverlässigkeit kein Feature ist, sondern eine Haltung.

    ⚡ Superkraft

    Ruhe im System.
    Luca erkennt Fehler, bevor sie eskalieren – und löst sie, bevor sie jemand bemerkt.

    💬 Motto

    „Wenn’s funktioniert und keiner weiß warum – dann hab ich’s repariert.“

  • Ensign Nova Trent

    24 Jahre alt, frisch von der Universität als Junior Entwicklerin bei Java Fleet Systems Consulting. Nova ist brilliant in Algorithmen und Datenstrukturen, aber neu in der praktischen Java-Enterprise-Entwicklung. Sie brennt darauf, ihre ersten echten Projekte zu bauen und entdeckt dabei die Lücke zwischen Uni-Theorie und Entwickler-Realität. Sie liebt Star Treck das ist der Grund warum alle Sie Ensign Nova nennen und arbeitet daraufhin das sie Ihren ersten Knopf am Kragen bekommt.