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?
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
- collectionner nos objets dans un
List
avec leCollectors.toList()
collector. - appliquer un finisher supplémentaire à la fin, qui renvoie l'élément simple - ou jette un
IllegalStateException
silist.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);
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));
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.
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.
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());
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.
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.
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();
Goyave a un Collector
pour ce appelé MoreCollectors.onlyElement()
.
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.
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é.
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
.
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.
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);
}
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.
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
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