Letztes Update: 12.04.2018
Behandelte Befehle: Interface, Subinterface, Collection, List, Set, SortedSet, HashMap, TreeSet, Comparable, Comparator

In diesem Kapitel lernen Sie ein weiteres wichtiges Werkzeug im Umgang mit Klassen kennen: das Interface.

Interfaces sind für Sie in zweifacher Hinsicht wichtig. Erstens, um sie selbst zu verwenden - als Vereinbarung innerhalb eines Teams (oder mit sich selbst) oder als Abstraktion einer Implementation für langlebigen Code. Zweitens, weil in der Java-Standardbibliothek viele Klassen und Mechanismen auf Interfaces beruhen, insbesondere im Collections-Framework.

Das Collections-Framework ist eine Menge von Klassen, abstrakten Klassen und Interfaces, die in der Java-Standardbibliothek angeboten wird, um mehrere Objekte zu verwalten. Neben den Ihnen bereits bekannten Listen und Mengen, können Sie sortierte Listen erstellen und Daten-Paare mit sogenannten Hashtabellen abspeichern. In diesem Kapitel lernen Sie die wichtigsten Aspekte dieser Klassensammlung kennen.

21.1 Interfaces

Interface als Vereinbarung

Aus Kap. 19 sind Ihnen bereits abstrakte Klassen bekannt. Eine abstrakte Klasse kann nicht instanziiert werden. Sie dient eher dazu, die Klassenhierarchie besser zu strukturieren. Eine abstrakte Methode ist dann ein Versprechen, dass alle nicht-abstrakten Unterklassen diese Methode implementieren.

Ein Interface ist einer abstrakten Klasse sehr ähnlich. Es ist eine Art Vereinbarung darüber, welche Methoden in einer Klasse vorhanden sind. Wenn sich eine Klasse A an die Vereinbarung von Interface I hält, sagt man, dass Klasse A das Interface implementiert (bei einer abstrakten Klasse würde man sagen, dass Klasse A die Unterklasse ist).

Nehmen wir an, Sie möchten garantieren, dass bestimmte Klassen die Methode move() haben. Dann defnieren Sie ein Interface namens Movable (der Name ist natürlich frei wählbar und könnte auch Foo sein).

public interface Movable {

  void move(); // Versprechen einer Methode

}

Sie sehen, statt class steht hier interface. Die Methode sieht aus wie einer abstrakte Methode in einer abstrakten Klasse.

Eine Klasse kann jetzt dieses Interface implementieren (auch: erfüllen) und muss dann auch move() explizit hinschreiben. Das definiert man ähnlich wie bei einer Unterklassen-Beziehung, aber mit dem Schlüsselwort implements:

public class SpaceShip implements Movable {

  private double x;
  private double y;
  private double xspeed;
  private double yspeed;

  public void move() {
    x += xspeed;
    y += yspeed;
  }
}

Sie sehen auch, dass bei der Implementierung der Methode move() auch der Zugriffsmodifikator (hier: public) angegeben werden muss.

In UML wird die Interface-Beziehung mit einem gestrichelten Pfeil dargestellt, der ansonsten wie der Pfeil der Unterklassen-Beziehung aussieht:

Ein Interface ist genauso wie eine Klasse auch ein Datentyp. Das heißt ich kann eine Variable anlegen und die Methode(n) des Interfaces aufrufen:

Movable m = new SpaceShip();
m.move();

Interessanter ist das natürlich, wenn Sie mehrere Klassen haben, die das Interface erfüllen, und von denen Sie Objekte in einer Liste halten möchten:

ArrayList<Movable> things = new ArrayList<Movable>();
things.add(new SpaceShip());
things.add(new Enemy()); // Enemy implements Movable
things.add(new Planet()); // Planet implements Movable

for (Movable m: things) {
  m.move();
}

Nun gibt es einige Unterschiede zwischen Interface und abstrakter Klasse:

  1. Ein Interface kann keinen Code (also keine Variablen und keine implementierten Methoden) beinhalten
  2. Eine Klasse, die ein Interface implementiert, kann zusätzlich eine Oberklasse haben.
  3. Eine Klasse kann mehrere Interfaces gleichzeitig implementieren.

Der erste Punkt ist klar: ein Interface enthält niemals ausführbaren Code. Der zweite Punkt ist wichtig: Die Klasse SpaceShip kann ich einer normalen Klassenhierachie stecken, z.B.:

public class SpaceShip extends GameObject implements Movable {

  // ...

}

Vielleicht gibt es ein weiteres Interface, um Objekte zerstörbar zu machen, und die entsprechenden Methoden bereitstellt:

public interface Destroyable {

  void hit(double power);
  boolean isDestroyed();

}

Dann kann SpaceShip auch dies Interface erfüllen:

public class SpaceShip implements Movable, Destroyable {

  private double x;
  private double y;
  private double xspeed;
  private double yspeed;
  private double energy;

  public void move() {
    x += xspeed;
    y += yspeed;
  }

  public void hit(double power) {
    energy -= power;
  }

  public boolean isDestroyed() {
    return energy <= 0;
  }
}

Beispiel Warenhaus

Nehmen wir an, Sie haben ein Verwaltungsprogramm für ein Warenhaus. Sie teilen Ihre Produkte in Kategorien ein:

Einige Ihrer Produktkategorien erfordern eine ausführliche Produktbeschreibung, die Sie mit getBeschreibung zugänglich machen wollen. Um die Produkte, die eine solche Beschreibung anbieten, von denen ohne Beschreibung zu unterscheiden, könnten Sie eine abstrakte Klasse MitBeschreibung einführen:

Beachten Sie aber, dass die neue Klasse inhaltlich nicht so richtig passt. Man würde eher Unterkategorien wie Bekleidung oder Medien erwarten. Die Klasse MitBeschreibung scheint ja nur eine bestimmte Eigenschaft auszudrücken.

Sinnvoller wäre es, diese Eigenschaft in einem Interface MitBeschreibung auszudrücken. Die Klasse Buch kann dann weiterhin Unterklasse von Product bleiben und gleichzeitig das Interface MitBeschreibung erfüllen.

Abgesehen davon, dass eine Klassenhierarchie semantisch "sauber" bleibt und besser erweitert werden kann, wenn solche Sekundäreigenschaften nicht hineincodiert werden, lässt sich ein Interface für viele verschiedene Klassenhierarchien nutzen.

Nehmen wir an, Sie hätten eine zweite Klassenhierarchie mit Diensten, die Ihre Firma anbietet. Auch Dienste können teilweise eine Langbeschreibung enthalten. Sie können das selbe Interface benutzen, um Klassen mit dieser Eigenschaft zu kennzeichnen:

Beispiel in Processing

In Processing haben Sie häufig mit animierten und interaktiven Objekten zu tun. Innerhalb eines 2D-Spiels kann man alle Spielobjekte als solche auffassen, die Koordinaten besitzen und eine render() Methode, um sie darzustellen.

Das würden Sie mit einer abstrakten Klasse realisieren, die einen Positionsvektor vorgibt und eine abstrakte Methode render() definiert:

abstract class GameObject {

  PVector pos = new PVector();

  abstract void render();
}

Jetzt können Sie weitere Klassen erstellen, die den Positionsvektor erben und die die Methode render() mit Code füllen müssen:

Wenn einige Klassen bewegte Objekte repräsentieren (hier: SpaceShip und Enemy), andere aber unbewegte Objekte (hier: Wall), dann müssten Sie eine weitere abstrakte Klasse einführen, um dies zu unterscheiden:

Der Nachteil einer abstrakten Klasse ist, dass es schwierig werden kann, die Hierarchie sinnvoll zu erweitern. Durch die Einschränkung, dass jede Klasse nur eine Oberklasse haben darf, kann es passieren, dass Sie Ihre Hierarchie nicht weiterentwickeln können. Nehmen wir an, Sie müssen alle Klassen, die Nicht-Spieler-Objekte abbilden, ebenfalls unter eine abstrakte Klasse versammeln. Dann haben Sie ein Problem mit Enemy, da diese Klasse sowohl Unterklasse von Movable als auch von NonPlayer sein müsste, aber eben nur eine Oberklasse haben darf:

Daher ist es hier sinnvoll, statt der abstrakten Klasse Movable ein Interface Movable zu definieren. Dadurch bleibt die Klassenhierarchie flexibel für Erweiterungen.

Der Nachteil ist jedoch, dass Sie dem Interface Movable keinen Code mitgeben können:

interface Movable {

  void move();

}

So könnte der Code für die drei Klassen aussehen:

class SpaceShip extends GameObject implements Movable {
  void move() {
    pos.x = mouseX;
    pos.y = mouseY;
  }
  void render() {
    fill(255);
    ellipse(pos.x, pos.y, 20, 20);
  }
}

class Enemy extends GameObject implements Movable {
  PVector speed = new PVector(random(-2,2), random(-2,2));

  Enemy(float x, float y) {
    pos.x = x;
    pos.y = y;
  }

  void move() {
    pos.add(speed);
    if (pos.x < 0 || pos.x > width)
      speed.x = - speed.x;
    if (pos.y < 0 || pos.y > width)
      speed.y = - speed.y;
  }

  void render() {
    fill(0);
    rectMode(CENTER);
    rect(pos.x, pos.y, 20, 20);
  }
}

class Wall extends GameObject {
  Wall(float x, float y) {
    pos.x = x;
    pos.y = y;
  }

  void render() {
    fill(255, 0, 0);
    rect(pos.x, pos.y, 10, 30);
}
}

Im Hauptcode würden Sie Objekte erzeugen und in draw() die entsprechenden Methoden aufrufen. Beachten Sie, dass Sie instanceof auch mit einem Interface verwenden können:

ArrayList<GameObject> objects = new ArrayList<GameObject>();

void setup() {
  size(200, 200);
  objects.add(new SpaceShip());
  objects.add(new Enemy(20, 20));
  objects.add(new Enemy(180, 50));
  objects.add(new Wall(30, 70));
  objects.add(new Wall(180, 50));
}

void draw() {
  background(200);

  for (GameObject obj : objects) {
    obj.render();

    if (obj instanceof Movable) {
      ((Movable)obj).move();
    }

  }
}

Interfaces in der Software-Entwicklung

Eine wichtige Anwendung von Interfaces ist es, in der Entwicklungsphase eines Software-Projekts einen Vertrag zwischen den Entwicklungs-Teams zu bilden. Nehmen wir an, das Software-Projekt besteht aus der Entwicklung einer Videosoftware. Team A entwickelt dabei den Video-Player zum Abspielen von Videos und Team B entwickelt die GUI dazu.

Damit Team B beginnen kann, müsste eigentlich der Video-Player schon fertig sein. Damit Team B aber loslegen kann, einigt man sich im Team auf ein Interface für den Video-Player:

public interface VideoPlayer {

  void play();
  void pause();
  void stop();
  void seek(double seekTime);
  double getTotalDuration();
  void setVolume(double value);
  double getVolume();

}

Team A könnte zunächst eine Dummy-Implementierung bereitstellen, so dass Team B bereits Code schreiben kann, der den Typ VideoPlayer benutzt.

public class DummyPlayer implements VideoPlayer {

  void play() {
    System.out.println("playing...");
  }

  void pause() {
    System.out.println("playing...");
  }

  // usw.
}

Sobald ein erster Prototyp vorhanden ist, kann der Prototyp verwendet werden - dann die finale Implementierung, dann die verbesserte Version 2.0 usw.

Sie werden im nächsten Abschnitt sehen, dass man Interfaces auch benutzt, um verschiedene Implementierungsvarianten (mit verschiedenen Vor- und Nachteilen) unter derselben Flagge fahren zu lassen.

Zusammenfassung

Ein Interface ist eine Vereinbarung über Methoden. Jede Klasse, die ein Interface implementiert, muss die dort angegebenen Methoden implementieren. Eine Klasse gibt mit dem Schlüsselwort implements an, dass Sie ein Interface implementiert.

Eine Klasse kann sowohl eine Oberklasse haben als auch ein oder mehrere Interfaces erfüllen.

Ein Interface ist auch ein Datentyp, so dass Variablen, Arrays, Listen etc. diesen Typ nutzen können. Mit instanceof kann man prüfen, ob eine Variable einem Interfacetyp angehört.

Ein Interface kann auch dazu dienen, dass mehrere Implementationen der selben Funktionalität angeboten werden.

In Softwareprojekten werden Interfaces genutzt, um Absprachen zwischen Teams formal zu definieren und um bei Bedarf vorläufige Implementierungen einzusetzen zu können.

Übungsaufgaben

(a) Einfaches Interface

Nehmen wir an, Ihr Kollege muss eine einfache Klasse Implementieren, die zwei Rechenoperationen kann: eine Summe bilden (aus einem Array von Zahlen) und einen Mittelwert von zwei Zahlen berechnen. Sie geben die Klasse in Auftrag und definieren ein Interface namens Rechner mit zwei Methoden:

  • summe bekommt einen Array von Zahlen (double) und gibt eine Zahl zurück (double)
  • mittelwert bekommt zwei Zahlen (double) und gibt eine Zahl zurück (double)

Damit Sie schonmal loslegen können, schreiben Sie die Klasse RechnerDummy, wo die zwei Methoden aber beide immer 0 zurückgeben.

In einer anderen Klasse, in Ihrer statischen main()-Methode, haben Sie etwas Testcode:

public static void main(String[] args) {
  Rechner rechner = new RechnerDummy();
  double[] beispiel = {1, 2, 3};
  System.out.println("1 + 2 + 3 = " + rechner.summe(beispiel));
  System.out.println("mittelwert(10, 20) = " + rechner.mittelwert(10, 20));
}

Mit der Dummyklasse bekommen Sie natürlich nur:

	1 + 2 + 3 = 0.0
	mittelwert(10, 20) = 0.0
	

Ihr Kollege ist jetzt fertig mit seiner "richtigen" Implementation von Rechner.Schreiben Sie diese neue Klasse, nennen Sie sie z.B. RechnerImpl - dort wird wirklich Summe und Mittelwert berechnet.

Sie können jetzt RechnerImpl in Ihren Testcode einfügen und das Programm erneut starten. Dann sehen Sie hoffentlich:

	1 + 2 + 3 = 6.0
	mittelwert(10, 20) = 15.0
	

(b) Warenhaus

Schreiben Sie Code, um das oben besprochene Szenario zu testen:

Erstellen Sie die angezeigten Klassen. Erstellen Sie auch das Interface MitBeschreibung. Für die Klassen, die MitBeschreibung erfüllen, müssen Sie eine Beschreibung speichern (diese wird z.B. im Konstruktor übergeben und gespeichert).

Erzeugen Sie eine Liste von Produkten und eine Liste von Diensten. Anschließend gehen Sie mit einer foreach-Schleife durch Ihre Objekte und nur wenn ein Objekte vom Typ MitBeschreibung ist, wird die Beschreibung ausgedruckt

21.2 Collections

In der Java-Standardbibliothek gehören die sogenannten Collections-Klassen zu den meist genutzten. Alle Klassen sind im Package java.util zu finden.

Listen

Sie haben mittlerweile hoffentlich häufigen Gebrauch von der ArrayList gemacht. Aus Kap. 16 wissen Sie, dass es verschiedene Möglichkeiten gibt, eine Liste zu programmieren - so ist z.B. die Klasse LinkedList eine Alternative zu ArrayList. Wenn Sie aber Ihren Code schreiben, müssen Sie sich entscheiden, welche Liste Sie verwenden wollen. Oder?

Nein, müssen Sie nicht. Mit Hilfe des Interfaces List (auch in der Java-Standardbibliothek) können Sie Ihren Code so gestalten, dass Sie nur an wenigen Stellen entscheiden müssen, welche Listenimplementation eingesetzt wird. Das Interface List definiert ein paar Standardmethoden von Listen: add(), get(), clear() ... Beide Klassen, ArrayList und LinkedList implementieren dies Interface.

Für Ihren Code heißt das, dass Sie nur bei der Erzeugung der Listeninstanz die konkrete Klasse (hier: ArrayList)angeben müssen. Für alles andere benutzen Sie das Interface. Dazu müssen Sie die Variable als List deklarieren:

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

Als Beispiel schauen wir uns eine sehr einfache Form der Kontaktverwaltung an. Es werden die Namen der Kontakte in einer Liste gespeichert. Eine Methode find erlaubt es, den ersten Namen zu finden, der den gegebenen Suchstring enthält:

public class Kontakte {

  private List<String> kontakte = new ArrayList<String>();

  public boolean find(String suchstring) {
    for (String k: kontakte) {
      if (k.contains(suchstring)) {
		    return true;
      }
    }
    return false;
  }
}

Der Code würde auch funktionieren, wenn wir uns für die verlinkte Liste entscheiden:

public class Kontakte {

  private List<String> kontakte = new LinkedList<String>();

  public boolean find(String suchstring) {
    for (String k: kontakte) {
      if (k.contains(suchstring)) {
	  	  return true;
      }
    }
    return false;
  }
}

Wir wollen die Methoden find() verbessern, indem wir eine Liste von Namen zurückgeben, die jeweils den Suchstring enthalten (es könnten ja mehrere sein). Auch hier nutzen wir lieber das allgemeinere List für die Rückgabe, obwohl wir in der Methode selbst eine spezifische Liste benutzen:

public class Kontakte {

  private List<String> kontakte = new ArrayList<String>();

  public List<String> find(String suchstring) {
    List<String> result = new ArrayList<String>();
    for (String k: kontakte) {
      if (k.contains(suchstring)) {
  		  result.add(k);
      }
    }
    return result;
  }
}

Das Interface List ist ein Subinterface eines Interfaces namens Collection. Ein Subinterface wird wie eine Unterklasse definiert und erbt alle Anforderungen des übergeordneten Interfaces:

// Aus der Java-Bibliothek:
public interface List extends Collection {

}

Wenn List Subinterface von Collection ist, ist es auch gleichzeitig Subtyp. Das bedeutet, der obige Code würde auch wie folgt funktionieren:

public class Kontakte {

  private Collection<String> kontakte = new ArrayList<String>();

  public Collection<String> find(String suchstring) {
    Collection<String> result = new ArrayList<String>();
    for (String k: kontakte) {
      if (k.contains(suchstring)) {
		    result.add(k);
      }
    }
    return result;
  }
}

In Java gibt es mehrere Interfaces, die alle Unter-Interfaces von Collection sind. Zu jedem Interface gibt es eine oder mehrere Implementierungen:

Sie kennen bereits die Klasse HashSet aus Kap. 23: eine handelt sich um eine Menge, d.h. kein Objekt kann mehr als einmal vorkommen.

Sortierte Listen

Die Klasse TreeSet ist ebenfalls eine Menge, allerdings liegen die Elemente immer in sortierter Reihenfolge vor. In der Regel wird die Sortierung also ständig beibehalten, ein neues Element wird direkt an der richtigen Stelle einsortiert. Wenn man dann die sortierte Liste benötigt, muss nicht erst dann die (potentiell große) Liste sortiert werden. Außerdem kann man zeigen, dass man in sortierten Listen besonders schnell suchen kann (z.B. mit Binärer Suche).

Dabei muss die Sortierung natürlich definiert werden: nach welcher Eigenschaft (meist eine Instanzvariable, zum Beispiel ein String oder eine Zahl) soll sortiert werden und wie herum (abwärts, aufwärts)?

Es gibt zwei Möglichkeiten, diese Sortierung zu definieren.

Möglichkeit 1: Sie nutzen das Interface Comparable. Nehmen wir an, Sie haben eine Klasse Buch. Sie möchten eine Liste von sortierten Büchern halten. Dann erweitern Sie mit Ihre Klasse Buch einfach so, dass das Interface Comparable erfüllt ist. Dies Interface fordert lediglich eine Methode compareTo, die -1, 0 oder 1 zurückgibt, je nachdem ob das Parameterobjekt größer, gleich oder kleiner ist.

public class Buch implements Comparable<Buch> {

  private double preis;

  public Buch(double preis) {
    this.preis = preis;
  }

  public double getPreis() {
    return preis;
  }

  public int compareTo(Buch b) {
    if (preis == b.preis) {
      return 0;
    } else if (preis < b.preis) {
      return -1;
    } else {
      return 1;
    }
  }

  public String toString() {
    return "Buch " + preis;
  }
}

Wenn Sie jetzt ein TreeSet verwenden, wird erkannt, dass Buch dies Interface erfüllt und entsprechend sortiert.

// In irgendeiner statischen main() ...

TreeSet<Buch> list = new TreeSet<Buch>();
list.add(new Buch(10));
list.add(new Buch(1));
list.add(new Buch(5));

for (Buch b: list) {
  System.out.println("> " + b);
}

Möglichkeit 2: Sie schreiben einen Comparator. Ein Comparator müsste eigentlich ein Objekt sein, aber Sie können einfach einen Lambda-Ausdruck definieren, der zwei Objekte vergleicht und wieder -1, 0 oder 1 zurückgibt. Diesen Lambda-Ausdruck übergeben Sie dann dem Konstruktor von TreeSet. Der folgende Code funktioniert also auch dann, wenn Buch nicht das Interface Comparable erfüllt.

// In irgendeiner statischen main() ...

TreeSet<Buch> list = new TreeSet<Buch>((b1, b2) -> {
  if (b1.getPreis() == b2.getPreis()) {
    return 0;
  } else if (b1.getPreis() );

list.add(new Buch(10));
list.add(new Buch(1));
list.add(new Buch(5));

for (Buch b: list) {
  System.out.println("> " + b);
}

Der obige Lambda-Ausdruck ist eine Funktion mit zwei Parametern b1 und b2, beide vom Typ Buch (der hier inferriert wird). Es muss ein int zurückgegeben werden, wobei -1 heißt, dass b1 kleiner ist, 0 heißt, beide sind gleich und 1 bedeutet, dass b1 größer ist als b2.

Hashtabellen

Wenn Sie einen Array anlegen, dann geben Sie jedem Element einen "Zugriffsschlüssel" in Form einer Indexzahl. Mit dieser Indexzahl können Sie dann auf das Element zugreifen. Nennen wir fortan diese Zahl den Schlüssel (engl. key) und das eigentliche Element den Wert (engl. value).

Manchmal möchten Sie etwas anderes als eine Zahl für den Zugriff verwenden. In einem Telefonbuch verwenden Sie den Nachnamen plus Vornamen (Schlüssel), um auf die Telefonnummer (Wert) zuzugreifen. Oder Sie verwalten eine Hochschule mit Studenten und möchten mit der Matrikelnummer (Schlüssel) auf die Studenten (Wert) zugreifen. Oder eine Bibliothek: Buchsignatur (Schlüssel) und Buch (Wert).

Man kann sich das als Tabelle vorstellen:

NameTelefonnummer
Harry1413
Sally2118
Billy4711

Eine effiziente Art, Tabellen zu speichern, nennt man Hashtabelle. In Java gibt es die Klasse HashMap, die diese Art der Speicherung erlaubt. HashMap ist ein Generic mit zwei Datentypen: den Schlüssel und den Wert.

Nehmen wir an, wir wollen ein Telefonbuch speichern, wo der Schlüssel ein Name ist (String) und der Wert eine Telefonnummer (wir nehmen hier zu Demonstrationszwecken int, auch wenn ein String normalerweise sinnvoller wäre). Dann erstellen wir unsere HashMap wie folgt:

HashMap<String, Integer> telefonbuch = new HashMap<String, Integer>();

Beachten Sie, dass wir die Wrapperklasse Integer von int verwenden müssen.

Um eine Telefonnummer zu speichern, verwenden wir die Methoden put:

telefonbuch.put("Harry", 1413);
telefonbuch.put("Sally", 2118);
telefonbuch.put("Billy", 4711);

Wenn wir auf eine Telefonnummer zugreifen wollen, verwenden wir get:

int num = telefonbuch.get("Billy");
System.out.println(num);

Auch HashMap erfüllt ein Interface, das einfach nur Map heißt.

Man könnte bei Deklaration auch schreiben:

Map<String, Integer> telefonbuch = new HashMap<String, Integer>();

Manchmal muss man doch durch die Liste der Telefonnummern gehen. Diese Liste bekommt man mit values():

for (Integer tel: telefonbuch.values()) {
  System.out.println("Nummer: + " + tel);
}

Manchmal möchte man alle möglichen Schlüssel ausgeben, diese bekommt man mit keySet()

for (String key: telefonbuch.keySet()) {
  System.out.println("Schlüssel: + " + key);
}

Übungsaufgaben

(a) Personen auflisten

Schreiben Sie eine Klasse Person mit Name und Alter. Erstellen Sie eine Liste von Personen. Verwenden Sie das List-Interface und die Klasse ArrayList. Befüllen Sie die Liste und geben Sie die Personen auf der Konsole aus.

Ändern Sie die Liste zu einer LinkedList. Haben Sie den Code auch wirklich nur an einer Stelle geändert?

(b) Personen sortieren

Schreiben Sie eine Klasse Person mit Name und Alter. Erstellen Sie einen TreeSet von Personen, so dass die Personen (a) nach Alter oder (b) nach Name sortiert sind. Verwenden Sie für (a) das Interface Comparable und für (b) einen Comparator (Lambda-Ausdruck).

Hinweis: Die String-Methode compareToIgnoreCase(String str) macht genau dass, was Sie für (b) brauchen.

(c) Bücherei

Schreiben Sie eine Bücherei-Software. Die Klasse Buch enthält Eigenschaften wie Titel, Autor, Verlag. Die Klasse Buecherei enthält eine HashMap mit Signatur (String) und Buch (Objekt der Klasse Buch).

Schreiben Sie eine Methode, die eine Signatur bekommt (String), und das Buch zurückliefert. Schreiben Sie dann eine Methode, die einen Suchstring bekommt, und eine Liste von Büchern zurückliefert, die jeweils diesen Suchstring in Titel, Autor oder Verlag enthalten.

Zusammenfassung

Das Collection-Framework in Java besteht aus einer Reihe von Interfaces und Klassen, die es dem Programmierer erleichtern, mit vielen Elementen umzugehen.

Um von konkreten Implementierungen z.B. einer Liste wegzuabstrahieren, gibt es das Interface List. Beide Klassen, ArrayList und LinkedList erfüllen beide jeweils dies Interface.

Genauso wie es Unterklassen gibt, gibt es Subinterfaces, die alle Anforderungen des übergeordneten Interfaces erben. Das Interface List ist Subinterface des Interface Collection.

Um Tabellen zu speichern, wie z.B. ein Telefonbuch, kann man eine Hashtabelle verwenden. Man unterscheidet zwischen Schlüssel (Name im Telefonbuch) und Wert (Telefonnummer). In Java gibt es dafür die Klasse HashMap, die das Interface Map erfüllt. Die Klasse ist ein Generic mit zwei Datentypen als Argumenten.

In einer HashMap bekommt man einen Wert mit der Methode get. Man fügt ein Schlüssel-Wert-Paar hinzu, indem man die Methode put benutzt.

21.3 Links