D) Collections durchsuchen
Während Ihrer Programmierer-Karriere wird es vermutlich das eine oder andere Mal vorkommen, dass Sie die Objekte in einer java.util.Collection
nach bestimmten Kriterien durchsuchen müssen. In diesem Kapitel lernen Sie einige Möglichkeiten kennen, um dieses Vorhaben in die Tat umzusetzen.
Collection contains
Beim Betrachten der API-Dokumentation fällt Ihnen sicherlich die Methode contains
im Interface Collection
auf. Damit lässt sich eine Collection
nach einem bestimmten Objekt durchsuchen. Bei bspw. einem String
ist das auch kein Problem:
Collection<string> coll = new ArrayList<string>(); coll.add("Eins"); coll.add("Zwei"); coll.add("Drei"); System.out.println(coll.contains("Eins")); System.out.println(coll.contains("Fünf"));
Ausgabe:
true
false
Sie werden jedoch feststellen, dass Sie schnell an die Grenzen dieser Methode stoßen. Sie können so bspw. nur feststellen, ob ein Element in einer Collection
enthalten ist, nicht aber wie oft und an welcher Stelle. Für eine solche Abfrage müsste die Collection
genauer spezifiziert (z. B. ArrayList
oder HashSet
), oder in ein Array (toArray
) umgewandelt werden. Und selbst dann bestehen noch Probleme:
- Es gibt keine allgemeine Methode, die eine Liste mit allen Objekten erstellt, auf die die Kriterien zutreffen.
- Befinden sich Objekte (keine primitive Datentypen und mit Ausnahme von
Strings
) in derCollection
, können diese zwar auf Gleichheit geprüft werden, es besteht aber keine Möglichkeit einzelne Attribute eines Objekts zu überprüfen.
Eine spezifische Collection durchsuchen
Eine spezifizierte Lösung je Anwendungsfall könnte durch manuelles prüfen jedes Elements in der Collection
realisiert werden. Als Beispiel nehmen wir eine Liste mit Communities. Jede Community hat eine Mitgliederzahl und einen Namen:
public class Community { private String name; private int mitglieder; public Community(String name, int mitglieder) { this.name = name; this.mitglieder = mitglieder; } public int getMitglieder() { return this.mitglieder; } public String getName() { return this.name; } public void setName(String name) { this.name = name; } public void setMitglieder(int mitglieder) { this.mitglieder = mitglieder; } public String toString() { return this.name + " " + this.mitglieder; } }
Wenn Sie nach der Community suchen wollen, die „Java-Blog-Buch.de“ heißt, könnte das im Spezifischen so aussehen:
ArrayList<community> communities = new ArrayList<community>(); communities.add(new Community("Byte-Welt.net", 500)); communities.add(new Community("Java-Blog-Buch.de", 20)); communities.add(new Community("wiki.byte-welt.net", 10000)); communities.add(new Community("Developers-Guide.net", 500));</community></community> for (Community c : communities) { if (c.getName().equals("Java-Blog-Buch.de")) { System.out.println(c); } }
Allerdings bräuchten Sie für jeden Suchvorgang eine eigene Abfrage, weshalb es erstrebenswerter ist, von der spezifischen Lösung eine allgemeine Lösung abzuleiten.
Collections allgemein durchsuchen
Wir benötigen also eine Methode, die eine Collection
(wird durchsucht) und die Kriterien, nach denen gesucht werden sollen, erwartet. Für letzteres bietet sich eine java.util.Map
an, die als Key den Attributnamen, und als Value das erwartete Objekt definiert. Zusätzlich sollte noch über einen boolean
festgelegt werden, ob alle Kriterien (And-Verknüpfung) erfüllt werden sollen, oder ob es reicht, wenn mindestens ein Kriterium erfüllt wurde (Or-Verknüpfung). Zurückgegeben wird eine Liste, die alle Treffer enthält. Ist die Collection
leer, wird eine leere Liste zurückgegeben.
public <t> ArrayList<t> find(Collection<t> coll, Map<string, ?=""> find, boolean andMatch) { ArrayList<t> result = new ArrayList<t>(); if (coll.size() < 1) { return result; } return result; }
Nun stellt sich die Frage, wie die Attribute eines Objekts (in coll
) mit ihrem Namen als Zeichenkette angesprochen, und ihr Wert verglichen werden kann. Hierzu muss Reflection eingesetzt werden.</string,>
Als erstes wird die Klasse benötigt, in der sich die Felder befinden. Unsere Methode hat zwar einen generischen Typen (T
), da Generics aber zur Laufzeit nicht mehr existieren, ist es so nicht möglich die Zielklasse zu ermitteln (siehe Kapitel 06.02 Generics). Deshalb behelfen wir uns eines Tricks: Die Collection
wird in ein Array umgewandelt. Über ein beliebiges Element in diesem Array kann nun die Klasse abgefragt werden. Diese Codezeile wird zusätzlich mit der Annotation @SuppressWarnings
versehen, da beim Casten sonst vom Compiler eine Warnmeldung ausgegeben werden würde.
@SuppressWarnings("unchecked") Class&lt;t&gt; cls = (Class&lt;t&gt;)coll.toArray()[0].getClass();
Der nächste Schritt besteht darin, die Attributnamen (die Keys der Map find
) als java.lang.reflect.Field
-Array zu speichern. Dies hat zum Einen den Grund, dass so überprüft werden kann, ob die Attribute auch wirklich in der Klasse existieren, und zum Anderen muss beim Test der einzelnen Objekte nicht jedes Mal ein neues Field
erzeugt werden. Zusätzlich wird die Kapselung der Attribute (die bei einer ordentlichen Programmierung wohl alle privat sein sollten) über die Methode setAccessible(true)
aufgebrochen, so dass die Felder direkt abgefragt werden können.
Field[] fields = new Field[find.size()]; int pos = 0; for (String s : find.keySet()) { fields[pos] = cls.getDeclaredField(s); fields[pos++].setAccessible(true); }
Evtl. auftretende Fehler werden über die throws
-Klausel des Methodenkopfs weitergereicht.
Hiermit sind Sie auch schon beim eigentlichen Suchalgorithmus angelangt. Es muss über alle Elemente der Collection coll
iteriert werden. Für jedes Element wird dann anhand des Field
-Arrays, der Attribute des Objekts in coll
, und der gewählten Verknüpfung (and oder or) überprüft, ob das Element den Anforderungen entspricht. Ist dies der Fall, wird es der Ergebnisliste hinzugefügt. Eine mögliche Implementierung sieht bspw. so aus:
boolean match = true; for (T t : coll) { match = andMatch; for (Field f : fields) { if (andMatch &amp;amp;&amp;amp; !f.get(t).equals(find.get(f.getName()))) { match = false; break; } else if (!andMatch &amp;amp;&amp;amp; f.get(t).equals(find.get(f.getName()))) { match = true; break; } } if (match) { result.add(t); } }
Hier noch einmal der komplette Quellcode zum Kopieren:
package de.jbb.tools; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.Map; public class Finder { public &lt;t&gt; ArrayList&lt;t&gt; find(Collection&lt;t&gt; coll, Map&lt;string, ?=""&gt; find) throws IllegalArgumentException, SecurityException, IllegalAccessException, NoSuchFieldException { return find(coll, find, true); }&lt;/string,&gt;&lt;/t&gt;&lt;/t&gt;&lt;/t&gt; public &lt;t&gt; ArrayList&lt;t&gt; find(Collection&lt;t&gt; coll, Map&lt;string, ?=""&gt; find, boolean andMatch) throws IllegalArgumentException, IllegalAccessException, SecurityException, NoSuchFieldException {&lt;/string,&gt;&lt;/t&gt;&lt;/t&gt;&lt;/t&gt; ArrayList&lt;t&gt; result = new ArrayList&lt;t&gt;(); if (coll.size() &amp;lt; 1) { return result; } @SuppressWarnings("unchecked") Class&lt;t&gt; cls = (Class&lt;t&gt;)coll.toArray()[0].getClass(); Field[] fields = new Field[find.size()]; int pos = 0; for (String s : find.keySet()) { fields[pos] = cls.getDeclaredField(s); fields[pos++].setAccessible(true); } boolean match = true; for (T t : coll) { match = andMatch; for (Field f : fields) { if (andMatch &amp;amp;&amp;amp; !f.get(t).equals(find.get(f.getName()))) { match = false; break; } else if (!andMatch &amp;amp;&amp;amp; f.get(t).equals(find.get(f.getName()))) { match = true; break; } } if (match) { result.add(t); } } return result; } }
Zum Testen können Sie folgenden Code verwenden:
Finder finder = new Finder(); ArrayList&lt;community&gt; communities = new ArrayList&lt;community&gt;(); communities.add(new Community("Byte-Welt.net", 500)); communities.add(new Community("Java-Blog-Buch.de", 20)); communities.add(new Community("wiki.byte-welt.net", 10000)); communities.add(new Community("Developers-Guide.net", 500));&lt;/community&gt;&lt;/community&gt; HashMap&lt;string, object=""&gt; find = new HashMap&lt;string, object=""&gt;();&lt;/string,&gt;&lt;/string,&gt; find.put("mitglieder", 500); List&lt;community&gt; result = finder.find(communities, find); System.out.println(result.size()); for (Community v : result) { System.out.println(v); }&lt;/community&gt; find.put("name", "Byte-Welt.de"); result = finder.find(communities, find); System.out.println(result.size()); for (Community v : result) { System.out.println(v); } result = finder.find(communities, find, false); System.out.println(result.size()); for (Community v : result) { System.out.println(v); }
Als Ausgabe erhalten Sie – wie erwartet
2
Byte-Welt.net 500
Developers-Guide.net 500
1
Byte-Welt.net 500
2
Byte-Welt.net 500
Developers-Guide.net 500
Aber auch diese Methode ist leider keine Eierlegende-Woll-Milch-Sau. Sie bekommen Probleme, wenn die Attribute nicht auf Gleichheit, sondern z. B. auf Größer/Kleiner-Als, oder Ungleichheit geprüft werden sollen, oder sobald auf Attribute und Methoden von Attributen des zu suchenden Objekts zugegriffen werden muss. Falls diese Kriterien für Sie nicht relevant sind, können Sie gerne die gerade vorgestellte Lösung verwenden. Ansonsten benötigen Sie eine flexiblere Möglichkeit eine Collection
zu durchsuchen.
Collections allgemein und flexibel durchsuchen
Bei dieser finalen Lösung habe ich mich von der Comparator
-Schnittstelle inspirieren lassen (siehe Kapitel D) Objekte sortieren – Comparator und Comparable). Es wird eine Schnittstelle benötigt, deren Implementierung festlegt, ob ein Objekt alle Kriterien erfüllt, um von der Suche gefunden zu werden:
public interface FindWith&lt;t&gt; { boolean match(T check); }
Anschließend wird noch eine Methode benötigt, die anhand dieser Schnittstelle eine Collection
durchsucht.
package de.jbb.tools; import java.util.ArrayList; import java.util.Collection; public class Finder { public &lt;t&gt; ArrayList&lt;t&gt; find(Collection&lt;t&gt; coll, FindWith&lt;t&gt; find) {&lt;/t&gt;&lt;/t&gt;&lt;/t&gt;&lt;/t&gt; ArrayList&lt;t&gt; result = new ArrayList&lt;t&gt;(); for (T t : coll) { if (find.match(t)) { result.add(t); } } return result; }&lt;/t&gt;&lt;/t&gt; public static interface FindWith&lt;t&gt; { public boolean match(T check); } }
Ein Suchvorgang würde dann so aussehen (liefert jede Community
, die mehr als 450 Mitglieder hat):
Finder finder = new Finder(); ArrayList&lt;community&gt; communities = new ArrayList&lt;community&gt;(); communities.add(new Community("Byte-Welt.net", 500)); communities.add(new Community("Java-Blog-Buch.de", 20)); communities.add(new Community("wiki.byte-welt.net", 10000)); communities.add(new Community("Developers-Guide.net", 500));&lt;/community&gt;&lt;/community&gt; Finder.FindWith&lt;community&gt; fw = new Finder.FindWith&lt;community&gt;() {&lt;/community&gt;&lt;/community&gt; @Override public boolean match(Community check) { return check.getMitglieder() &amp;gt; 450; } }; result = finder.find(communities, fw); System.out.println(result.size()); for (Community v : result) { System.out.println(v); }
Ausgabe:
3
Byte-Welt.net 500
wiki.byte-welt.net 10000
Developers-Guide.net 500
Der Programmieraufwand für eine Suche ist natürlich größer, als bei der vorherigen Methode. Allerdings hat man bei dieser Implementierung auch die volle Flexibilität.