Filtrer le flux Java en 1 et seulement 1 élément
Je suis en train d'utiliser Java 8 Stream
s 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 User
correspondant 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?
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
- Rassembler nos objets dans un
List
avec leCollectors.toList()
collector. - En appliquant un finisseur supplémentaire à la fin, qui renvoie l'élément unique - ou jette un
IllegalStateException
iflist.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);
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));
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.
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.
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());
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.
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.
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();
Goyave est un Collector
pour ce qui est appelé MoreCollectors.onlyElement()
.
À 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.
, 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é.
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
.
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.
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);
}
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.
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
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