4.10Datenstrukturen mit Änderungsmeldungen
Datenstrukturen aus den Paketen java.util und java.util.concurrent, wie ArrayList, HashSet, TreeMap, ConcurrentHashMap, haben die Aufgabe, Daten zu verwalten und Zugriffe zur Verfügung zu stellen. Was diese Datenstrukturen (und Arrays) nicht können, ist das Melden von Veränderungen. Wenn zum Beispiel eine Standardsammlung ein neues Element bekommt, kann sich kein Interessent bei der Datenstruktur anmelden, der über die Neuerung informiert wird und zum Beispiel das neue Element verarbeitet. Was wir uns wünschen, ist eine Listener-Benachrichtigung über alle Änderungen an der Sammlung an sich (nicht an den Elementen in der Sammlung).
4.10.1Das Paket javafx.collections
Im Zuge von JavaFX sind neue Datenstrukturen zur Java SE gestoßen, allerdings finden sich diese nicht im Paket java.util, sondern im Paket javafx.collections inklusive eines Unterpakets javafx.collections.transformation. Die neuen Datenstrukturen erweitern die java.util-Datenstrukturen List, Set und Map und können Veränderungen melden.
Tabelle 4.5Alle Typen des Pakets javafx.collections[.transformation]
Die Schnittstelle ObservableList erweitert java.util.List, ObservableSet erweitert java. util.Set und ObservableMap erweitert java.util.Map. Somit bieten die neuen ObservableXXX-Typen mindestens die gleichen Methoden wie die bekannten Basistypen.
4.10.2Fabrikmethoden in FXCollections
Alle änderungsmeldenden Datenstrukturen sind nur über die genannten ObservableXXX-Schnittstellen beschrieben, und es gibt keine sichtbaren implementierenden Klassen, die über einen Konstruktor angelegt werden. Als Erzeuger der Typen deklariert FXCollections genau zehn Fabrikmethoden, die alle mit dem Präfix observable beginnen:
static <E> ObservableList<E> observableArrayList()
static <E> ObservableList<E> observableArrayList(E... items)
static <K,V> ObservableMap<K,V> observableHashMap()
static <E> ObservableSet<E> observableSet(E... elements)
Die Datenstrukturen lassen sich über die statischen Methoden leer oder bereits gefüllt anlegen, wobei die Konstruktoren dann Elemente direkt über eine variable Argumentliste annehmen.
Eine weitere Möglichkeit ist, existierende Sammlungen zu übergeben (und natürlich auch andere änderungsmeldende Sammlungen) und auf diese Weise die änderungsmeldende Sammlung zu initialisieren:
static <E> ObservableList<E> observableArrayList(Collection<? extends E> col)
static <E> ObservableList<E> observableList(List<E> list)
static <E> ObservableList<E> observableList(List<E> list, Callback<E,Observable[]> extractor)
static <E> ObservableSet<E> observableSet(Set<E> set)
static <K,V> ObservableMap<K,V> observableMap(Map<K,V> map)
static <E> ObservableList<E> observableArrayList(Callback<E,Observable[]> extractor)
Die letzte Methode in der Liste tanzt etwas aus der Reihe, da sie auf den Typ javafx.beans.Callback und javafx.beans.Observable (nicht java.util.Observable) aufbaut und somit eine Abhängigkeit zu einem anderen JavaFX-Paket hat.
[»]Hinweis
Keine der Fabrikmethoden schreibt direkt auf die darunterliegende Sammlung durch. Es sind also keine Wrapper. Mit der übergebenen Sammlung wird lediglich die neue Datenstruktur initialisiert, um am Anfang keine Ereignisse auszulösen.
Eine Methode observableTreeMap(…) fehlt, da hier ein Sortierkriterium nötig wäre; wer einen geordneten Assoziativspeicher benötigt, kann so etwas wie FXCollections.observableMap(new TreeMap<>(comparator)) einsetzen.
Überhaupt ist interessant, dass die Methodenamen auf der einen Seite verraten, welche Implementierung eingesetzt wird (observableArrayList) und dann wiederum nicht (observableSet), wobei hinter Letzterem ein HashSet steht, die Elemente also daher eine gültige hashCode()-Methode implementieren müssen, auch wenn sie sonst überhaupt nichts mit Hashing zu tun haben. Wer ein änderungsmeldendes TreeSet erzeugen möchte, nutzt den gleichen Trick wie auch bei der TreeMap, verwendet also FXCollections.observableSet(new TreeSet<>(comparator)).
[»]Hinweis
Änderungsmeldende Datenstrukturen können natürlich auch geschachtelt werden. Es spricht nichts dagegen, mehrere ObservableListen in eine ObservableList zu setzen, um so eine Tabelle nachzubauen. Allerdings heißt das nicht, dass eine Änderung an der »inneren« Liste zur Meldung der äußeren Liste führt.
4.10.3Änderungen melden über InvalidationListener
An änderungsmeldenden Datenstrukturen lassen sich zwei Arten von Listenern setzen, die über Änderungen informieren:
Zunächst erweitern alle ObservableXXX-Schnittstellen die JavaFX-Schnittstelle javafx.beans. Observable und lassen sich so mit einem InvalidationListener beobachten. Das dient dazu, herauszufinden, ob sich ein beobachteter Wert (in diesem Fall die Datenstruktur) irgendwie verändert hat. Das sagt aber nichts über die genaue Veränderung aus. Meldungen dieser Art reichen aber oft aus und sind unkompliziert. Anwendungen gibt es viele: Nehmen wir an, wir stellen ein geometrisches Objekt auf dem Bildschirm dar. Das Objekt selbst kann aus einer Liste von Punkten bestehen. Ändert sich die Liste, reicht ein an die Liste angehängter InvalidationListener aus, um dort bei einer Änderung den Befehl zum Neuzeichnen zu geben.
Der InvalidationListener informiert nur, dass die Datenstruktur »invalide« ist, aber nicht, was genau mit der Datenstruktur passiert ist. Um das herauszufinden, etwa an welcher Stelle etwas in einer Liste passiert ist, gibt es für die ObservableXXX-Typen extra XXXChangeListener mit sehr vielen Statusinformationen. Die versendeten Change-Objekte sind etwas teuer in der Herstellung, sodass sie nur dann zum Einsatz kommen sollten, wenn die feinen Informationen auch verwendet werden, sonst ist ein InvalidationListener besser.
4.10.4Änderungen melden über XXXChangeListener
Im Folgenden schauen wir uns erst die Behandlung der änderungsmeldenden Datenstrukturen an, Arrays werden folgen. Drei ObservableXXX-Typen bieten eigene überladene Methoden addListener(…)/removeListener(…) für XXXChangeListener, die die genauen Änderungen in den Datenstrukturen melden:
Tabelle 4.6Alle addListener(…)/removeListener(…)-Methoden von drei ObservableXXX-Typen
Argumente der xxxListener(…)-Methoden sind XXXChangeListener-Objekte. Werden die Listener hinzugefügt und folgt dann eine Änderung an der Datenstruktur, wird sie diese Änderung an den Listener melden. Es können beliebig viele Listener hinzugefügt werden.
Schauen wir uns diese XXXChangeListener-Schnittstellen genauer an (WeakXXXChangeListener außen vor gelassen); sie verfügen alle über genau eine onChanged(…)-Methode:
XXXChangeListener-Typ | void-Methode |
---|---|
ListChangeListener | onChanged(ListChangeListener.Change<? extends E> c) |
SetChangeListener | onChanged(SetChangeListener.Change<? extends E> change) |
MapChangeListener | onChanged(MapChangeListener.Change<? extends K,? extends V> change) |
Tabelle 4.7onChanged(…)-Methoden der XXXChangeListener
4.10.5Change-Klassen
Drei XXXChangeListener-Schnittstellen haben eine statische innere Klasse, genannt Change, welche genaue Informationen über die Änderungen verrät, also etwa, was hinzukam oder entfernt wurde.
[zB]Beispiel
Fülle eine ObservableMap mit allen Einträgen aus den System-Properties. Mache drei Änderungen, und lass einen Listener die Änderungen protokollieren:
MapChangeListener<Object, Object> listener = change -> System.out.println( change );
observableMap.addListener( listener );
observableMap.put( "name", "chris" ); // added chris at key name
observableMap.put( "name", "sha-sha" ); // replaced chris by sha-sha at key name
observableMap.remove( "name" ); // removed sha-sha at key name
Hinter jeder XXXChangeListener.Change-Klasse stehen diverse Abfragemethoden, die uns genau sagen, was passiert ist und was modifiziert und verändert wurde:
Klasse | Methoden |
---|---|
ListChangeListener.Change<E> | boolean wasAdded() |
SetChangeListener.Change<E> | abstract boolean wasAdded() |
MapChangeListener.Change<K,V> | abstract boolean wasAdded() |
Tabelle 4.8Abfragemethoden der Change-Ereignis-Klassen
Alle drei Change-Klassen deklarieren eine getXXX()-Methode, die die Datenstruktur selbst wieder hergibt. Das Objekt ListChangeListener.Change verfügt über die meisten Informationen, was ganz einfach daran liegt, dass das Ereignis mehrere geänderte Elemente beschreiben kann, während bei der Menge und dem Assoziativspeicher höchstens eine Veränderung bei einem Element stattfindet. Bei einer Liste kann eine Teilliste komplett hinzukommen – was dann getAddedSubList() erfragbar macht. So etwas ist bei einer Menge oder einem Assoziativspeicher nicht möglich. Hier werden mehrere Elemente auch über mehrere Ereignisse gemeldet.
Während die meisten Methoden durch ihren Namen klar beschrieben sind, gilt das nicht für alle Methoden, insbesondere von ListChangeListener.Change:
wasPermutted() zeigt Permutationen an, wie sie bei einer Durchmischung oder Sortierung stattfinden. wasReplaced() zeigt an, dass es eine Ersetzung gab, wasRemoved(), wenn ein Element aus der Liste genommen wurde, und wasAdded() wenn ein Element hinzukam.
War die Änderung eine Permutation, so lässt sich eine Schleife bauen der Art for ( int i = change.getFrom(); i < getTo(); i++) und für den Index i mit change.getPermutation(i) bestimmen, wohin ihn die Permutation am Ende verschoben hat; das heißt, dass Element an der Stelle i wanderte an die Stelle change.getPermutation(i).
Ein Änderungsereignis kann mehrere Vorgänge repräsentieren. Es gibt wie beim Iterator eine Art Cursor, den next() weitersetzt und reset() zurücksetzt. Es ist so lange ein next() gültig, bis die Rückgabe false() ergibt. Um alle Änderungen abzuarbeiten, ist eine Iteration der Art while ( change.next() ) { … } nötig.
[zB]Beispiel
Die wasXXX()-Methoden müssen in einer speziellen Reihenfolge ausgewertet werden. Am Anfang steht der Test auf Permutation, dann folgt der Test auf Ersetzung, dann Entfernung und zum Schluss Ergänzung. In einem Beispiel wollen wir 1, 2, 3 in eine Liste setzen, dann 1, 3 entfernen und sehen, wie der Durchlauf durch die Änderungen arbeitet:
ListChangeListener<Integer> listener = change -> {
while ( change.next() ) {
if ( change.wasPermutated() )
System.out.println( "permutted" );
else if ( change.wasReplaced() )
System.out.println( "replaced" );
else if ( change.wasRemoved() )
System.out.printf( "removed [%d,%d]%n", change.getFrom(), change.getTo() );
else if ( change.wasAdded() )
System.out.println( "added" );
}
};
list.addListener( listener );
System.out.println( list );
list.removeAll( 1, 3 );
System.out.println( list );
Die Ausgabe ist:
removed [0,0]
removed [1,1]
[2]
An der zweimaligen Ausgabe von »removed« lässt sich ganz gut der Durchlauf der Schleife ablesen und zudem die Position, die next() beim Change-Objekt verändert.
In den Change-Objekten gibt es viele Informationen, so dass auf diese Weise zwei Sammlungen synchron gehalten werden können: Gibt es in einer beobachteten Liste Änderungen, kann ein eigener XXXChangeListener die durchgeführten Operationen auf eine andere Sammlung spielen und so synchron halten. Meine Leser können als Übung so etwas einmal implementieren.
4.10.6Weitere Hilfsmethoden einer ObservableList
Die ObservableXXX-Typen bieten alle die Methoden addListener(…)/removeListener(…), doch ObservableList hat als einzige von den drei Schnittstellen noch ein paar nützliche Extra-Methoden, weil ObservableList in der Praxis – zumindest bei JavaFX-Anwendungen – am häufigsten eingesetzt wird.
extends java.util.List<E>, Observable
boolean addAll(E... elements)
void remove(int from, int to)
boolean removeAll(E... elements)
boolean retainAll(E... elements)
boolean setAll(Collection<? extends E> col)
boolean setAll(E... elements)
Die Hilfsmethoden bieten auf den ersten Blick nichts wirklich Neues, haben aber zwei Vorteile. Erstens sind sie komfortabler, denn ein list.remove(from, to) ist allemal kürzer als list.sublist(from, to).clear(). Der zweite Vorteil der Methoden ist die Anzahl der Änderungen, die sie verschicken. Ein observablelist.addAll(…) wird nur ein Change-Event produzieren, aber ein Collections.addAll(observablelist, …) ist eine Kaskade von add(…)-Aufrufen, was also eine Reihe von Ereignissen nach sich zieht.[ 54 ](Die Implementierung ist: public static <T> boolean addAll(Collection<? super T> c, T... elements) { boolean result = false; for (T element : elements) result |= c.add(element); return result; }) Das Gleiche gilt für setAll(…), was erst löscht und dann hinzufügt, aber nur ein Ereignis auslöst.
Die anderen beiden ObservableXXX-Typen für die java.util-Datenstrukturen bieten bisher keine solche Hilfsmethoden, sondern haben nur die Methoden zum An-/Abmelden der Listener.
4.10.7Melden von Änderungen an Arrays
Die Datenstrukturen der Collection-API sind schön über Schnittstellen gekapselt, weshalb es einfach ist, hier einen Stellvertreter mit der gleichen API hinzustellen, der im Hintergrund Ereignisse meldet. Bei Arrays geht das aber nicht, da hier ein Feldzugriff über eine Sprachsyntax gegeben ist und nicht über einen Methodenaufruf. Ein Beobachten von Array-Schreibzugriffen lässt sich also nur über einen neuen Typ realisieren, der genau wie ArrayList Methoden anbietet, die auf die Feldwerte schreiben.
JavaFX bietet änderungsmeldende Arrays für Ganzzahlen vom Typ int und Fließkommazahlen vom Typ float über zwei Hilfstypen ObservableIntegerArray und ObservableFloatArray.
static ObservableIntegerArray observableIntegerArray()
static ObservableIntegerArray observableIntegerArray(int... values)
static ObservableIntegerArray observableIntegerArray(ObservableIntegerArray array)
static ObservableFloatArray observableFloatArray()
static ObservableFloatArray observableFloatArray(float... values)
static ObservableFloatArray observableFloatArray(ObservableFloatArray array)
Beide Schnittstellen ObservableIntegerArray und ObservableFloatArray erweitern die Schnittstelle ObservableArray, was gemeinsame Methoden wie das An-/Abmelden von Listenern vorschreibt sowie die Methoden size()und clear(). Die typbezogenen Schnittstellen ObservableIntegerArray und ObservableFloatArray haben die erwarteten Methoden wie get(int index) oder set(int index).
Bei der Ereignisbehandlung sind die Array-Typen etwas einfacher gestrickt, denn es gibt kein Ereignisobjekt. Stattdessen meldet der ArrayChangeListener bei Aufruf von onChanged(T observableArray, boolean sizeChanged, int from, int to) alle Details, mehr Informationen gibt es nicht.
4.10.8Transformierte FXCollections
Insbesondere auf grafischen Oberflächen werden Listen von Einträgen oftmals gefiltert oder sortiert dargestellt. Das ist aber in der Regel nur eine Frage der Darstellung, und die darunterliegende Datenstruktur ist natürlich vollständig und unsortiert. Um jetzt bei Änderungen so einer unterliegenden Datenstruktur komfortabel zu einer gefilterten und sortierten Darstellung zu kommen, bietet JavaFX zwei Implementierungen für spezielle ObservableList-Wrapper an: FilteredList und SortedList. Die erste Klasse enthält nur Elemente, die einem Predicate genügen (eine Schnittstelle aus java.util.function), und die zweite Klasse SortedList sortiert nach einem Comparator. FilteredList und SortedList sind selbst auch wieder vom Typ Observable und ObservableList, sodass sie sich problemlos schachteln lassen. Ungewöhnlich ist, dass die Klassen mittels Konstruktor erzeugt werden und nicht über die Fabrikmethoden in FXCollections.
[zB]Beispiel
Eine observableList soll auf dem Bildschirm sortiert dargestellt werden, aber keinen Leer-String enthalten:
ObservableList<String> filteredList = new FilteredList<>(
observableList, s -> ! s.isEmpty() );
ObservableList<String> sortedList = new SortedList<>( filteredList, Comparator.naturalOrder() );
observableList.addAll( "", "Hausschwein", "Buttercup" );
System.out.println( sortedList ); // [Buttercup, Hausschwein]
Änderungen an observableList werden nun automatisch weitergeleitet, so dass der Beobachter sortedList diese Veränderung mitbekommt und die Liste neu darstellen kann.
4.10.9Weitere statische Methoden in FXCollections
Von FXCollections haben wir bisher die Fabrikmethoden für die ObservableXXX-Typen kennengelernt. Die Klasse bietet aber Hilfsmethoden, die an java.util.Collections angelehnt sind:
static <E> ObservableList<E> checkedObservableList(ObservableList<E> list, Class<E> type)
static <K,V> ObservableMap<K,V> checkedObservableMap(ObservableMap<K,V> map, Class<K> keyType, Class<V> valueType)
static <E> ObservableSet<E> checkedObservableSet(ObservableSet<E> set, Class<E> type)
static <E> ObservableList<E> concat(ObservableList<E>... lists)
static <T> void copy(ObservableList<? super T> dest, List<? extends T> src)
static <E> ObservableList<E> emptyObservableList()
static <K,V> ObservableMap<K,V> emptyObservableMap()
static <E> ObservableSet<E> emptyObservableSet()
static <T> void fill(ObservableList<? super T> list, T obj)
static <T> boolean replaceAll(ObservableList<T> list, T oldVal, T newVal)
static void reverse(ObservableList list)
static void rotate(ObservableList list, int distance)
static void shuffle(ObservableList<?> list)
static void shuffle(ObservableList list, Random rnd)
static <E> ObservableList<E> singletonObservableList(E e)
static <T extends Comparable<? super T>> void sort(ObservableList<T> list)
static <T> void sort(ObservableList<T> list, Comparator<? super T> c)
static <E> ObservableList<E> synchronizedObservableList(ObservableList<E> list)
static <K,V> ObservableMap<K,V> synchronizedObservableMap(ObservableMap<K,V> map)
static <E> ObservableSet<E> synchronizedObservableSet(ObservableSet<E> set)
static <E> ObservableList<E> unmodifiableObservableList(ObservableList<E> list)
static <K,V> ObservableMap<K,V> unmodifiableObservableMap(ObservableMap<K,V> map)
static <E> ObservableSet<E> unmodifiableObservableSet(ObservableSet<E> set)
Die Methoden sind alleine deshalb schon wichtig, weil sie die Anzahl an gemeldeten Änderungen reduzieren. Würde etwa Collections.shuffle(anObservableList) aufgerufen, würde es an Ereignissen nur so rappeln, denn jede Umpositionierung ist ein set(…), und das ist ein Ereignis (zumindest könnte eine Implementierung das so machen). FXCollections.shuffle(anObservableList) führt jedoch zu nur einem Ereignis.