web-dev-qa-db-fra.com

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

J'essaie d'utiliser Java 8 Stream s pour rechercher des éléments dans un LinkedList. Je veux cependant garantir qu’il n’ya qu’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 User en fonction de son ID. Cependant, rien ne garantit combien Users correspond au filtre.

Changer la ligne de filtre pour:

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

Lance un NoSuchElementException (bien!)

Je voudrais cependant qu'il y ait une erreur s'il y a plusieurs correspondances. Y a-t-il un moyen de faire cela?

164
ryvantage

Créer un 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 Collector désirée par 

  1. Collecter nos objets dans une List avec le collecteur Collectors.toList().
  2. L'application d'un finisseur supplémentaire à la fin, qui renvoie l'élément unique - ou génère une variable IllegalStateException si list.size != 1.

Utilisé comme:

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

Vous pouvez ensuite personnaliser cette Collector autant de fois que vous le souhaitez, par exemple en donnant l'exception en tant qu'argument dans le constructeur, ajustez-la pour autoriser deux valeurs, et plus.

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

Vous pouvez utiliser une «solution de contournement» impliquant peek() et une AtomicInteger, mais vous ne devriez vraiment pas l'utiliser.

Ce que vous pouvez faire istead, c'est simplement le rassembler dans une 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);
140
skiwi

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

Ceci obtient le seul élément d’appariement du flux, en 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 lancer une exception à l'avance et représente le résultat sous la forme d'une Optional contenant soit l'élément unique, soit rien (vide) s'il existe zéro ou plusieurs éléments:

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

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

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

Ensuite, vérifiez la taille de la liste de résultats.

77
Stuart Marks

Goyave fournit MoreCollectors.onlyElement() qui fait la bonne chose ici. Mais si vous devez le faire vous-même, vous pouvez utiliser votre propre Collector pour ceci:

<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 cette Collector autant que vous le souhaitez.

46
Louis Wasserman

Utilisez MoreCollectors.onlyElement() ( JavaDoc ) de Guava.

Il fait ce que vous voulez et lance une IllegalArgumentException si le flux est composé de deux éléments ou plus, et un NoSuchElementException si le flux est vide.

Usage:

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

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

L'opération "hachure d'échappement" qui vous permet de faire des choses étranges qui ne sont autrement pas supportées par des flux consiste à demander une 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 une Iterator et obtenir le seul élément, levé s'il y a zéro ou plusieurs éléments, ce qui pourrait remplacer les n-1 lignes du bas.

28
Brian Goetz

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 ne vous 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 se sent 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 revient 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 encapsulé dans une option.

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

19
assylias

Une alternative consiste à utiliser la réduction: (Cet exemple utilise des chaînes mais peut facilement s'appliquer à tout 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 de User, vous auriez:

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

Utilisation de 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 Collection contient exactement un élément. Si vous savez déjà que c'est le cas, appelez le:

User user = result.orElseThrow();

Cela impose à l'appelant le fardeau de la gestion de l'erreur - comme il se doit.

5
Lonely Neuron

Guava a une Collector pour cette fonction appelée MoreCollectors.onlyElement() .

4
Hans

Si vous n’êtes pas dérangé par l’utilisation d’une bibliothèque tierce partie, SequenceM from cyclops-streams (et LazyFutureStream from simple-react ) sont tous deux des opérateurs single et singleOptional. 

singleOptional() lève une exception s'il y a 0 ou plus de 1 éléments dans la variable Stream, sinon la valeur unique est renvoyée.

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 valeur ou plus d'une valeur dans 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.

1
John McClean

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

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 une IllegalStateException pour les clés en double. Mais à la fin, je ne suis pas sûr que le code ne serait pas encore plus lisible avec un if.

1
Arne Burmeister

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);
}
1
Xavier Dury

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

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

1
frhack

En utilisant réduire

C’est la méthode la plus simple et la plus 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:

  • the Optional - 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 
0
Fabio Bonfante

Je suis allé avec l'approche directe et viens d'implémenter 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 not threadsafe.

0
gerardw