web-dev-qa-db-fra.com

Java Lambda Stream Distinct () sur clé arbitraire?

Je rencontrais souvent un problème avec les expressions Java lambda où, lorsque je voulais distinguer () un flux sur une propriété ou une méthode arbitraire d'un objet, mais que je voulais conserver l'objet plutôt que le mapper sur cette propriété ou méthode. J'ai commencé à créer des conteneurs comme indiqué ici ici , mais j'ai commencé à le faire assez pour que cela devienne ennuyeux et que je crée beaucoup de classes standard. 

J'ai réuni cette classe Pairing, qui contient deux objets de deux types et vous permet de spécifier le masquage des objets gauche, droit ou les deux. Ma question est la suivante: n'y a-t-il pas vraiment de fonction de flux lambda intégrée à distinct () sur un fournisseur clé? Cela me surprendrait vraiment. Sinon, cette classe remplira-t-elle cette fonction de manière fiable?

Voici comment cela s'appellerait

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

Voici le cours de jumelage

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

    }

METTRE À JOUR:

Testé la fonction de Stuart ci-dessous et il semble bien fonctionner. L'opération ci-dessous se distingue sur la première lettre de chaque chaîne. La seule partie que j'essaie de comprendre est la manière dont ConcurrentHashMap ne gère qu'une seule instance pour l'ensemble du flux.

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

La sortie est ...

ABQ
CHI
PHX
BWI
56
tmn

L'opération distinct est une opération de pipeline avec état; dans ce cas, c'est un filtre avec état. C'est un peu gênant de les créer vous-même, car il n'y a rien de intégré, mais une petite classe d'aide devrait faire l'affaire

/**
 * 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;
    }
}

Je ne connais pas vos classes de domaine, mais je pense qu'avec cette classe d'assistance, vous pourriez faire ce que vous voulez comme ceci:

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

Malheureusement, l'inférence de type ne pouvait pas aller assez loin dans l'expression, j'ai donc dû spécifier explicitement les arguments de type pour la classe DistinctByKey.

Cela implique plus de paramétrage que l'approche collectionneurs décrite par Louis Wasserman , mais présente l'avantage que les éléments distincts passent immédiatement au lieu d'être mis en mémoire tampon jusqu'à la fin de la collecte. L'espace devrait être le même, car (inévitablement) les deux approches finissent par accumuler toutes les clés distinctes extraites des éléments de flux.

METTRE &AGRAVE; JOUR

Il est possible de supprimer le paramètre de type K car il n'est en réalité utilisé que pour être stocké dans une carte. Donc Object est suffisant.

/**
 * 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);

Cela simplifie un peu les choses, mais je devais quand même spécifier l'argument de type au constructeur. Essayer d'utiliser le diamant ou une méthode d'usine statique ne semble pas améliorer les choses. Je pense que la difficulté est que le compilateur ne peut pas déduire les paramètres de type générique - pour un constructeur ou un appel de méthode statique - quand l'un ou l'autre est dans l'expression d'instance d'une référence de méthode. Tant pis.

(Une autre variante qui simplifierait probablement ceci est de créer DistinctByKey<T> implements Predicate<T> et de renommer la méthode en eval. Cela supprimerait la nécessité d'utiliser une référence de méthode et améliorerait probablement l'inférence de type. Cependant, il est peu probable qu'elle soit aussi belle que la solution ci-dessous. .)

UPDATE 2

Je ne peux pas m'empêcher d'y penser. Au lieu d'une classe d'assistance, utilisez une fonction d'ordre supérieur. Nous pouvons utiliser les locaux capturés pour maintenir l'état, de sorte que nous n'avons même pas besoin d'une classe séparée! Bonus, les choses sont simplifiées, l'inférence de type fonctionne!

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

Vous devez plus ou moins faire quelque chose comme

 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

Une variante de la deuxième mise à jour de Stuart Marks. Utiliser un ensemble.

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

Nous pouvons aussi utiliser RxJava (très puissant extension réactive bibliothèque)

Observable.from(persons).distinct(Person::getName)

ou

Observable.from(persons).distinct(p -> p.getName())
5
frhack

Pour répondre à votre question dans votre deuxième mise à jour:

Le seul élément que j'essaie de comprendre est la manière dont ConcurrentHashMap ne gère qu'une seule instance pour l'ensemble du flux:

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

Dans votre exemple de code, distinctByKey n'est appelé qu'une fois. ConcurrentHashMap n'a donc été créé qu'une seule fois. Voici une explication:

La fonction distinctByKey est une simple fonction qui retourne un objet et qui se trouve être un prédicat. Gardez à l'esprit qu'un prédicat est fondamentalement un morceau de code qui peut être évalué ultérieurement. Pour évaluer manuellement un prédicat, vous devez appeler une méthode dans l'interface de prédicat telle que test . Donc, le prédicat

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

est simplement une déclaration qui n'est pas réellement évaluée dans distinctByKey.

Le prédicat est transmis comme n'importe quel autre objet. Il est renvoyé et passé dans l'opération filter, qui évalue fondamentalement le prédicat de manière répétée par rapport à chaque élément du flux en appelant test.

Je suis sûr que filter est plus compliqué que ce que je pensais, mais le fait est que le prédicat est évalué plusieurs fois en dehors de distinctByKey. Il n'y a rien de spécial * à propos de distinctByKey; il s'agit simplement d'une fonction que vous avez appelée une fois. Le ConcurrentHashMap n'est donc créé qu'une fois. 

* En plus d'être bien fait, @ stuart-marks :)

3
Jamish

Vous pouvez utiliser la méthode distinct(HashingStrategy) dans Collections Eclipse .

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

Si vous pouvez refactoriser list pour implémenter une interface de collections Eclipse, vous pouvez appeler la méthode directement dans la liste.

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 est simplement une interface de stratégie qui permet de définir des implémentations personnalisées d’égal à égal et de hashcode.

public interface HashingStrategy<E>
{
    int computeHashCode(E object);
    boolean equals(E object1, E object2);
}

Remarque: je suis un partisan des collections Eclipse.

2
Craig P. Motlin

Une autre façon de trouver des éléments distincts

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) renvoie true si l'ensemble ne contenait pas déjà element, sinon false . Vous pouvez le faire comme ceci.

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

Si vous voulez faire ce parallèle, vous devez utiliser une carte simultanée.

0
saka1029

On peut faire quelque chose comme 

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