it-swarm.com.de

Java 8 Unterscheidend durch Eigenschaft

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?

306
RichK

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? )

382
Stuart Marks

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.

95
wha'eve'

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();
    }
}
76
nosid

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())
23
frhack

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())) 
));
22
josketres

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()));
19
Santhosh

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.

9
Craig P. Motlin

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
8

Sie können StreamEx library verwenden:

StreamEx.of(persons)
        .distinct(Person::getName)
        .toList()
7
Sllouyssgort

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)));
6
Saeed Zarinfam

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())
6

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());
5
Set<YourPropertyType> set = new HashSet<>();
list
        .stream()
        .filter(it -> set.add(it.getYourProperty()))
        .forEach(it -> ...);
4

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());
4
asdasdsdf

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 .

4

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.

2
Holger

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;
    }
}
1
Garrett Smith

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));
1
Naveen Dhalaria

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());
0
uneq95

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)); }
0
Aliaksei Yatsau

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());
}

}

0
Flavio Oliva