Filtrer le flux Java sur 1 et 1 seul élément


230

J'essaie d'utiliser Java 8 Streampour trouver des éléments dans a LinkedList. Je veux cependant garantir qu'il y a une et une seule correspondance avec les critères de filtrage.

Prenez ce code:

public static void main(String[] args) {

    LinkedList<User> users = new LinkedList<>();
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));

    User match = users.stream().filter((user) -> user.getId() == 1).findAny().get();
    System.out.println(match.toString());
}

static class User {

    @Override
    public String toString() {
        return id + " - " + username;
    }

    int id;
    String username;

    public User() {
    }

    public User(int id, String username) {
        this.id = id;
        this.username = username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public int getId() {
        return id;
    }
}

Ce code trouve un Userbasé sur leur ID. Mais rien ne garantit le nombre de Users correspondant au filtre.

Changer la ligne de filtre en:

User match = users.stream().filter((user) -> user.getId() < 0).findAny().get();

Jettera un NoSuchElementException(bon!)

Je voudrais cependant qu'il génère une erreur s'il existe plusieurs correspondances. Y a-t-il un moyen de faire cela?


count()est une opération de terminal, vous ne pouvez donc pas le faire. Le flux ne peut pas être utilisé après.
Alexis C.

Ok, merci @ZouZou. Je n'étais pas tout à fait certain de ce que faisait cette méthode. Pourquoi n'y en a-t-il pas Stream::size?
ryvantage

7
@ryvantage Parce qu'un flux ne peut être utilisé qu'une seule fois: calculer sa taille signifie "itérer" dessus et après cela, vous ne pouvez plus utiliser le flux.
assylias

3
Sensationnel. Ce seul commentaire m'a aidé à comprendre Streambien plus qu'avant ...
ryvantage

2
C'est à ce moment que vous vous rendez compte que vous aviez besoin d'utiliser un LinkedHashSet(en supposant que vous souhaitiez conserver l'ordre d'insertion) ou un HashSettout le long. Si votre collection n'est utilisée que pour trouver un seul identifiant utilisateur, pourquoi collectez-vous tous les autres éléments? S'il existe un potentiel dont vous aurez toujours besoin pour trouver un identifiant utilisateur qui doit également être unique, alors pourquoi utiliser une liste et non un ensemble? Vous programmez à l'envers. Utilisez la bonne collection pour le travail et épargnez-vous ce mal de tête
smac89

Réponses:


192

Créer un personnalisé Collector

public static <T> Collector<T, ?, T> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                if (list.size() != 1) {
                    throw new IllegalStateException();
                }
                return list.get(0);
            }
    );
}

Nous utilisons Collectors.collectingAndThenpour construire notre désir Collectorpar

  1. Collectionner nos objets en un Listavec le Collectors.toList()collectionneur.
  2. Appliquer un finisseur supplémentaire à la fin, qui renvoie l'élément unique - ou lance un IllegalStateExceptionif list.size != 1.

Utilisé comme:

User resultUser = users.stream()
        .filter(user -> user.getId() > 0)
        .collect(toSingleton());

Vous pouvez ensuite personnaliser cela Collectorautant que vous le souhaitez, par exemple donner l'exception comme argument dans le constructeur, l'ajuster pour autoriser deux valeurs, et plus encore.

Une solution alternative - sans doute moins élégante -:

Vous pouvez utiliser une solution de contournement qui implique peek()et un AtomicInteger, mais vous ne devriez vraiment pas utiliser cela.

Ce que vous pourriez faire, c'est simplement le collecter dans un List, comme ceci:

LinkedList<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));
List<User> resultUserList = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.toList());
if (resultUserList.size() != 1) {
    throw new IllegalStateException();
}
User resultUser = resultUserList.get(0);

24
Les goyaves Iterables.getOnlyElementraccourciraient ces solutions et fourniraient de meilleurs messages d'erreur. Juste un conseil pour les autres lecteurs qui utilisent déjà Google Guava.
Tim Büthe

2
j'ai enveloppé cette idée dans une classe - gist.github.com/denov/a7eac36a3cda041f8afeabcef09d16fc
denov

1
@LonelyNeuron Veuillez ne pas modifier mon code. Cela me place dans une situation où je dois valider l'intégralité de ma réponse, que j'ai écrite il y a quatre ans, et je n'ai tout simplement pas le temps pour le moment.
skiwi

2
@skiwi: L'édition de Lonely a été utile et correcte, donc je l'ai réintégrée après examen. Les personnes visitant cette réponse aujourd'hui ne se soucient pas de la façon dont vous êtes parvenu à la réponse, elles n'ont pas besoin de voir l'ancienne version et la nouvelle version et une section mise à jour . Cela rend votre réponse plus confuse et moins utile. Il est préférable de mettre les messages dans un état final , et si les gens veulent voir comment tout cela s'est déroulé, ils peuvent afficher l'historique des messages.
Martijn Pieters

1
@skiwi: Le code dans la réponse est absolument ce que vous avez écrit. Tout ce que l'éditeur a fait était de nettoyer votre publication, en supprimant uniquement une version antérieure de la singletonCollector()définition obsolète par la version qui reste dans la publication et en la renommant toSingleton(). Mon expertise en flux Java est un peu rouillée, mais le changement de nom me semble utile. La révision de ce changement m'a pris 2 minutes, en tête. Si vous n'avez pas le temps de revoir les modifications, puis-je vous suggérer de demander à quelqu'un d'autre de le faire à l'avenir, peut-être dans la salle de chat Java ?
Martijn Pieters

118

Par souci d'exhaustivité, voici le «one-liner» correspondant à l'excellente réponse de @ prunge:

User user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })
        .get();

Cela obtient le seul élément correspondant du flux, jetant

  • NoSuchElementException dans le cas où le flux est vide, ou
  • IllegalStateException dans le cas où le flux contient plus d'un élément correspondant.

Une variante de cette approche évite de lever une exception plus tôt et représente à la place le résultat comme Optionalcontenant soit le seul élément, soit rien (vide) s'il y a zéro ou plusieurs éléments:

Optional<User> user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.reducing((a, b) -> null));

3
J'aime l'approche initiale dans cette réponse. À des fins de personnalisation, il est possible de convertir le dernier get()enorElseThrow()
arin

1
J'aime la brièveté de celui-ci et le fait qu'il évite de créer une instance de List inutile chaque fois qu'il est appelé.
LordOfThePigs

83

Les autres réponses qui impliquent l'écriture d'une coutume Collectorsont probablement plus efficaces (comme celle de Louis Wasserman , +1), mais si vous voulez être bref, je suggère ce qui suit:

List<User> result = users.stream()
    .filter(user -> user.getId() == 1)
    .limit(2)
    .collect(Collectors.toList());

Vérifiez ensuite la taille de la liste des résultats.

if (result.size() != 1) {
  throw new IllegalStateException("Expected exactly one user but got " + result);
User user = result.get(0);
}

5
Quel est l'intérêt de limit(2)cette solution? Quelle différence cela ferait-il si la liste résultante était 2 ou 100? Si elle est supérieure à 1.
ryvantage

18
Il s'arrête immédiatement s'il trouve une deuxième correspondance. C'est ce que font tous les collectionneurs sophistiqués, en utilisant simplement plus de code. :-)
Stuart marque

10
Que diriez-vous d'ajouterCollectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
Lukas Eder

1
Javadoc dit le param de cette limite sur: maxSize: the number of elements the stream should be limited to. Donc, il ne devrait pas être au .limit(1)lieu de .limit(2)?
alexbt

5
@alexbt L'énoncé du problème est de s'assurer qu'il y a exactement un (pas plus, pas moins) élément correspondant. Après mon code, on peut tester result.size()pour s'assurer qu'il est égal à 1. Si c'est 2, alors il y a plus d'une correspondance, c'est donc une erreur. Si le code le faisait à la place limit(1), plus d'une correspondance entraînerait un seul élément, qui ne peut pas être distingué de l'existence d'une seule correspondance. Cela raterait un cas d'erreur qui inquiétait le PO.
Stuart marque

67

La goyave fournit MoreCollectors.onlyElement()ce qui fait la bonne chose ici. Mais si vous devez le faire vous-même, vous pouvez lancer le vôtre Collectorpour cela:

<E> Collector<E, ?, Optional<E>> getOnly() {
  return Collector.of(
    AtomicReference::new,
    (ref, e) -> {
      if (!ref.compareAndSet(null, e)) {
         throw new IllegalArgumentException("Multiple values");
      }
    },
    (ref1, ref2) -> {
      if (ref1.get() == null) {
        return ref2;
      } else if (ref2.get() != null) {
        throw new IllegalArgumentException("Multiple values");
      } else {
        return ref1;
      }
    },
    ref -> Optional.ofNullable(ref.get()),
    Collector.Characteristics.UNORDERED);
}

... ou en utilisant votre propre Holdertype au lieu de AtomicReference. Vous pouvez le réutiliser Collectorautant que vous le souhaitez.


@ singletonCollector de skiwi était plus petit et plus facile à suivre que cela, c'est pourquoi je lui ai donné le chèque. Mais bon de voir un consensus dans la réponse: une coutume Collectorétait la voie à suivre.
ryvantage

1
C'est suffisant. Je visais principalement la vitesse, pas la concision.
Louis Wasserman

1
Ouais? Pourquoi le vôtre est-il plus rapide?
ryvantage

3
Principalement parce que l'allocation d'un all-up Listest plus coûteuse qu'une seule référence mutable.
Louis Wasserman

1
@LouisWasserman, la dernière phrase de mise à jour sur MoreCollectors.onlyElement()devrait en fait être la première (et peut-être la seule :))
Piotr Findeisen

46

Utilisez Guava MoreCollectors.onlyElement()( JavaDoc ).

Il fait ce que vous voulez et lance un IllegalArgumentExceptionsi le flux est composé de deux éléments ou plus, et un NoSuchElementExceptionsi le flux est vide.

Usage:

import static com.google.common.collect.MoreCollectors.onlyElement;

User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());

2
Remarque pour les autres utilisateurs: MoreCollectorsfait partie de la version 21 non encore publiée (à partir de 2016-12) non
publiée

2
Cette réponse devrait aller plus haut.
Emdadul Sawon

31

L'opération "hachure d'échappement" qui vous permet de faire des choses étranges qui ne sont pas autrement prises en charge par les flux consiste à demander un Iterator:

Iterator<T> it = users.stream().filter((user) -> user.getId() < 0).iterator();
if (!it.hasNext()) 
    throw new NoSuchElementException();
else {
    result = it.next();
    if (it.hasNext())
        throw new TooManyElementsException();
}

La goyave a une méthode pratique pour prendre un Iteratoret obtenir le seul élément, en jetant s'il y a zéro ou plusieurs éléments, ce qui pourrait remplacer les n-1 lignes inférieures ici.


4
Méthode de la goyave: Iterators.getOnlyElement (itérateur Iterator <T>).
anre

23

Mettre à jour

Belle suggestion dans le commentaire de @Holger:

Optional<User> match = users.stream()
              .filter((user) -> user.getId() > 1)
              .reduce((u, v) -> { throw new IllegalStateException("More than one ID found") });

Réponse originale

L'exception est levée par Optional#get, mais si vous avez plus d'un élément, cela n'aidera pas. Vous pouvez collecter les utilisateurs dans une collection qui n'accepte qu'un seul élément, par exemple:

User match = users.stream().filter((user) -> user.getId() > 1)
                  .collect(toCollection(() -> new ArrayBlockingQueue<User>(1)))
                  .poll();

qui jette un java.lang.IllegalStateException: Queue full , mais qui semble trop hacky.

Ou vous pouvez utiliser une réduction combinée avec une option:

User match = Optional.ofNullable(users.stream().filter((user) -> user.getId() > 1)
                .reduce(null, (u, v) -> {
                    if (u != null && v != null)
                        throw new IllegalStateException("More than one ID found");
                    else return u == null ? v : u;
                })).get();

La réduction renvoie essentiellement:

  • null si aucun utilisateur n'est trouvé
  • l'utilisateur si un seul est trouvé
  • lève une exception si plus d'un est trouvé

Le résultat est ensuite enveloppé dans une option.

Mais la solution la plus simple serait probablement de simplement collecter dans une collection, de vérifier que sa taille est de 1 et d'obtenir le seul élément.


1
J'ajouterais un élément d'identité ( null) pour empêcher l'utilisation get(). Malheureusement, votre reducene fonctionne pas comme vous le pensez, pensez à un élément Streamqui contient des nulléléments, peut-être pensez-vous que vous l'avez couvert, mais je peux l'être [User#1, null, User#2, null, User#3], maintenant il ne lèvera pas d'exception, je pense, à moins que je ne me trompe ici.
skiwi

2
@Skiwi s'il y a des éléments nuls, le filtre lancera d'abord un NPE.
assylias

2
Puisque vous savez que le flux ne peut pas passer nullà la fonction de réduction, la suppression de l'argument de valeur d'identité rendrait l'ensemble du traitement de nullla fonction obsolète: reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )fait le travail et encore mieux, il renvoie déjà un Optional, éliminant la nécessité d'appeler Optional.ofNullablele résultat.
Holger

15

Une alternative est d'utiliser la réduction: (cet exemple utilise des chaînes mais pourrait facilement s'appliquer à n'importe quel type d'objet, y compris User)

List<String> list = ImmutableList.of("one", "two", "three", "four", "five", "two");
String match = list.stream().filter("two"::equals).reduce(thereCanBeOnlyOne()).get();
//throws NoSuchElementException if there are no matching elements - "zero"
//throws RuntimeException if duplicates are found - "two"
//otherwise returns the match - "one"
...

//Reduction operator that throws RuntimeException if there are duplicates
private static <T> BinaryOperator<T> thereCanBeOnlyOne()
{
    return (a, b) -> {throw new RuntimeException("Duplicate elements found: " + a + " and " + b);};
}

Donc, dans le cas où Uservous auriez:

User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();

8

Utilisation de réduire

C'est la manière la plus simple et flexible que j'ai trouvée (basée sur la réponse @prunge)

Optional<User> user = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })

De cette façon, vous obtenez:

  • l'Optionnel - comme toujours avec votre objet ou Optional.empty()s'il n'est pas présent
  • l'exception (avec éventuellement VOTRE type / message personnalisé) s'il y a plus d'un élément

6

Je pense que cette façon est plus simple:

User resultUser = users.stream()
    .filter(user -> user.getId() > 0)
    .findFirst().get();

4
Il ne trouve que le premier mais l'affaire devait aussi lancer Exception quand il y en a plus d'un
lczapski

5

En utilisant un Collector:

public static <T> Collector<T, ?, Optional<T>> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.size() == 1 ? Optional.of(list.get(0)) : Optional.empty()
    );
}

Usage:

Optional<User> result = users.stream()
        .filter((user) -> user.getId() < 0)
        .collect(toSingleton());

Nous retournons un Optional, car nous ne pouvons généralement pas supposer que le Collectioncontient exactement un élément. Si vous savez déjà que c'est le cas, appelez:

User user = result.orElseThrow();

Cela met le fardeau de gérer l'erreur sur l'appelant - comme il se doit.



1

Nous pouvons utiliser RxJava ( bibliothèque d' extensions réactives très puissante )

LinkedList<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

User userFound =  Observable.from(users)
                  .filter((user) -> user.getId() == 1)
                  .single().toBlocking().first();

L' opérateur unique lève une exception si aucun utilisateur ou plus d'un utilisateur n'est trouvé.


Réponse correcte, l'initialisation d'un flux ou d'une collection de blocage n'est probablement pas très bon marché (en termes de ressources).
Karl Richter

1

Comme Collectors.toMap(keyMapper, valueMapper)utilise une fusion de lancement pour gérer plusieurs entrées avec la même clé, il est facile:

List<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

int id = 1;
User match = Optional.ofNullable(users.stream()
  .filter(user -> user.getId() == id)
  .collect(Collectors.toMap(User::getId, Function.identity()))
  .get(id)).get();

Vous obtiendrez un IllegalStateExceptionpour les clés en double. Mais à la fin, je ne sais pas si le code ne serait pas encore plus lisible en utilisant un if.


1
Bonne solution! Et si vous le faites .collect(Collectors.toMap(user -> "", Function.identity())).get(""), vous avez un comportement plus générique.
glglgl

1

J'utilise ces deux collecteurs:

public static <T> Collector<T, ?, Optional<T>> zeroOrOne() {
    return Collectors.reducing((a, b) -> {
        throw new IllegalStateException("More than one value was returned");
    });
}

public static <T> Collector<T, ?, T> onlyOne() {
    return Collectors.collectingAndThen(zeroOrOne(), Optional::get);
}

Soigné! onlyOne()lance IllegalStateExceptionpour> 1 éléments et NoSuchElementException` (in Optional::get) pour 0 éléments.
simon04

@ simon04 Vous pourriez surcharger les méthodes pour prendre un Supplierde (Runtime)Exception.
Xavier Dury

1

Si cela ne vous dérange pas d'utiliser une bibliothèque tierce, à SequenceMpartir de flux cyclops (et LazyFutureStreamde simple-react ), les deux ont des opérateurs single et singleOptional.

singleOptional()lève une exception s'il y a 0ou plus d' 1éléments dans le Stream, sinon il retourne la valeur unique.

String result = SequenceM.of("x")
                          .single();

SequenceM.of().single(); // NoSuchElementException

SequenceM.of(1, 2, 3).single(); // NoSuchElementException

String result = LazyFutureStream.fromStream(Stream.of("x"))
                          .single();

singleOptional()renvoie Optional.empty()s'il n'y a pas de valeurs ou plus d'une valeur dans le Stream.

Optional<String> result = SequenceM.fromStream(Stream.of("x"))
                          .singleOptional(); 
//Optional["x"]

Optional<String> result = SequenceM.of().singleOptional(); 
// Optional.empty

Optional<String> result =  SequenceM.of(1, 2, 3).singleOptional(); 
// Optional.empty

Divulgation - Je suis l'auteur des deux bibliothèques.


0

Je suis allé avec l'approche directe et je viens de mettre en œuvre la chose:

public class CollectSingle<T> implements Collector<T, T, T>, BiConsumer<T, T>, Function<T, T>, Supplier<T> {
T value;

@Override
public Supplier<T> supplier() {
    return this;
}

@Override
public BiConsumer<T, T> accumulator() {
    return this;
}

@Override
public BinaryOperator<T> combiner() {
    return null;
}

@Override
public Function<T, T> finisher() {
    return this;
}

@Override
public Set<Characteristics> characteristics() {
    return Collections.emptySet();
}

@Override //accumulator
public void accept(T ignore, T nvalue) {
    if (value != null) {
        throw new UnsupportedOperationException("Collect single only supports single element, "
                + value + " and " + nvalue + " found.");
    }
    value = nvalue;
}

@Override //supplier
public T get() {
    value = null; //reset for reuse
    return value;
}

@Override //finisher
public T apply(T t) {
    return value;
}


} 

avec le test JUnit:

public class CollectSingleTest {

@Test
public void collectOne( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    Integer o = lst.stream().collect( new CollectSingle<>());
    System.out.println(o);
}

@Test(expected = UnsupportedOperationException.class)
public void failOnTwo( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    lst.add(8);
    Integer o = lst.stream().collect( new CollectSingle<>());
}

}

Cette implémentation n'est pas threadsafe.


0
User match = users.stream().filter((user) -> user.getId()== 1).findAny().orElseThrow(()-> new IllegalArgumentException());

5
Bien que ce code puisse résoudre la question, y compris une explication de comment et pourquoi cela résout le problème aiderait vraiment à améliorer la qualité de votre message, et entraînerait probablement plus de votes positifs. N'oubliez pas que vous répondrez à la question aux lecteurs à l'avenir, pas seulement à la personne qui pose la question maintenant. Veuillez modifier votre réponse pour ajouter des explications et donner une indication des limitations et hypothèses applicables.
David Buck

-2

As-tu essayé

long c = users.stream().filter((user) -> user.getId() == 1).count();
if(c > 1){
    throw new IllegalStateException();
}

long count()
Returns the count of elements in this stream. This is a special case of a reduction and is equivalent to:

     return mapToLong(e -> 1L).sum();

This is a terminal operation.

Source: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html


3
Il a été dit que ce count()n'est pas bon à utiliser car c'est une opération de terminal.
ryvantage

Si c'est vraiment un devis, veuillez ajouter vos sources
Neuron
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.