Filtrer le flux Java en 1 et seulement 1 élément


Je suis en train d'utiliser Java 8 Streams pour trouver des éléments dans un LinkedList. Je veux garantir, cependant, qu'il y a une et une seule correspondance avec les critères de filtre.

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 Usercorrespondant au filtre.

Changer 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 en a plusieurs les matchs de, si. Est-il un moyen de faire cela?

Author: Lonely Neuron, 2014-03-27

17 answers

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.collectingAndThen pour construire notre souhaité Collector par

  1. Rassembler nos objets dans un List avec le Collectors.toList() collector.
  2. En appliquant un finisseur supplémentaire à la fin, qui renvoie l'élément unique - ou jette un IllegalStateException if list.size != 1.

Utilisé comme:

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

Vous pouvez ensuite personnaliser ce Collector autant que vous voulez, par exemple donner de l'exception comme argument dans le constructeur, l'ajuster à 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 l'utiliser.

Ce que vous pourriez faire istead, 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);
 124
Author: 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 correspondant du flux, jetant

  • NoSuchElementException si 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 lancer une exception tôt et représente plutôt le résultat sous la forme d'un Optional contenant 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));
 78
Author: 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 de la brièveté, 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 de résultats.

 72
Author: Stuart Marks, 2018-09-05 10:09:10

Goyave donne MoreCollectors.onlyElement() ce qui fait la bonne chose ici. Mais si vous devez le faire vous-même, vous pouvez rouler vos propres Collector pour 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 type Holder au lieu de AtomicReference. Vous pouvez réutiliser ce Collector autant que vous le souhaitez.

 38
Author: 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 éléments ou plus, 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
Author: trevorade, 2018-05-24 18:00:48

L'opération "escape hatch" qui vous permet de faire des choses étranges qui ne sont pas 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();
}

Guava 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, ce qui pourrait remplacer les n-1 lignes inférieures ici.

 26
Author: 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 levée par Optional#get, mais si vous avez plus d'un élément qui ne va pas aider. Vous pourriez recueillir 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 lance 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'une est trouvée

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

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

 18
Author: assylias, 2016-12-14 07:34:28

Une alternative consiste à 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 pour le cas avec User vous auriez:

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

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

 4
Author: Hans, 2018-09-05 10:01:48

À l'Aide d'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());

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

User user = result.orElseThrow();

Cela met le fardeau de la manipulation de l'erreur sur l'appelant - comme il se doit.

 2
Author: Lonely Neuron, 2018-05-24 18:11:04

, Nous pouvons utiliser RxJava (très puissant réactives de l'extension 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 renvoie une exception si aucun utilisateur ou de plus d'un utilisateur est trouvé.

 1
Author: frhack, 2016-01-07 21:56:27

Comme Collectors.toMap(keyMapper, valueMapper) utilise un lancer de fusion pour gérer plusieurs entrées avec la même clé, c'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 en double. Mais à la fin, je ne sais pas si le code ne serait pas encore plus lisible en utilisant un if.

 1
Author: Arne Burmeister, 2016-09-08 07:57:25

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

singleOptional() lève une exception si il y a 0 ou plus de 1 éléments dans le Stream, sinon, elle 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 le auteur des deux bibliothèques.

 1
Author: John McClean, 2018-09-05 10:06:25

J'utilise ces deux collectionneurs:

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
Author: Xavier Dury, 2017-05-16 07:15:35

Je suis allé avec l'approche directe et j'ai juste implémenté 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 pas threadsafe.

 0
Author: gerardw, 2018-04-03 19:00:54

Utilisation de reduce

C'est le moyen le plus simple et le plus flexible que j'ai trouvé (basé 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:

  • le Facultatif - comme toujours avec votre objet ou Facultatif.vide () s'il n'est pas présent
  • l'exception (avec éventuellement VOTRE type/message personnalisé) s'il y a plus d'un élément
 0
Author: 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
Author: pardeep131085, 2018-09-11 13:30:46