it-swarm.com.de

Java Lambda Stream Distinct () auf einem beliebigen Schlüssel?

Ich bin häufig auf ein Problem mit Java-Lambda-Ausdrücken gestoßen, bei dem ich einen Stream für eine beliebige Eigenschaft oder Methode eines Objekts festlegen wollte, das Objekt aber behalten wollte, anstatt es dieser Eigenschaft oder Methode zuzuordnen. Ich habe angefangen, Container zu erstellen, wie beschrieben - hier , aber ich fing an, es genug zu machen, wo es nervig wurde, und machte viele Boilerplate-Klassen. 

Ich habe diese Pairing-Klasse zusammengewürfelt, die zwei Objekte zweier Typen enthält und das Festlegen von Schlüsseln von links, rechts oder von beiden Objekten ermöglicht. Meine Frage ist ... Gibt es wirklich keine eingebaute Lambda-Stream-Funktion, um auf einem Schlüssellieferanten zu unterscheiden ()? Das würde mich wirklich überraschen. Wenn nicht, wird diese Klasse diese Funktion zuverlässig erfüllen?

So würde es heißen

BigDecimal totalShare = orders.stream().map(c -> Pairing.keyLeft(c.getCompany().getId(), c.getShare())).distinct().map(Pairing::getRightItem).reduce(BigDecimal.ZERO, (x,y) -> x.add(y));

Hier ist die Pairing-Klasse

    public final class Pairing<X,Y>  {
           private final X item1;
           private final Y item2;
           private final KeySetup keySetup;

           private static enum KeySetup {LEFT,RIGHT,BOTH};

           private Pairing(X item1, Y item2, KeySetup keySetup) {
                  this.item1 = item1;
                  this.item2 = item2;
                  this.keySetup = keySetup;
           }
           public X getLeftItem() { 
                  return item1;
           }
           public Y getRightItem() { 
                  return item2;
           }

           public static <X,Y> Pairing<X,Y> keyLeft(X item1, Y item2) { 
                  return new Pairing<X,Y>(item1, item2, KeySetup.LEFT);
           }

           public static <X,Y> Pairing<X,Y> keyRight(X item1, Y item2) { 
                  return new Pairing<X,Y>(item1, item2, KeySetup.RIGHT);
           }
           public static <X,Y> Pairing<X,Y> keyBoth(X item1, Y item2) { 
                  return new Pairing<X,Y>(item1, item2, KeySetup.BOTH);
           }
           public static <X,Y> Pairing<X,Y> forItems(X item1, Y item2) { 
                  return keyBoth(item1, item2);
           }

           @Override
           public int hashCode() {
                  final int prime = 31;
                  int result = 1;
                  if (keySetup.equals(KeySetup.LEFT) || keySetup.equals(KeySetup.BOTH)) {
                  result = prime * result + ((item1 == null) ? 0 : item1.hashCode());
                  }
                  if (keySetup.equals(KeySetup.RIGHT) || keySetup.equals(KeySetup.BOTH)) {
                  result = prime * result + ((item2 == null) ? 0 : item2.hashCode());
                  }
                  return result;
           }

           @Override
           public boolean equals(Object obj) {
                  if (this == obj)
                         return true;
                  if (obj == null)
                         return false;
                  if (getClass() != obj.getClass())
                         return false;
                  Pairing<?,?> other = (Pairing<?,?>) obj;
                  if (keySetup.equals(KeySetup.LEFT) || keySetup.equals(KeySetup.BOTH)) {
                         if (item1 == null) {
                               if (other.item1 != null)
                                      return false;
                         } else if (!item1.equals(other.item1))
                               return false;
                  }
                  if (keySetup.equals(KeySetup.RIGHT) || keySetup.equals(KeySetup.BOTH)) {
                         if (item2 == null) {
                               if (other.item2 != null)
                                      return false;
                         } else if (!item2.equals(other.item2))
                               return false;
                  }
                  return true;
           }

    }

AKTUALISIEREN:

Die Funktion von Stuart unten getestet und es scheint großartig zu funktionieren. Die folgende Operation unterscheidet sich vom ersten Buchstaben jeder Zeichenfolge. Der einzige Teil, den ich versuche herauszufinden, ist, wie die ConcurrentHashMap nur eine Instanz für den gesamten Stream verwaltet

public class DistinctByKey {

    public static <T> Predicate<T> distinctByKey(Function<? super T,Object> keyExtractor) {
        Map<Object,Boolean> seen = new ConcurrentHashMap<>();
        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }

    public static void main(String[] args) { 

        final ImmutableList<String> arpts = ImmutableList.of("ABQ","ALB","CHI","CUN","PHX","PUJ","BWI");

        arpts.stream().filter(distinctByKey(f -> f.substring(0,1))).forEach(s -> System.out.println(s));
    }

Ausgabe ist ...

ABQ
CHI
PHX
BWI
56
tmn

Die distinct-Operation ist eine stateful -Pipeline-Operation. In diesem Fall handelt es sich um einen Stateful-Filter. Es ist etwas umständlich, diese selbst zu erstellen, da nichts eingebaut ist, aber eine kleine Helferklasse sollte den Trick tun:

/**
 * Stateful filter. T is type of stream element, K is type of extracted key.
 */
static class DistinctByKey<T,K> {
    Map<K,Boolean> seen = new ConcurrentHashMap<>();
    Function<T,K> keyExtractor;
    public DistinctByKey(Function<T,K> ke) {
        this.keyExtractor = ke;
    }
    public boolean filter(T t) {
        return seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }
}

Ich kenne Ihre Domain-Klassen nicht, aber ich denke, dass Sie mit dieser Helper-Klasse das tun könnten, was Sie möchten:

BigDecimal totalShare = orders.stream()
    .filter(new DistinctByKey<Order,CompanyId>(o -> o.getCompany().getId())::filter)
    .map(Order::getShare)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

Leider konnte die Typinferenz innerhalb des Ausdrucks nicht weit genug kommen, sodass ich die Typargumente für die DistinctByKey-Klasse explizit angeben musste.

Dies beinhaltet mehr Setup als der von Louis Wasserman beschriebene Collector-Ansatz , dies hat jedoch den Vorteil, dass verschiedene Elemente sofort durchlaufen werden und nicht bis zum Abschluss der Sammlung gepuffert werden. Der Raum sollte derselbe sein, da (unvermeidlich) beide Ansätze dazu führen, dass alle unterschiedlichen Schlüssel gesammelt werden, die aus den Stream-Elementen extrahiert werden.

UPDATE

Es ist möglich, den Parameter K loszuwerden, da er nicht für etwas anderes als für das Speichern in einer Karte verwendet wird. Object ist also ausreichend.

/**
 * Stateful filter. T is type of stream element.
 */
static class DistinctByKey<T> {
    Map<Object,Boolean> seen = new ConcurrentHashMap<>();
    Function<T,Object> keyExtractor;
    public DistinctByKey(Function<T,Object> ke) {
        this.keyExtractor = ke;
    }
    public boolean filter(T t) {
        return seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }
}

BigDecimal totalShare = orders.stream()
    .filter(new DistinctByKey<Order>(o -> o.getCompany().getId())::filter)
    .map(Order::getShare)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

Dies vereinfacht die Sache ein wenig, aber ich musste das Typargument immer noch dem Konstruktor angeben. Der Versuch, Diamant oder eine statische Fabrikmethode zu verwenden, scheint nichts zu verbessern. Ich denke, die Schwierigkeit besteht darin, dass der Compiler keine generischen Typparameter (für einen Konstruktor oder einen statischen Methodenaufruf) ableiten kann, wenn sich einer der beiden im Instanzausdruck einer Methodenreferenz befindet. Naja.

(Eine weitere Variante, die wahrscheinlich die Vereinfachung vereinfachen würde, besteht darin, DistinctByKey<T> implements Predicate<T> zu machen und die Methode in eval umzubenennen. Dies würde die Verwendung einer Methodenreferenz überflüssig machen und würde wahrscheinlich die Typinferenz verbessern. Es ist jedoch unwahrscheinlich, dass die folgende Lösung so schön ist .)

UPDATE 2

Kann nicht aufhören, darüber nachzudenken. Verwenden Sie statt einer Hilfsklasse eine Funktion höherer Ordnung. Wir können erfasste Einheimische verwenden, um den Status aufrechtzuerhalten, sodass wir nicht einmal eine separate Klasse benötigen! Bonus, die Dinge werden vereinfacht, so dass Typ Inferenz funktioniert!

public static <T> Predicate<T> distinctByKey(Function<? super T,Object> keyExtractor) {
    Map<Object,Boolean> seen = new ConcurrentHashMap<>();
    return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

BigDecimal totalShare = orders.stream()
    .filter(distinctByKey(o -> o.getCompany().getId()))
    .map(Order::getShare)
    .reduce(BigDecimal.ZERO, BigDecimal::add);
98
Stuart Marks

Sie müssen mehr oder weniger etwas tun

 elements.stream()
    .collect(Collectors.toMap(
        obj -> extractKey(obj), 
        obj -> obj, 
       (first, second) -> first
           // pick the first if multiple values have the same key
       )).values().stream();
27
Louis Wasserman

Eine Variation von Stuart Marks zweitem Update. Set verwenden.

public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
    Set<Object> seen = Collections.newSetFromMap(new ConcurrentHashMap<>());
    return t -> seen.add(keyExtractor.apply(t));
}
6
rognlien

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

So beantworten Sie Ihre Frage in Ihrem zweiten Update:

Ich versuche nur herauszufinden, wie die ConcurrentHashMap nur eine Instanz für den gesamten Stream verwaltet:

public static <T> Predicate<T> distinctByKey(Function<? super T,Object> keyExtractor) {
        Map<Object,Boolean> seen = new ConcurrentHashMap<>();
        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }

In Ihrem Codebeispiel wird distinctByKey nur einmal aufgerufen, sodass die ConcurrentHashMap nur einmal erstellt wird. Hier ist eine Erklärung:

Die distinctByKey-Funktion ist nur eine einfache alte Funktion, die ein Objekt zurückgibt, und dieses Objekt ist zufällig ein Prädikat. Beachten Sie, dass ein Prädikat im Wesentlichen ein Code ist, der später ausgewertet werden kann. Um ein Prädikat manuell auszuwerten, müssen Sie eine Methode in der Predicate-Schnittstelle aufrufen wie test . Also das Prädikat

t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null

ist lediglich eine Deklaration, die in distinctByKey nicht wirklich ausgewertet wird.

Das Prädikat wird wie jedes andere Objekt weitergegeben. Sie wird zurückgegeben und in die filter -Operation übergeben, die das Prädikat im Prinzip wiederholt für jedes Element des Streams auswertet, indem test aufgerufen wird.

Ich bin sicher, dass filter komplizierter ist, als ich es ausgemacht habe, aber das Prädikat wird oft außerhalb von distinctByKey ausgewertet. Es gibt nichts Besonderes über distinctByKey; Es ist nur eine Funktion, die Sie einmal aufgerufen haben. Daher wird die ConcurrentHashMap nur einmal erstellt. 

* Außer gut gemacht, @ stuart-marks :)

3
Jamish

Sie können die distinct(HashingStrategy)-Methode in Eclipse Collections verwenden.

List<String> list = Lists.mutable.with("ABQ", "ALB", "CHI", "CUN", "PHX", "PUJ", "BWI");
ListIterate.distinct(list, HashingStrategies.fromFunction(s -> s.substring(0, 1)))
    .each(System.out::println);

Wenn Sie list zur Implementierung einer Eclipse Collections-Schnittstelle umwandeln können, können Sie die Methode direkt in der Liste aufrufen.

MutableList<String> list = Lists.mutable.with("ABQ", "ALB", "CHI", "CUN", "PHX", "PUJ", "BWI");
list.distinct(HashingStrategies.fromFunction(s -> s.substring(0, 1)))
    .each(System.out::println);

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.

2
Craig P. Motlin

Eine andere Möglichkeit, bestimmte Elemente zu finden

List<String> uniqueObjects = ImmutableList.of("ABQ","ALB","CHI","CUN","PHX","PUJ","BWI")
            .stream()
            .collect(Collectors.groupingBy((p)->p.substring(0,1))) //expression 
            .values()
            .stream()
            .flatMap(e->e.stream().limit(1))
            .collect(Collectors.toList());
1
Arshed

Set.add(element) gibt true zurück, wenn der Satz nicht bereits element enthielt, ansonsten false .

Set<String> set = new HashSet<>();
BigDecimal totalShare = orders.stream()
    .filter(c -> set.add(c.getCompany().getId()))
    .map(c -> c.getShare())
    .reduce(BigDecimal.ZERO, BigDecimal::add);

Wenn Sie dies parallel durchführen möchten, müssen Sie eine gleichzeitige Karte verwenden.

0
saka1029

Es kann so etwas gemacht werden 

Set<String> distinctCompany = orders.stream()
        .map(Order::getCompany)
        .collect(Collectors.toSet());
0
Fahad