Das Arbeiten mit Collections bzw. insbesondere Liste gehört zum täglichen Brot eines Entwicklers – sei es etwa um eine Ergebnisliste zu sortieren, einzelne Werte eines Sets zu aggregieren oder etwa um die Rückgabe eines Services nach speziellen Kriterien zu filtern.
Und weil diese Probleme so alttäglich sind, handeln wir sie auch immer mit den gleichen Codeblöcken ab – ohne einmal links und rechts zu schauen. Oft bieten aber genau hier Libraries Lösungen an, die unser Code an der Stelle lesbarer und wartbarer machen können.
Im folgenden möchte ich mich dem Problem widmen, aus einer Collection von Objekten, ein Subset mit speziellen Eigenschaften herauszufiltern.
Unsere Domainklasse ist die der Person:
public class Person {
private long id;
private String name;
private int age;
private boolean married;
// getters and setters
// ...
}
Wir haben einen Service, der uns eine Collection aller verfügbarer Personen im System zurückliefert.
public interface PersonService {
Collection findAllPersons();
}
Uns interessieren jedoch nur die unverheirateten Personen, die älter sind als 18 und deren Id zwischen 1000 und 2000 liegt. Der Service ist nicht erweiterbar, so dass wir die Collectionnachträglich filtern müssen.
Die sicherlich naheliegenste Variante ist es, über alle Elemente zu iterieren
und die einschränkenden Kriterien abzufragen:
public Collection getFilteredCollection() {
Collection persons = service.findAllPersons();
Collection filtered = new ArrayList();
for(Person p : persons) {
if(p.isMarried()) {
continue;
}
if(p.getAge() < 18){
continue;
}
if(p.getId() < 1000 || p.getId() > 2000) {
continue;
}
filtered.add(p);
}
return filtered;
}
Ich denke, das Codesnippet braucht keine weitere Erklärung. Jeder hat soetwas schon mindestens einmal geschrieben.
Jedes weitere Kriterium macht die Methode nicht lesbarer.
Das ehemals unter dem Namen Google Collections verfügbare Projekt ist mittlerweile
im Guava-Projekt aufgegangen – ein umfangreiche Java-Library die in etlichen Google Projekten zum Einsatz kommt.
In Guava wird die einschränkende Logik mittels Predikaten (Predicates) umgesetzt.
// ...
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
// ...
public Collection getFilteredCollection() {
Collection allPersons = service.findAllPersons();
return Collections2.filter(allPersons,
Predicates.and(
new AgePredicate(),
new MarriedPredicate(),
new IdPredicate()));
}
private static class AgePredicate implements Predicate {
@Override
public boolean apply(Person person) {
return person.getAge() > 18;
}
}
private static class MarriedPredicate implements Predicate {
@Override
public boolean apply(Person person) {
return !person.isMarried();
}
}
private static class IdPredicate implements Predicate {
@Override
public boolean apply(Person person) {
return person.getId() >= 1000 && person.getId() }
}
Was direkt auffällt, ist, dass der resultierende Code länger ist als die direkte Implementierung (es sei denn man implementiert die Predicates mittels anonymer Klassen). Aber mehr Code bedeutet ja nicht, dass dieser schlechter ist. Denn die Umsetzung der Businesslogik ist deutlich übersichtlicher (Zeile 10-14).
Die Abbildung über Predicates erlaubt etwa – gerade bei komplizierteren Abfragen – diese gesondert unitzutesten bzw. dann auch in verschiedenen Kontexten wiederzuverwenden. Denkbar ist auch, dass die Predikate wie Funktionen parallel auf den einzelnen Collection-Einträgen ausgeführt werden können.
Ein dritter, sehr eleganter Ansatz ist die vom LambdaJ-Projekt angebotene DSL, die eine pseudo-funktionale Schreibweise bietet.
//...
import static ch.lambdaj.Lambda.filter;
import static ch.lambdaj.Lambda.having;
import static ch.lambdaj.Lambda.on;
import static ch.lambdaj.function.matcher.AndMatcher.and;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.number.OrderingComparisons.greaterThan;
import static org.hamcrest.number.OrderingComparisons.lessThan;
// ...
public Collection<Person> getFilteredCollection() {
Collection<Person> allPersons = service.findAllPersons();
return filter(
having(on(Person.class).isMarried(), equalTo(true)
.and(having(on(Person.class).getAge(), greaterThan(18)))
.and(having(on(Person.class).getId(),
and(greaterThan(1000L), lessThan(2000L)))),
allPersons);
}
Zugegeben, wenn man die zwei anderen Beispiele gesehen (und insbesondere das if-then-else-Konstrukt im Kopf, wirkt diese Schreibweise erst etwas befremdlich. Allerdings kann man sehr schnell die Essenz herauslesen. – Dies auch durch die von der hamcrest-Library zur Verfügung gestellten Matcher, welche seit Version 4.4 auch in JUnit verwendet werden.
Schön ist, dass LambdaJ statisch typisiert ist und so ein leichtes Refactoring des Codes erlaubt.
Das Filtern von Collections ist nur eine Anwendung, die LambdaJ für die Arbeit mit Collections bietet: Weitere Features sind etwa das Setzen einer speziellen Property mit dem gleichen Wert auf allen Elementen, das Konvertieren von einzelnen Elementen, der Aufruf einer speziellen Methode auf jedem Collection-Element, Sortieren von Collections, etc.
Es lohnt sich oft, auch für simple alltägliche Probleme einmal zu recherchieren, ob nicht eine Library-Unterstützung elegantere, besser lesbarere Wege bietet, um das Problem zu lösen.
In dem gezeigten Fall sind sicherlich Guava und LambdaJ zwei Perlen, die man in seinen Werkzeug-Kasten aufnehmen sollte.
Have a look at Querydsl, a LINQ-like library to query data stores but also plain collections. It works best when generating a meta-model of your domain classes but ends up being the most concise one then: Your example would end up like this:
QPerson person = new QPerson("person"); // setup query meta classBooleanExpression isAdult = person.age.gt(18);
BooleanExpression isMarried = person.married.eq(true);
BooleanExpression idInRange = person.id.between(1000L, 2000L);
List source = // … get source collection
List result = from(person, source).where(isAdult.and(isMarried).and(idInRange));
Forgot the link to the documentation: http://www.querydsl.com/static/querydsl/2.5.0/reference/html/ch02s07.html
Hi Oliver, thanks for the hint.
But in your example you need an extra build-step for generating the meta-model.
I think that’s a little overkill for just filtering a
CollectionWouldn’t it be better to use the
Alias-feature here instead?