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 der Collection, 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.de", 500));
communities.add(new Community("Java-Blog-Buch.de", 20));
communities.add(new Community("Java-Forum.org", 10000));
communities.add(new Community("Developers-Guide.net", 500));

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;
}&#91;/sourcecode&#93;

Nun stellt sich die Frage, wie die Attribute eines Objekts (in <code>coll</code>) mit ihrem Namen als Zeichenkette angesprochen, und ihr Wert verglichen werden kann. Hierzu muss <a href="#" title="In Arbeit">Reflection</a> eingesetzt werden.

Als erstes wird die Klasse benötigt, in der sich die Felder befinden. Unsere Methode hat zwar einen generischen Typen (<code>T</code>), da Generics aber zur Laufzeit nicht mehr existieren, ist es so nicht möglich die Zielklasse zu ermitteln (siehe <a href="http://www.java-blog-buch.de/0602-generics/">Kapitel 06.02 Generics</a>). Deshalb behelfen wir uns eines Tricks: Die <code>Collection</code> 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 <a href="http://www.java-blog-buch.de/0604-annotation/">Annotation</a> <code>@SuppressWarnings</code> versehen, da beim Casten sonst vom Compiler eine Warnmeldung ausgegeben werden würde.

@SuppressWarnings("unchecked")
Class<T> cls = (Class<T>)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 && !f.get(t).equals(find.get(f.getName()))) {
      match = false;
      break;
    }
    else if (!andMatch && 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 <T> ArrayList<T> find(Collection<T> coll, Map<String, ?> find) throws IllegalArgumentException, SecurityException, IllegalAccessException, NoSuchFieldException {
    return find(coll, find, true);
  }

  public <T> ArrayList<T> find(Collection<T> coll, Map<String, ?> find, boolean andMatch) throws IllegalArgumentException, IllegalAccessException, SecurityException, NoSuchFieldException {

    ArrayList<T> result = new ArrayList<T>();
    if (coll.size() < 1) {
      return result;
    }
    @SuppressWarnings("unchecked")
    Class<T> cls = (Class<T>)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 && !f.get(t).equals(find.get(f.getName()))) {
          match = false;
          break;
        }
        else if (!andMatch && 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<Community> communities = new ArrayList<Community>();
communities.add(new Community("Byte-Welt.de", 500));
communities.add(new Community("Java-Blog-Buch.de", 20));
communities.add(new Community("Java-Forum.org", 10000));
communities.add(new Community("Developers-Guide.net", 500));

HashMap<String, Object> find = new HashMap<String, Object>();

find.put("mitglieder", 500);
List<Community> result = finder.find(communities, find);
System.out.println(result.size());
for (Community v : result) {
  System.out.println(v);
}

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.de 500
Developers-Guide.net 500
1
Byte-Welt.de 500
2
Byte-Welt.de 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<T> {
  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 <T> ArrayList<T> find(Collection<T> coll, FindWith<T> find) {

    ArrayList<T> result = new ArrayList<T>();
    for (T t : coll) {
      if (find.match(t)) {
        result.add(t);
      }
    }
    return result;
  }

  public static interface FindWith<T> {
    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<Community> communities = new ArrayList<Community>();
communities.add(new Community("Byte-Welt.de", 500));
communities.add(new Community("Java-Blog-Buch.de", 20));
communities.add(new Community("Java-Forum.org", 10000));
communities.add(new Community("Developers-Guide.net", 500));

Finder.FindWith<Community> fw = new Finder.FindWith<Community>() {

  @Override
  public boolean match(Community check) {
    return check.getMitglieder() > 450;
  }
};

result = finder.find(communities, fw);
System.out.println(result.size());
for (Community v : result) {
  System.out.println(v);
}

Ausgabe:

3
Byte-Welt.de 500
Java-Forum.org 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.

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.