Filtrer Java Stream à 1 et 1 seul élément

je suis en train d'utiliser Java 8 Stream s pour trouver des éléments dans un LinkedList . Je veux toutefois garantir qu'il n'y a qu'une seule correspondance aux 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 User basé sur leur ID. Mais il n'y a aucune garantie sur le nombre de User correspondant au filtre.

Changement de la ligne de filtre en:

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

lancera un NoSuchElementException (bon!)

je voudrais qu'il lance une erreur s'il y a plusieurs correspondances, cependant. Est-il un moyen de faire cela?

151
demandé sur Lonely Neuron 2014-03-27 21:25:32

17 réponses

créer une coutume 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.collectingAndThen pour construire notre désiré Collector par

  1. collectionner nos objets dans un List avec le Collectors.toList() collector.
  2. appliquer un finisher supplémentaire à la fin, qui renvoie l'élément simple - ou jette un IllegalStateException si list.size != 1 .

utilisé comme:

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

vous pouvez alors personnaliser ce Collector autant que vous voulez, par exemple donner l'exception comme argument dans le constructeur, le modifier pour permettre deux valeurs, et plus.

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

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

ce que vous pourriez faire, c'est simplement la 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);
124
répondu skiwi 2018-05-24 14:53:35

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 de correspondance du flux, lancer

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

Une variante de cette approche évite de jeter une exception early et représente plutôt le résultat comme un Optional contenant soit l'élément unique, 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));
78
répondu glts 2016-01-16 22:11:03

les autres réponses qui impliquent l'écriture d'une coutume Collector sont probablement plus efficaces (comme de Louis Wasserman , +1), mais si vous voulez brièveté, je suggérerais 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 de résultats.

72
répondu Stuart Marks 2018-09-05 10:09:10

Goyave fournit MoreCollectors.onlyElement() ce qui fait la bonne chose ici. Mais si vous devez le faire vous-même, vous pouvez rouler vos propres Collector :

<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 type Holder au lieu de AtomicReference . Vous pouvez réutiliser ce Collector autant que vous le souhaitez.

38
répondu Louis Wasserman 2018-09-05 10:02:02

Utilisation de Goyave MoreCollectors.onlyElement() ( JavaDoc ).

il fait ce que vous voulez et lance un IllegalArgumentException si le flux se compose de deux ou plusieurs éléments, et un NoSuchElementException si le flux est vide.

Utilisation:

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

User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());
31
répondu trevorade 2018-05-24 18:00:48

l'opération "escape hatch" qui vous permet de faire des choses bizarres qui ne sont pas autrement supportées par les ruisseaux est de 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();
}

Goyave a une méthode pratique pour prendre un Iterator et obtenir le seul élément, en lançant s'il y a zéro ou plusieurs éléments, qui pourrait remplacer les lignes N-1 inférieures ici.

26
répondu Brian Goetz 2014-10-03 17:32:51

mise à 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 lancée par Optional#get , mais si vous avez plus d'un élément qui n'aidera pas. Vous pouvez collecter les utilisateurs dans une collection qui n'accepte qu'un seul article, par exemple:

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

qui lance un java.lang.IllegalStateException: Queue full , mais ça fait 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 retourne essentiellement:

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

le résultat est alors enveloppé dans une option.

mais la solution la plus simple serait probablement de simplement recueillir à un collection, vérifiez que sa taille est 1 et obtenir le seul élément.

18
répondu assylias 2016-12-14 07:34:28

une alternative est d'utiliser la réduction: (cet exemple utilise des chaînes de caractères mais pourrait facilement s'appliquer à n'importe quel type d'objet incluant 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 pour le cas avec User vous auriez:

User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();
8
répondu prunge 2015-05-13 02:01:28

Goyave a un Collector pour ce appelé MoreCollectors.onlyElement() .

4
répondu Hans 2018-09-05 10:01:48

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

Utilisation:

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

We return an Optional , puisque nous ne pouvons généralement pas supposer que le Collection contient exactement un élément. Si vous savez déjà que c'est le cas, appeler:

User user = result.orElseThrow();

Cela met le fardeau de la handeling l'erreur sur l'appelant - il le devrait.

2
répondu Lonely Neuron 2018-05-24 18:11:04

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

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

Le unique opérateur déclenche une exception si aucun utilisateur ou de plus d'un utilisateur est trouvé.

1
répondu frhack 2016-01-07 21:56:27

comme Collectors.toMap(keyMapper, valueMapper) utilise une fusion de lancer pour gérer les entrées multiples 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 IllegalStateException pour les clés dupliquées. Mais à la fin je ne suis pas sûr que le code ne serait pas encore plus lisible en utilisant un if .

1
répondu Arne Burmeister 2016-09-08 07:57:25

si cela ne vous dérange pas d'utiliser une bibliothèque tierce, SequenceM de cyclops-streams (et LazyFutureStream de simple-react ) tous les deux ont un seul et même opérateur.

singleOptional() lance une exception s'il y a 0 ou plus que 1 éléments dans le Stream , sinon il renvoie 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() retourne Optional.empty() s'il n'y a pas de valeur 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 de deux bibliothèques.

1
répondu John McClean 2018-09-05 10:06:25

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);
}
0
répondu Xavier Dury 2017-05-16 07:15:35

je suis allé avec l'approche directe et juste mis 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 de 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 mise en œuvre pas thread-safe.

0
répondu gerardw 2018-04-03 19:00:54

en utilisant reduce

C'est la façon plus simple et flexible que j'ai trouvé (basé sur la réponse de @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:

  • le facultatif - comme toujours avec votre objet ou facultatif.empty() si non présent
  • L'Exception (avec éventuellement votre type/message personnalisé) s'il y a plus d'un élément
0
répondu Fabio Bonfante 2018-08-24 14:07:02

avez-vous essayé ceci

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

-2
répondu pardeep131085 2018-09-11 13:30:46