Zur Hauptnavigation springen [Alt]+[0] Zum Seiteninhalt springen [Alt]+[1]

Das Collections Framework

Das Paket java.util bietet mit dem Collections Framework generische Datentypen für Sammlungen15 gleichartiger Objekte an. Für die meisten Anwendungen finden sich hier Schnittstellen und Implementierungen. Es ist daher selten notwendig, selbst komplexe generische Datentypen zu deklarieren. Als Ausgangspunkt für die Suche nach dem „richtigen“ Datentyp bietet sich folgende Seite in der Java-Dokumentation an:

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/doc-files/coll-reference.html

Ebenso gibt es ein offizielles Tutorial zum Framework: https://docs.oracle.com/javase/tutorial/collections/index.html

Die Dokumentation der jeweiligen Schnittstellen ist ausführlich genug, dass Schülerinnen und Schüler der Kursstufe nach Einführung der generischen Datentypen und einer Erklärung zu Interfaces, diese ohne weitere Hilfsmittel nutzen können.

In Abbildung 7 sind die wichtigsten Interfaces und Klassen dargestellt. Dieses Klassendiagramm kann im Unterricht zur Verfügung gestellt werden. Wenn genügend Zeit ist, kann der Kurs einen Teil davon selbst durch Recherche in der Dokumentation entwerfen.

Programmierparadigmen

Abbildung 7: Klassendiagramm des Collections Framework ZPG Informatik [CC BY-SA 4.0 DE], aus 01_hintergrund.pdf, bearbeitet

„Collection“ gilt als Wurzel-Interface der Collection Hierarchie. Allerdings ist es für den Unterricht wichtig zuerst das Iterable Interface genauer zu betrachten. Dabei kann auch die Arbeit mit der Dokumentation exemplarisch vorgeführt werden.

Iteration und erweiterte for-Schleife

Nahezu alle Klassen des Collection Framework implementieren die Iterable Schnittstelle. Diese bildet die Grundlage für das Durchlaufen aller Elemente einer Sammlung.

Diese Schnittstelle ist recht einfach und schreibt lediglich drei Methoden vor:

void forEach(Consumer<? Super T> action)
Iterator<T> iterator() // Iteration
Spliterator<T> spliterator() // Traversierung und Partitionierung

Die erste wird zusammen mit Lambdaausdrücken genutzt. Die zweite liefert einen Iterator, der mit Schleifen (while, for und erweitertes for) genutzt wird. Die letzte wird für den Unterricht nicht benötigt.

Im Unterricht sollte zunächst auf den Iterator eingegangen werden. Ein gemeinsamer Blick in die Dokumentation zeigt, dass diese Schnittstelle vier Methoden vorschreibt. Zwei werden für den weiteren Unterrichtsgang benötigt:

boolean hasNext() //Gibt true zurück, falls noch unbesuchte Elemente
E next() //Besucht das nächste Element und gibt es zurück

Es wird klar, dass eine Sammlung durchlaufen werden kann, indem man sich zunächst den Iterator zurückgeben lässt und mit diesem Schritt für Schritt alle Elemente besucht bzw. durchläuft. Für folgende Beispiele sei eine Sammlung Collection<String> c beliebiger Zeichenketten gegeben, welche der Reihe nach ausgegeben werden sollen.

Iteration mit while-Schleife:

Iterator<String> iter = c.iterator();
 while( iter.hasNext() ){
     System.out.println( "Wert= " + iter.next() );
}

Iteration mit for-Schleife:

for( Iterator<String> it = c.iter(); iter.hasNext(); ){
     System.out.println( "Wert= " + iter.next() );
}

Vereinfachte Notation mit der erweiterten for-Schleife:

for( String s : c ){ //Sprich: für jeden String s in c
     System.out.println( "Wert= " + s );
}

Da im Beispielprojekt FilmsammlungGeneric<E> ein Subtyp von ArrayList<E> ist, implementiert diese auch die nötige Iterable Schnittstelle. Sollen beispielsweise für eine FilmsammlungGeneric<Film> fsG die Titel aller enthaltenen Filme ausgegeben werden, geht dies mit der erweiterten for-Schleife wie folgt:

for( Film f : fsG ){// Für jeden Film f in der Filmsammlung fsG
     System.out.println( f.getTitel() );
 }

Lambdaausdrücke

Java ist keine funktionale Programmiersprache, unterstützt aber seit Version 8 einige Aspekte funktionaler Programmierung. In Java sind Methoden immer an ein Objekt gebunden, auf das über eine Referenz zugegriffen wird. In einer funktionalen Sprache können Funktionen als Argumente übergeben werden. Dies ist in Java nicht möglich, da es keine Referenz auf Methoden gibt.

Allerdings entspricht ein Objekt ohne Attribute mit nur einer einzigen Methode im Wesentlichen einer Funktion. Die Referenz auf dieses Objekt kann als Referenz auf die enthaltene Methode aufgefasst werden. Deklariert man einen Typ durch ein Interface oder eine abstrakte Klasse mit nur einer einzigen Methode, nennt diesen auch SAM-Typ (engl. single abstract method). Ist ein formaler Wertparameter von einem solchen SAM-Typ, kann ihm als konkretes Argument jedes Objekt übergeben werden, das den SAM-Typ erweitert. Das Argument kann aus einer benannten Klasse, aber auch über eine anonyme Klasse instanziiert werden. In beiden Fällen entsteht Boilerplate16 . Um die damit verbundene Schreibarbeit zu vermeiden und den Quellcode lesbarer zu gestalten, stellt Java sogenannte funktionale Interfaces und Lambdaausdrücke zur Verfügung.

Ein Funktionales Interface ist mit der Annotation @FunctionalInterface versehen und besitzt nur eine einzige abstrakte Methode, die funktionale Methode genannt wird. Ein solche funktionale Schnittstelle liefert den Zieltyp für einen Lambdaausdruck.

Ein Lambdaausdruck ist eine Notation für anonyme (namenlose) Objekte, die ein funktionales Interface implementieren. Sie lesen sich somit wie die direkte Deklaration einer Funktionen.

Die allgemeine Syntax für einen Lambdaausdruck lautet:

( Parameterliste ) -> { Ausdruck oder Anweisungen }

Der Lambdaausdruck wird als konkretes Argument für einen formalen Parameter vom Typ eines funktionalen Interfaces übergeben. Sowohl der Rückgabewert des Ausdrucks, als auch die Typangaben in der Parameterliste werden durch Typinferenz automatisch abgeleitet und können daher weggelassen werden.

Für die Zieltypen der Lambdaausdrücke stellt das Paket java.util.function17 genügend generische funktionale Interfaces zur Verfügung. Auch hier sollten die Schülerinnen und Schüler direkt mit der Dokumentation arbeiten, um passende Interfaces zu einer Aufgabe zu finden.

Beispiele für Lambdaausdrücke18 :

Code Schnipsel Erläuterung Interface mit funktionaler Methode Typ des formalen Parameters
( File f ) -> {
                      return f.isFile();}
                   
Bei Anweisungen müssen geschweifte Klammern gesetzt werden.
Predicate<T>{
                          boolean test(T t)}
                         Predicate<File>
                       
double x -> 2 * x
                     
Bei Ausdrücken entfallen die rechten geschweiften Klammern. Bei nur einem Argument zudem die runden Klammern.
ToDoubleFunction<T>{
                            double applyAsDouble(
                             T value)}
                           ToDoubleFunction<double>
                         
() -> "blupp"
                           
Eine leere Parameterliste ist erlaubt.
Supplier<T>{ T get()}
                              Supplier<String>
                              
( int x, int y ) -> x + y
                                 
Es können mehrere Parameter deklariert werden.
IntBinarayOperator{
                                   int applyAsInt(int
                                  left,
                                   int right)}
                                  IntBinaryOperator
( x, y ) -> x + y
                                       
Auch bei mehreren Parametern können die Typangaben entfallen.
( x, y ) ->
                                    (x>y )?x:y
                                 
Der ?:-Operator gilt als ein einziger Ausdruck und die rechten geschweiften Klammern entfallen.
( x, y ) -> {
                                          if( x > y )return x;
                                          if( y > x ) return y;
                                          return 0;}
                                       
Anweisungssequenzen sind möglich.

Lambdaausdrücke können in mehreren Kontexten verwendet werden, z.b. in Zuweisungen, Methodenaufrufen, und Typkonvertierungen:

// Zuweisungs-Kontext
Predicate<String> p = String::isEmpty;
// Methodenaufruf-Kontext stream.filter(e -> e.getSize() > 10)...
// Cast-Kontext (Typkonvertierung) stream.map((ToIntFunction) e -> e.getSize())...

Iteration mit Lambda-Ausdrücken

Die Iterable Schnittstelle schreibt folgende Methode vor, welche als Wertparameter das funktionale Interface Consumer<T> deklariert:

void forEach(Consumer<? super T> action)

Diese Methode kann demnach mit einem Lambdaausdruck aufgerufen werden. Dabei wird der Lambdaausdruck auf jedes Element der Sammlung (welche die Schnittstelle implementiert) angewandt.

Für folgendes Beispiel sei eine Sammlung Collection<String> c; beliebiger Zeichenketten gegeben, welche der Reihe nach ausgegeben werden sollen.

c.forEach( s → { System.out.println( "Wert= " + s ) } );

Mit dem Lambdaausdruck wird der Code für die Iteration über die Sammlung sehr einfach und zudem für einen Menschen gut lesbar.

Sollen beispielsweise für eine FilmsammlungGeneric<Film> fsG die Titel aller enthaltenen Filme ausgegeben werden geht das mit einer einzigen Zeile Code:

fsG.forEach(f -> { System.out.println( f.getTitel() ); });

Sollte beim Entwickeln der Filmdatenbank FilmsammlungGeneric auf eine andere Collection (z.B. LinkedList statt ArrayList) umgestellt werden, dann funktioniert das Beispiel immer noch und zwar ohne Änderung der Syntax. Sollen statt dem Titel die Bewertungen aller Filme ausgegeben werden, muss nicht etwa FilmsammlungGeneric um eine Methode hierfür ergänzt, sondern lediglich der Lambdaausdruck angepasst werden. Dies erhöht die Wiederverwendbarkeit und Flexibilität von Code drastisch.

Streams

Neben den Lambdaausdrücken wurden mit Java Version 8 mit dem Paket java.util.stream mächtige Schnittstellen für Operationen auf Reihungen und Sammlungen eingeführt. Da eine genaue Behandlung den Rahmen des Bildungsplans überschreitet, wird hier auf die offizielle Dokumentation verwiesen, die „Streams“ recht gut erklärt.

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/package-summary.html

Das Collection<E>Interface definiert die Methode Stream<E> stream(), welche die Elemente der Sammlung als Strom von Referenzen darstellt. Dieser erlaubt es, verkettete Operationen auf diesen Referenzen nacheinander oder parallel auszuführen. Wie bei funktionaler Programmierung werden die Daten, die durch die Referenzen repräsentiert werden, durch den Stream selbst nicht verändert.19

Von der Schnittstelle Stream<E> wird keine Implementierung mitgeliefert. Dies braucht es auch nicht, da die von ihr definierten Methoden meist Lambdaausdrücke übergeben bekommen. Die Methoden werden in zwei Hauptkategorien eingeteilt:

  • intermediäre Operationen (intermediate operations) liefern wiederum einen Stream, der weiterverarbeitet werden kann (z.B. filter(), map(), distinct(), sorted(), etc.).
  • terminale Operationen (terminal operations) führen ihrerseits Operationen auf den Referenzen des Streams aus (forEach(), reduce(), toArray(), etc.). Sie können einen Wert liefern und schließen den Strom. Danach können keine weiteren Operationen auf ihm ausgeführt werden.

Nehmen wir beispielsweise eine FilmsammlungGeneric<Film> fsG, kann der Filmtitel mit der schlechtesten Bewertung in einer Zeile Code bestimmt werden:

fsG.stream().reduce(fsG.get(0),
       (min, t) -> (t.getBewertung() < min.getBewertung()) ? t : min)
     ).getTitel();

Oft werden die Methoden nach dem Muster Filter-Map-Reduce auf einen Stream angewandt.

  • Filter: Zunächst werden gewünschte Elemente aus dem Stream ausgewählt. Zum Beispiel mit filter()
  • Map: Die Elemente des Streams werden transformiert. Zum Beispiel mit map(), mapToInt()
  • Reduce: Der Stream wird auf ein Endergebnis reduziert. Zum Beispiel mit reduce(), sum()

Die Anzahl aller Filme der Sammlung mit einer Bewertung besser als 7.0 erhält man mit:

fsG.stream().filter( f -> f.getBewertung()>7.0 ).mapToInt(f -> 1).sum() ); 

 

15 Siehe auch Fußnote 1 auf Seite 2.

16 Unter Boilerplate werden hier Codeschnipsel ohne eigene Funktionalität verstanden, welche beinahe unverändert an vielen Stellen im Code eingefügt werden müssen.

17 https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/function/package-summary.html (ausgewertet am 22.12.20)

18 https://www.torsten-horn.de/techdocs/java-lambdas.htm (ausgewertet am 22.12.2020) ergänzt um rechte Spalte.

19 https://javabeginners.de/Arrays_und_Verwandtes/Streams.php (ausgewertet am 22.12.2020)

 

Hintergrundinformationen: Herunterladen [odt][370 KB]

Hintergrundinformationen: Herunterladen [pdf][480 KB]

 

Weiter zu Quellen