Zum Inhalt

1. Lineare Datenstrukturen

Kommen Datenstrukturen zum Einsatz, geht es meist darum, beliebig viele Objekte in einer passenden Datenstruktur zu verwalten, z.B. alle Schüler einer Schule. In einem solchen Falle macht es keinen Sinn, alle Eigenschaften eines Schülers als primitive Datentypen zu modellieren und für jede Eigenschaft ein eigenes Feld o.ä. zu bilden. Deshalb wird zu Beginn dieses Kapitels zunächst erläutert, wie Objekte als Datensatzspeicher verwendet werden können. Im Anschluss daran werden die linearen Datenstrukturen Feld, Liste, Stack und Queue behandelt.

Objekte vs. primitive Datentypen

Objekte als Datenspeicher

In diesem Abschnitt möchten wir eine Schülerverwaltung modellieren. Dazu entwerfen wir eine eigene Klasse Schueler und modellieren zunächst die benötigten Eigenschaften als Attribute. Bei Bedarf kommen noch Methoden zum Ändern und Abfragen der Attribute hinzu.

Im vorliegenden Beispiel sind die beiden Attribute Alter und Name definiert worden. Beide müssen im beim Erzeugen eines Schüler-Objekts übergeben werden (vgl. Konstruktor), beide können mithilfe der set-Methoden auch nachträglich verändert und mithilfe der get-Methoden jederzeit abgefragt werden.

public class Schueler {

    private String name;
    private int alter;

    public Schueler(String name, int alter) {
        this.name = name;
        this.alter = alter;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setAlter(int alter) {
        this.alter = alter;
    }

    public int getAlter() {
        return alter;
    }

}

Objekte einer Klasse, die wie in diesem Beispiel lediglich als Datenspeicher fungieren und selbst keine oder fast keine eigene Programmlogik besitzen, werden in Java als Beans ("Kaffeebohnen") bezeichnet.

Wrapper-Klassen

Die einzige Datenstruktur, die in Java neben Objekten auch primitive Datentypen verwalten kann, ist das Feld. Alle anderen Datenstrukturen (z.B. Liste, Stack, Queue usw.) erlauben lediglich das Speichern von Objekten.

Doch was ist zu tun, wenn es für eine Anwendung ausreicht, lediglich Zahlen (z.B. ISBN-Nummern oder Schuhgrößen) zu speichern? Muss dann erst umständlich eine wie im Schüler-Beispiel beschriebene eigene Klasse entworfen werden?

Für diese seltenen Fälle sind in Java so genannte Wrapper-Klassen eingebaut.

primitiver Datentyp Wrapper-Klasse Service-Methode
int Integer intValue()
double Double doubleValue()
char Character charValue()
boolean Boolean boolValue()

Beispiel: Ein primitiver int-Wert 17 wird in einem Objekt der Klasse Integer "verpackt" (Boxing).

int i = 17;
Integer iobj = new Integer(i);  // Boxing

Um ihn anschließend wieder aus dem Objekt herauszulesen (Unboxing), steht für jede Wrapper-Klasse eine entsprechende Service-Methode (vgl. Tabelle) zur Verfügung.

int j = iobj.intValue(); // Unboxing

Feld

Die naheliegende Datenstruktur zur Verwaltung von Objekten ist das Feld. Es ist in Java bereits eingebaut und ist recht einfach zu nutzen.

Verwendung eines Felds

An dieser Stelle soll das Beispiel einer Schülerverwaltung wieder aufgegriffen werden. Eine entsprechende Klasse Schüler ist bereits im vorherigen Abschnitt (vgl. Objekte als Datenspeicher) entwickelt worden.

Die Erzeugung und Verwendung eines Feld über Schüler-Objekten verläuft analog zur Verwendung eines Felds über primitiven Datentypen (vgl. auch Abschnitt Felder).

Wie im Quelltext zu sehen ist, werden zunächst die Schüler-Objekte erzeugt und anschließend wie in einem Feld üblich an die entsprechenden Positionen gesetzt. Die Methode istVorhanden() zeigt exemplarisch den Durchlauf durch ein Feld.

public class Schuelerverwaltung {

    private Schueler[] meineschueler;

    public Schuelerverwaltung() {
        Schueler s1 = new Schueler("Otto",14);
        Schueler s2 = new Schueler("Susi",13);
        Schueler s3 = new Schueler("Hans",15);

        meineschueler = new Schueler[3];
        meineschueler[0] = s1;
        meineschueler[1] = s2;
        meineschueler[2] = s3;
    }

    public boolean istVorhanden(String suchname) {
        for (int i=0; i<meineschueler.length; i++) {
            Schueler derschueler = meineschueler[i];
            String name = derschueler.getName();
            if (name.equals(suchname)) {
                return true;
            }
        }
        return false;
    }

}

Diskussion

Vorteile:

  • Das Feld ist in Java eingebaut und kann ohne zusätzlichen Aufwand eingesetzt werden.
  • Der Zugriff ist auf jede Feldposition möglich. Damit kann wieder eine schnelle binäre Suche mit logarithmischem Aufwand realisiert werden.

Nachteile:

  • Das Feld ist statisch, d.h. seine Größe muss beim Erzeugen festgelegt werden. Dadurch könnte es entweder viel zu groß (Speicherverschwendung) oder viel zu klein (kein nachträgliches Einfügen möglich) sein.

Dem Nachteil der statischen Größe begegnen dynamische Datenstrukturen wie die Liste (vgl. Abschnitt Liste). Die ideale Kombination von schneller Suche und Dynamik stellt der Suchbaum dar (vgl. Abschnitt Suchbaum).

Liste

Die Liste ist eine lineare Datenstruktur, die sequentiell (d.h. von vorn nach hinten) durchlaufen wird. Jedes Element der Liste hat lediglich Zugriff auf seinen Nachfolger, wodurch sich eine verkettete Struktur ergibt.

Die Liste ist dynamisch, da an jeder Position neue Elemente in die Kette eingefügt werden können. Eine Suche auf einer Liste hat lineare Laufzeit, da nur der vollständige Durchlauf von vorn nach hinten möglich ist.

Die Abiturklasse List ist generisch, d.h. bereits beim Erzeugen wird festgelegt, von welcher Klasse die Objekte sein müssen, die in der Liste verwaltet werden sollen. Dabei ist es natürlich möglich (wenn auch nur selten sinnvoll), auch Objekte von Unterklassen dieser Klasse zu verwalten.

Eine Liste aufbauen

Im folgenden Beispiel wird beim Erzeugen der Liste festgelegt, dass Objekte der Klasse String verwaltet werden sollen. Mit Hilfe der Methode append werden die Zeichenketten jeweils an das Ende der Liste gehängt, wodurch sich die Reihenfolge Babsi - Franzi - Susi ergibt.

String s1 = "Babsi";
String s2 = "Franzi";
String s3 = "Susi";

List<String> l = new List<String>();
l.append(s1);
l.append(s2);
l.append(s3);

Eine Liste durchlaufen

Beim Durchlaufen einer Liste (z.B. um eine Suche zu realisieren) ist ein Sprung an den Anfang der Liste mithilfe der Methode toFirst() erforderlich. Anschließend hilft die Anfrage hasAccess() dabei zu erfragen, ob noch ein Listenelement verfügbar ist oder durch das Weiterlaufen durch die Liste mit der Methode next() bereits das Ende der Liste erreicht ist.

Wird die Zeichenketten-Liste aus dem vorherigen Abschnitt zugrunde gelegt, so lautet die Ausgabe der Schleife: Babsi -> Franz -> Susi -> ENDE!

l.toFirst();

while (l.hasAccess()) {
    String s = l.getContent();
    System.out.print(s+" -> ");
    l.next();
}

System.out.println("ENDE!");

Eine Liste verändern

Für das nächste Beispiel stellen wir uns eine Musiksammlung vor, in der Objekte der Klasse Titel verwaltet werden. Für jedes Titelobjekt ist eine Bewertung zwischen 1 und 5 angegeben, die durch die Methode getBewertung() erfragt werden kann.

Damit durchläuft die Schleife die Liste aller Titelobjekte und löscht mit der Methode remove() diejenigen Titel, die eine Bewertung 1 erhalten haben. Zusätzlich wird die Anzahl der gelöschten Objekte in der Variablen counter mitgezählt.

l.toFirst();
int counter = 0;

while (l.hasAccess()) {

    Titel t = l.getContent();
    if (t.getBewertung()==1) { 
        l.remove(); 
        counter++;
    }
    else { 
        l.next(); 
    }

}

In eine sortierte Liste einfügen

Eine Sortierung einer Liste kann mitunter ebenfalls sinnvoll sein. In unserem Beispiel der Musiksammlung könnten die Titelobjekte sortiert nach der Bewertung abgelegt werden. Damit die Sortierung der Liste nicht verloren geht, muss dem Benutzer der Musiksammlung eine eigene Methode zum sortierten Einfügen eines neuen Titels zur Verfügung gestellt werden.

Die Schleife wird solange durchlaufen, bis ein Titel gefunden wurde, der eine höhere oder gleich hohe Bewertung als der neue Titel besitzt. An dieser Stelle wird das zugehörige Titelobjekt mit der Methode insert vor dem aktuellen Listenobjekt eingefügt.

public void fuegeTitelSortiertEin(Titel t, List l) {
    l.toFirst();
    while (l.hasAccess()) {
        Titel aktuell = l.getContent();
        if (t.getBewertung() <= aktuell.getBewertung()) {
            l.insert(t);
            return;
        }
        else {
            l.next();
        }
    }
    l.append(t);
}

Stack und Queue

Als Spezialfall der Liste sind sich Stack und Queue sehr ähnlich, weshalb sie oft gemeinsam besprochen werden. Die Queue (Schlange) ist eine FIFO-Datenstruktur (first in, first out) - die Objekte werden in der Reihenfolge verwaltet, in der sie in die Datenstruktur eingefügt worden sind. Direkter Zugriff besteht nur auf das vorderste Element der Schlange (den Kopf), eingefügt wird immer hinten (am Schlangenende). Der Stack (Stapel) hingegen wird als LIFO-Datenstruktur (last in, first out) bezeichnet. Hier werden neue Objekte immer oben auf den Stapel gelegt, direkter Zugriff besteht ebenfalls nur auf das oberste Objekt des Stapels.

Einen Stack aufbauen

Wie bei einer Liste ist beim Erzeugen eines Stacks zunächst die Klasse der Objekte anzugeben, die im Stack verwaltet werden sollen. Im nachfolgenden Beispiel werden drei Zeichenketten auf den Stack gelegt. Durch die LIFO-Struktur ergibt sich auf dem Stapel von oben nach unten gesehen die Abfolge Hans - Maria - Manfred.

String s1 = "Manfred";
String s2 = "Maria";
String s3 = "Hans";

Stack<String> s = new Stack<String>();

s.push(s1);
s.push(s2);
s.push(s3);

Einen Stack durchlaufen

Der Stapel kann mit einer Schleife leicht von oben nach unten durchlaufen werden.

!!! Hinweis Dabei wird der Stapel abgebaut, d.h. der Zugriff auf die Objekte geht verloren. Ist dies unerwünscht, müssen die einzelnen Objekte während des Durchlaufs in einer anderen Datenstruktur zwischengespeichert werden.

while (!s.isEmpty()) {

    String aktuell = s.top();

    System.out.println(aktuell);

    s.pop();
}

Eine Queue aufbauen

Wie bei einer Liste ist beim Erzeugen einer Queue zunächst die Klasse der Objekte anzugeben, die in der Queue verwaltet werden sollen. Im nachfolgenden Beispiel werden drei Zeichenketten in die Schlange eingefügt. Durch die FIFO-Struktur ergibt sich in der Schlange die Abfolge Manfred - Maria - Hans.

String s1 = "Manfred";
String s2 = "Maria";
String s3 = "Hans";

Queue<String> s = new Queue<String>();

s.enqueue(s1);
s.enqueue(s2);
s.enqueue(s3);

Eine Queue durchlaufen

Eine Schlange kann mit einer Schleife leicht von vorn nach hinten durchlaufen werden.

!!! Hinweis Dabei wird die Schlange abgebaut, d.h. der Zugriff auf die Objekte geht verloren. Ist dies unerwünscht, müssen die einzelnen Objekte während des Durchlaufs in einer anderen Datenstruktur zwischengespeichert werden.

while (!s.isEmpty()) {

    String aktuell = s.front();

    System.out.println(aktuell);

    s.dequeue();
}