Javaschubla.de - Java als erste Programmiersprache

Collections: Listen

Arrays sind nützlich, um mehrere Variablen des gleichen Typs zu speichern, aber sie haben den Nachteil, dass die Größe unveränderlich ist. Dafür gibt es Listen, z.B. ArrayList und LinkedList. Listen sind eine Art von Collections, weitere Arten sind z.B. Sets (Mengen) und Maps (Zuordnungen). ArrayList und LinkedList sind Klassen im Package java.util.

import java.util.*;

public class ListenBeispiel1
{
  public static void main(String[] args)
  {
    ArrayList<String> liste1 = new ArrayList<String>();
    liste1.add("ABC");
    liste1.add("Nachbar");
    liste1.add("Transfer");
    liste1.add("Oma");
    liste1.add("Niemand");
    
    System.out.println(liste1.get(2)); // gibt "Transfer" aus

    // Das erste Zeichen jedes Elements ausgeben, also "ANTON"
    for(String s: liste1)
    {
      System.out.print(s.charAt(0));
    }

    // Zeilenumbruch
    System.out.println();

  }
}

Link zum Download: ListenBeispiel1.java.

Was passiert im Programm?

Bei diesem Beispiel hätte man genau so gut ein String-Array der Länge 5 nehmen können: String[] array1 = new String[5]; Wenn man aber nicht vorher weiß, wie viele Elemente es geben wird, z.B. weil der Benutzer sie eingibt oder weil sie aus einer Datei oder Datenbank ausgelesen werden, dann ist eine Liste besser als ein Array. Wenn man ein Array benutzen würde und es ist voll, müsste man ein größeres Array anlegen und die vorhandenen Elemente dorthin kopieren.

Man kann Elemente nicht nur hinzufügen und lesen, sondern auch mit remove() (mit dem Index oder mit dem Wert als Argument (Parameter)) entfernen und mit set(index, wert) ändern. Mit size() erfährt man die aktuelle Anzahl Elemente.

import java.util.*;

public class ListenBeispiel2
{
  public static void main(String[] args)
  {
    ArrayList<String> liste2 = new ArrayList<String>();
    liste2.add("ABC");
    liste2.add("Nachbar");
    liste2.add("Transfer");
    liste2.add("Oma");
    liste2.add("Niemand");

    System.out.println(liste2.size()); // gibt 5 aus
    
    liste2.remove("Nachbar"); // liste2.remove(1) würde dasselbe tun

    System.out.println(liste2.size()); // gibt jetzt 4 aus

    System.out.println(liste2.get(2)); // gibt nun "Oma" aus, nicht "Transfer"

    liste2.set(3, "Marmelade"); // "Niemand" wird mit "Marmelade" ersetzt

    // Das erste Zeichen jedes Elements ausgeben, also diesmal "ATOM"
    for(String s: liste2)
    {
      System.out.print(s.charAt(0));
    }

    // Zeilenumbruch
    System.out.println();

  }
}

Link zum Download: ListenBeispiel2.java.

ArrayList und LinkedList

Was ist nun der Unterschied zur LinkedList? Ersetze in den Beispielprogrammen die ArrayList durch LinkedList und es scheint sich überhaupt nichts zu ändern. Tatsächlich bieten sie genau dieselben Methoden an. Der Unterschied ist die Implementierung, das heißt, wie die Klasse innendrin aussieht, also der Quellcode. Die ArrayList enthält ein Array, das vergrößert wird, wenn kein Platz mehr ist. Die LinkedList enthält kein Array, stattdessen gibt es jeweils einen Zeiger (Pointer) auf das nächste Element.

Welche Auswirkungen hat das? Der Unterschied besteht vorwiegend darin, welche Operationen langsam oder schnell gehen. Z.B. remove() geht bei LinkedLists sehr schnell, das Element wird einfach entfernt und das Vorgänger-Element wird mit dem Nachfolger-Element (welches vorher das übernächste Element war) verknüpft. Bei der ArrayList dauert remove() viel länger, wenn es nicht gerade das letzte Element ist, welches entfernt wird, denn nun müssen alle Elemente, die nach dem gelöschten Element stehen, in dem Array um eins nach links kopiert werden. Wenn man also ein Programm schreibt, in dem öfter mal Elemente entfernt werden, dann ist eine LinkedList besser als eine ArrayList. Das Gleiche gilt für das Hinzufügen von Elementen mit add(index, wert), also das "Dazwischenschieben" eines Elements irgendwo in der Mitte der Liste.

Das Durchgehen der Elemente vom Anfang bis zum Ende ist gleich schnell, es macht keinen Unterschied, ob man auf eine bestimmte Position des Arrays, das von ArrayList benutzt wird, zugreift, oder sich in der LinkedList von Element zu Element hangelt. Aber wenn man häufig mit get() auf beliebige Elemente zugreifen muss in keiner bestimmten Reihenfolge, so ist die ArrayList besser: Sie greift quasi "sofort" (man nennt das "O(1)" lies "Oh von 1", "Größenordnung von 1" oder "in konstanter Zeit") auf das i-te Element zu, genau so schnell wie man in einem Array auf array[i] zugreift. Dort macht es auch keinen Unterschied, ob man array[4] oder array[1004] lesen will. Aber bei der LinkedList müsste man sich immer zu diesem Element "durchhangeln", also anhand der Pointer von einem Element zum nächsten Laufen. Analog läuft natürlich auch set(index, wert) für ArrayLists viel schneller als für LinkedLists (es sei denn, man geht immer alle Element vom Anfang bis zum Ende oder zumindest aufeinanderfolgende Bereiche durch, dann macht es wiederum keinen Unterschied).

Wenn immer nur Elemente mit add() am Ende hinzugefügt werden, so ist es für ArrayLists und LinkedLists in den meisten Fällen gleich schnell - aber wenn das interne Array der ArrayList gerade seine Maximalgröße erreicht hat, so dauert es dann ziemlich lange, weil das Array umkopiert werden muss. Deshalb ist es bei ArrayLists sinnvoll, dem Konstruktor die geschätzte benötigte Größe mitzugeben: ArrayList<Elementtyp> liste = new ArrayList<Elementtyp>(größe).

Iteratoren

Bevor in Java 1.5 (= Java 5.0) die for-in-Schleife hinzugefügt wurde, musste man mit Iteratoren über die Elemente von Listen und anderen Collections gehen. Auch jetzt braucht man sie in einigen Fällen noch. Z.B. darf man innerhalb der for-in-Schleife nicht remove() aufrufen! Dadurch verändern sich ja die Indeces bzw. Verlinkungen und alles kommt durcheinander. Wenn man jedoch einen Iterator verwendet, dann klappt es.

Mit liste.iterator() holt man sich den Iterator. Er hat nur drei Methoden:

Die aktuelle Position befindet sich jeweils zwischen zwei Elementen, also am Anfang auf 0 vor dem ersten Element der Liste, nach dem ersten Aufruf von next() auf Position 1, das heißt zwischen Element 0 und Element 1 der Liste, so dass beim zweiten Aufruf von next() das Element Nummer 1 (das zweite Element der Liste) zurückgegeben wird - genau so, wie man es erwarten würde.

import java.util.*;

public class IteratorBeispiel1
{
  public static void main(String[] args)
  {
    ArrayList<String> liste3 = new ArrayList<String>();
    liste3.add("ABC");
    liste3.add("Nachbar");
    liste3.add("Transfer");
    liste3.add("Oma");
    liste3.add("Niemand");
    
    Iterator<String> iterator = liste3.iterator();

    while(iterator.hasNext()) // überprüfen, ob noch ein Element folgt
    {
      String s = iterator.next(); // das nächste Element holen und die aktuelle Position dahinter setzen
      if (s == null || s.startsWith("N"))
      {
        iterator.remove(); // entfernt das zuletzt gelesene Element
      }
      System.out.print(s.charAt(0));
    }
    // Ausgabe ist trotzdem ANTON!

    System.out.println(); // Zeilenumbruch

    System.out.println(liste3.size()); // 3

  }
}

Link zum Download: IteratorBeispiel1.java.

Normalerweise wird eine for-Schleife benutzt. Das findet man vielleicht zuerst komisch, weil der dritte Teil des Schleifenkopfes leer bleibt, aber es ist so üblich. Das Programm unten tut genau das Gleiche wie das oben. Der einzige Unterschied ist, dass der Scope der Variable iterator am Ende der Schleife endet, d.h. sie hört dort auf zu leben.

import java.util.*;

public class IteratorBeispiel2
{
  public static void main(String[] args)
  {
    ArrayList<String> liste4 = new ArrayList<String>();
    liste4.add("ABC");
    liste4.add("Nachbar");
    liste4.add("Transfer");
    liste4.add("Oma");
    liste4.add("Niemand");
    
    for(Iterator<String> iterator = liste4.iterator(); iterator.hasNext(); )
    {
      String s = iterator.next();
      if (s == null || s.startsWith("N"))
      {
        iterator.remove(); // entfernt das aktuelle Element
      }
      System.out.print(s.charAt(0));
    }
    // Ausgabe ist trotzdem ANTON!

    System.out.println(); // Zeilenumbruch

    System.out.println(liste4.size()); // 3

  }
}

Link zum Download: IteratorBeispiel2.java.