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 User
s 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?
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
List
avec le collecteur Collectors.toList()
.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.
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);
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, ouIllegalStateException
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));
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.
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.
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.
import static com.google.common.collect.MoreCollectors.onlyElement;
User match =
users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());
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.
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") });
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:
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.
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();
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()
);
}
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.
Guava a une Collector
pour cette fonction appelée MoreCollectors.onlyElement()
.
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.
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
.
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);
}
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é.
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:
Optional.empty()
s'il n'est pas présentJe 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.