Wie kann ich in Java 8 eine Sammlung mithilfe der Stream
-API filtern, indem die Unterscheidbarkeit einer Eigenschaft jedes Objekts überprüft wird?
Ich habe zum Beispiel eine Liste mit Person
-Objekten und möchte Personen mit demselben Namen entfernen.
persons.stream().distinct();
Verwendet die Standard-Gleichheitsprüfung für ein Person
-Objekt, also brauche ich so etwas wie
persons.stream().distinct(p -> p.getName());
Leider hat die distinct()
-Methode keine solche Überladung. Ist es möglich, die Gleichheitsprüfung in der Person
-Klasse zu ändern, ohne dies zu ändern?
Betrachten Sie distinct
als einen stateful-Filter. Hier ist eine Funktion, die ein Prädikat zurückgibt, das den Status des zuvor Gesehenen aufrechterhält und ob das angegebene Element zum ersten Mal gesehen wurde:
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
Dann kannst du schreiben:
persons.stream().filter(distinctByKey(Person::getName))
Beachten Sie, dass, wenn der Stream geordnet und parallel ausgeführt wird, ein arbitrary - Element aus den Duplikaten anstelle des ersten Elements beibehalten wird, wie dies bei distinct()
der Fall ist.
(Dies ist im Wesentlichen das Gleiche wie meine Antwort auf diese Frage: Java Lambda Stream Distinct () auf einem beliebigen Schlüssel? )
Eine Alternative wäre, die Personen auf einer Karte mit dem Namen als Schlüssel zu platzieren:
persons.collect(toMap(Person::getName, p -> p, (p, q) -> p)).values();
Beachten Sie, dass die Person, die im Falle eines doppelten Namens aufbewahrt wird, die erste gefunden wird.
Sie können die Personenobjekte in eine andere Klasse einschließen, die nur die Namen der Personen vergleicht. Anschließend wickeln Sie die verpackten Objekte aus, um wieder einen Personenstrom zu erhalten. Die Stream-Vorgänge könnten wie folgt aussehen:
persons.stream()
.map(Wrapper::new)
.distinct()
.map(Wrapper::unwrap)
...;
Die Klasse Wrapper
könnte folgendermaßen aussehen:
class Wrapper {
private final Person person;
public Wrapper(Person person) {
this.person = person;
}
public Person unwrap() {
return person;
}
public boolean equals(Object other) {
if (other instanceof Wrapper) {
return ((Wrapper) other).person.getName().equals(person.getName());
} else {
return false;
}
}
public int hashCode() {
return person.getName().hashCode();
}
}
Wir können auch RxJava (sehr leistungsfähige reaktive Erweiterung library) verwenden.
Observable.from(persons).distinct(Person::getName)
oder
Observable.from(persons).distinct(p -> p.getName())
Es gibt einen einfacheren Ansatz, ein TreeSet mit einem benutzerdefinierten Vergleicher zu verwenden.
persons.stream()
.collect(Collectors.toCollection(
() -> new TreeSet<Person>((p1, p2) -> p1.getName().compareTo(p2.getName()))
));
Eine andere Lösung mit Set
. Vielleicht nicht die ideale Lösung, aber es funktioniert
Set<String> set = new HashSet<>(persons.size());
persons.stream().filter(p -> set.add(p.getName())).collect(Collectors.toList());
Wenn Sie die ursprüngliche Liste ändern können, können Sie die Methode removeIf verwenden
persons.removeIf(p -> !set.add(p.getName()));
Sie können die distinct(HashingStrategy)
-Methode in Eclipse Collections verwenden.
List<Person> persons = ...;
MutableList<Person> distinct =
ListIterate.distinct(persons, HashingStrategies.fromFunction(Person::getName));
Wenn Sie persons
zur Implementierung einer Eclipse Collections-Schnittstelle umwandeln können, können Sie die Methode direkt in der Liste aufrufen.
MutableList<Person> persons = ...;
MutableList<Person> distinct =
persons.distinct(HashingStrategies.fromFunction(Person::getName));
HashingStrategy ist einfach eine Strategieschnittstelle, mit der Sie benutzerdefinierte Implementierungen von Gleichheitszeichen und Hashcode definieren können.
public interface HashingStrategy<E>
{
int computeHashCode(E object);
boolean equals(E object1, E object2);
}
Hinweis: Ich bin ein Committer für Eclipse-Sammlungen.
Ich empfehle, Vavr zu verwenden, wenn Sie können. Mit dieser Bibliothek können Sie Folgendes tun:
io.vavr.collection.List.ofAll(persons)
.distinctBy(Person::getName)
.toJavaSet() // or any another Java 8 Collection
Sie können StreamEx library verwenden:
StreamEx.of(persons)
.distinct(Person::getName)
.toList()
Sie können groupingBy
collector verwenden:
persons.collect(groupingBy(p -> p.getName())).values().forEach(t -> System.out.println(t.get(0).getId()));
Wenn Sie einen anderen Stream haben möchten, können Sie Folgendes verwenden:
persons.collect(groupingBy(p -> p.getName())).values().stream().map(l -> (l.get(0)));
Ich habe eine generische Version gemacht:
private <T, R> Collector<T, ?, Stream<T>> distinctByKey(Function<T, R> keyExtractor) {
return Collectors.collectingAndThen(
toMap(
keyExtractor,
t -> t,
(t1, t2) -> t1
),
(Map<R, T> map) -> map.values().stream()
);
}
Ein Beispiel:
Stream.of(new Person("Jean"),
new Person("Jean"),
new Person("Paul")
)
.filter(...)
.collect(distinctByKey(Person::getName)) // return a stream of Person with 2 elements, jean and Paul
.map(...)
.collect(toList())
Stuart Marks 'Antwort kann erweitert werden, dies kann auf kürzere Weise und ohne gleichzeitige Karte erfolgen (wenn Sie keine parallelen Streams benötigen):
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
final Set<Object> seen = new HashSet<>();
return t -> seen.add(keyExtractor.apply(t));
}
Dann ruf an:
persons.stream().filter(distinctByKey(p -> p.getName());
Set<YourPropertyType> set = new HashSet<>();
list
.stream()
.filter(it -> set.add(it.getYourProperty()))
.forEach(it -> ...);
Ein ähnlicher Ansatz, den Saeed Zarinfam verwendete, aber mehr Java 8-Style :)
persons.collect(groupingBy(p -> p.getName())).values().stream()
.map(plans -> plans.stream().findFirst().get())
.collect(toList());
Eine andere Bibliothek, die dies unterstützt, ist jOOλ und ihre Seq.distinct(Function<T,U>)
-Methode:
Seq.seq(persons).distinct(Person::getName).toList();
Unter der Haube tut es praktisch dasselbe wie die akzeptierte Antwort .
Die einfachste Möglichkeit, dies zu implementieren, besteht darin, die Sortierfunktion aufzurufen, da sie bereits eine optionale Comparator
enthält, die mit der Eigenschaft eines Elements erstellt werden kann. Dann müssen Sie Duplikate herausfiltern, was mit einer statefull Predicate
geschehen kann, die die Tatsache ausnutzt, dass für einen sortierten Stream alle gleichen Elemente nebeneinander liegen:
Comparator<Person> c=Comparator.comparing(Person::getName);
stream.sorted(c).filter(new Predicate<Person>() {
Person previous;
public boolean test(Person p) {
if(previous!=null && c.compare(previous, p)==0)
return false;
previous=p;
return true;
}
})./* more stream operations here */;
Natürlich ist eine statefull Predicate
nicht threadsicher. Wenn Sie dies jedoch benötigen, können Sie diese Logik in eine Collector
verschieben und den Stream für die Thread-Sicherheit sorgen, wenn Sie Ihre Collector
verwenden. Dies hängt davon ab, was Sie mit dem Strom verschiedener Elemente tun möchten, die Sie uns in Ihrer Frage nicht mitgeteilt haben.
Aufbauend auf der Antwort von @ josketres habe ich eine generische Methode erstellt:
Sie könnten dies Java 8-freundlicher machen, indem Sie einen Collector erstellen.
public static <T> Set<T> removeDuplicates(Collection<T> input, Comparator<T> comparer) {
return input.stream()
.collect(toCollection(() -> new TreeSet<>(comparer)));
}
@Test
public void removeDuplicatesWithDuplicates() {
ArrayList<C> input = new ArrayList<>();
Collections.addAll(input, new C(7), new C(42), new C(42));
Collection<C> result = removeDuplicates(input, (c1, c2) -> Integer.compare(c1.value, c2.value));
assertEquals(2, result.size());
assertTrue(result.stream().anyMatch(c -> c.value == 7));
assertTrue(result.stream().anyMatch(c -> c.value == 42));
}
@Test
public void removeDuplicatesWithoutDuplicates() {
ArrayList<C> input = new ArrayList<>();
Collections.addAll(input, new C(1), new C(2), new C(3));
Collection<C> result = removeDuplicates(input, (t1, t2) -> Integer.compare(t1.value, t2.value));
assertEquals(3, result.size());
assertTrue(result.stream().anyMatch(c -> c.value == 1));
assertTrue(result.stream().anyMatch(c -> c.value == 2));
assertTrue(result.stream().anyMatch(c -> c.value == 3));
}
private class C {
public final int value;
private C(int value) {
this.value = value;
}
}
Die Liste der einzelnen Objekte kann wie folgt gefunden werden:
List distnictPersons = persons.stream()
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person:: getName))),
ArrayList::new));
Mein Ansatz dabei ist, alle Objekte mit derselben Eigenschaft zusammenzufassen, dann die Gruppen auf die Größe 1 zu kürzen und sie schließlich als List
zu sammeln.
List<YourPersonClass> listWithDistinctPersons = persons.stream()
//operators to remove duplicates based on person name
.collect(Collectors.groupingBy(p -> p.getName()))
.values()
.stream()
//cut short the groups to size of 1
.flatMap(group -> group.stream().limit(1))
//collect distinct users as list
.collect(Collectors.toList());
Vielleicht wird es für jemanden nützlich sein. Ich hatte noch eine andere Anforderung. Mit der Liste der Objekte A
von Drittanbietern entfernen Sie alle, die dasselbe A.b
-Feld für denselben A.id
haben (mehrere A
-Objekte mit demselben A.id
in der Liste). Stream-Partition Antwort von Tagir Valeev inspirierte mich, benutzerdefinierte Collector
zu verwenden, die Map<A.id, List<A>>
zurückgibt. Einfach flatMap
erledigt den Rest.
public static <T, K, K2> Collector<T, ?, Map<K, List<T>>> groupingDistinctBy(Function<T, K> keyFunction, Function<T, K2> distinctFunction) {
return groupingBy(keyFunction, Collector.of((Supplier<Map<K2, T>>) HashMap::new,
(map, error) -> map.putIfAbsent(distinctFunction.apply(error), error),
(left, right) -> {
left.putAll(right);
return left;
}, map -> new ArrayList<>(map.values()),
Collector.Characteristics.UNORDERED)); }
In meinem Fall musste ich kontrollieren, was das vorherige Element war. Ich habe dann ein stateful Prädikat erstellt, bei dem ich überprüft habe, ob das vorherige Element vom aktuellen Element abweicht. In diesem Fall habe ich es beibehalten.
public List<Log> fetchLogById(Long id) {
return this.findLogById(id)
.stream().filter(new LogPredicate())
.collect(Collectors.toList());
}
public class LogPredicate implements Predicate<Log> {
private Log previous;
public boolean test(Log atual) {
boolean isDifferent = previouws == null || verifyIfDifferentLog(current, previous);
if (isDifferent) {
previous = current;
}
return isDifferent;
}
private boolean verifyIfDifferentLog(Log current,
Log previous) {
return !current.getId().equals(previous.getId());
}
}