Sto cercando di utilizzare Java 8 Stream
s per trovare elementi in una LinkedList
. Voglio garantire, tuttavia, che esiste una sola corrispondenza per i criteri di filtro.
Prendi questo codice:
public static void main(String[] args) { LinkedList 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; } }
Questo codice trova un User
base al suo ID. Ma non ci sono garanzie su quanti User
abbinato il filtro.
Modifica della linea del filtro su:
User match = users.stream().filter((user) -> user.getId() < 0).findAny().get();
NoSuchElementException
una NoSuchElementException
(buona!)
Mi piacerebbe che lanciasse un errore se ci sono più corrispondenze, però. C’è un modo per fare questo?
Collector
personalizzato public static Collector toSingleton() { return Collectors.collectingAndThen( Collectors.toList(), list -> { if (list.size() != 1) { throw new IllegalStateException(); } return list.get(0); } ); }
Usiamo Collectors.collectingAndThen
per build il nostro Collector
desiderato da
List
con il Collectors.toList()
. IllegalStateException
se list.size != 1
. Usato come:
User resultUser = users.stream() .filter(user -> user.getId() > 0) .collect(toSingleton());
È quindi ansible personalizzare questo Collector
quanto si desidera, ad esempio fornire l’eccezione come argomento nel costruttore, modificarlo per consentire due valori e altro.
È ansible utilizzare una “soluzione alternativa” che include AtomicInteger
peek()
e un object AtomicInteger
, ma in realtà non si dovrebbe utilizzarlo.
Quello che potresti fare istead è semplicemente raccoglierlo in un List
, come questo:
LinkedList users = new LinkedList<>(); users.add(new User(1, "User1")); users.add(new User(2, "User2")); users.add(new User(3, "User3")); List resultUserList = users.stream() .filter(user -> user.getId() == 1) .collect(Collectors.toList()); if (resultUserList.size() != 1) { throw new IllegalStateException(); } User resultUser = resultUserList.get(0);
Per completezza, ecco il ‘one-liner’ corrispondente all’eccellente risposta di @ prunge:
User user1 = users.stream() .filter(user -> user.getId() == 1) .reduce((a, b) -> { throw new IllegalStateException("Multiple elements: " + a + ", " + b); }) .get();
Questo ottiene l’unico elemento corrispondente dal stream, lanciando
NoSuchElementException
nel caso in cui lo stream sia vuoto, o IllegalStateException
nel caso in cui lo stream contenga più di un elemento corrispondente. Una variante di questo approccio evita di lanciare un’eccezione in anticipo e invece rappresenta il risultato come Optional
contenente il solo elemento, o nulla (vuoto) se ci sono zero o più elementi:
Optional user1 = users.stream() .filter(user -> user.getId() == 1) .collect(Collectors.reducing((a, b) -> null));
Le altre risposte che implicano la scrittura di un Collector
personalizzato sono probabilmente più efficienti (come Louis Wasserman , +1), ma se vuoi brevità, suggerirei quanto segue:
List result = users.stream() .filter(user -> user.getId() == 1) .limit(2) .collect(Collectors.toList());
Quindi verificare la dimensione dell’elenco dei risultati.
Guava fornisce MoreCollectors.onlyElement()
che fa la cosa giusta qui. Ma se devi farlo da solo, puoi arrotolare il tuo Collector
per questo:
Collector> 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); }
… o utilizzando il proprio tipo di Holder
posto di AtomicReference
. Puoi riutilizzare quel Collector
quanto vuoi.
Usa MoreCollectors.onlyElement()
( JavaDoc ) di Guava.
Fa ciò che vuoi e genera un IllegalArgumentException
se lo stream è costituito da due o più elementi e una NoSuchElementException
se lo stream è vuoto.
import static com.google.common.collect.MoreCollectors.onlyElement; User match = users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());
L’operazione “escape hatch” che ti permette di fare cose strane che non sono altrimenti supportate dai flussi è chiedere un Iterator
:
Iterator 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 ha un metodo comodo per prendere un Iterator
e ottenere l'unico elemento, lanciando se ci sono zero o più elementi, che potrebbero sostituire qui le linee inferiori n-1.
Bel suggerimento nel commento di @Holger:
Optional match = users.stream() .filter((user) -> user.getId() > 1) .reduce((u, v) -> { throw new IllegalStateException("More than one ID found") });
L’eccezione viene generata da Optional#get
, ma se si dispone di più di un elemento che non sarà di aiuto. Puoi raccogliere gli utenti in una raccolta che accetta solo un elemento, ad esempio:
User match = users.stream().filter((user) -> user.getId() > 1) .collect(toCollection(() -> new ArrayBlockingQueue(1))) .poll();
che lancia una java.lang.IllegalStateException: Queue full
, ma che sembra troppo hacky.
Oppure puoi utilizzare una riduzione combinata con un’opzione facoltativa:
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 riduzione essenzialmente ritorna:
Il risultato è quindi avvolto in un opzionale.
Ma la soluzione più semplice sarebbe probabilmente quella di raccogliere solo una raccolta, verificare che la sua dimensione sia 1 e ottenere l’unico elemento.
Un’alternativa è usare la riduzione: (questo esempio usa le stringhe ma potrebbe facilmente applicarsi a qualsiasi tipo di object che include l’ User
)
List 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 BinaryOperator thereCanBeOnlyOne() { return (a, b) -> {throw new RuntimeException("Duplicate elements found: " + a + " and " + b);}; }
Quindi nel caso User
avresti:
User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();
Guava ha un Collector
per questo chiamato MoreCollectors.onlyElement()
.
Collector
: public static Collector> toSingleton() { return Collectors.collectingAndThen( Collectors.toList(), list -> list.size() == 1 ? Optional.of(list.get(0)) : Optional.empty() ); }
Optional result = users.stream() .filter((user) -> user.getId() < 0) .collect(toSingleton());
Restituiamo un Optional
, poiché di solito non possiamo assumere che la Collection
contenga esattamente un elemento. Se già conosci questo, chiama:
User user = result.orElseThrow();
Ciò pone l'onere di gestire l'errore sul chiamante, come dovrebbe.
Possiamo usare RxJava (libreria di estensione retriggers molto potente)
LinkedList 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();
Il singolo operatore genera un’eccezione se non viene trovato nessun utente o più di un utente.
Collectors.toMap(keyMapper, valueMapper)
utilizza una fusione di lancio per gestire più voci con la stessa chiave è facile:
List 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();
Otterrai una IllegalStateException
per chiavi duplicate. Ma alla fine non sono sicuro se il codice non sarebbe ancora più leggibile usando un if
.
Se non ti dispiace usare una libreria di terze parti, SequenceM
da cyclops-streams (e LazyFutureStream
da simple-react ) entrambi hanno operatori single e singleOptional.
singleOptional()
genera un’eccezione se ci sono 0
o più di 1
elementi nel Stream
, altrimenti restituisce il valore singolo.
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()
restituisce Optional.empty()
se non ci sono valori o più di un valore nel Stream
.
Optional result = SequenceM.fromStream(Stream.of("x")) .singleOptional(); //Optional["x"] Optional result = SequenceM.of().singleOptional(); // Optional.empty Optional result = SequenceM.of(1, 2, 3).singleOptional(); // Optional.empty
Divulgazione – Sono l’autore di entrambe le biblioteche.
Sto usando quei due collezionisti:
public static Collector> zeroOrOne() { return Collectors.reducing((a, b) -> { throw new IllegalStateException("More than one value was returned"); }); } public static Collector onlyOne() { return Collectors.collectingAndThen(zeroOrOne(), Optional::get); }
Sono andato con l’approccio diretto e ho appena implementato la cosa:
public class CollectSingle implements Collector, BiConsumer, Function, Supplier { T value; @Override public Supplier supplier() { return this; } @Override public BiConsumer accumulator() { return this; } @Override public BinaryOperator combiner() { return null; } @Override public Function finisher() { return this; } @Override public Set 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; } }
con il test JUnit:
public class CollectSingleTest { @Test public void collectOne( ) { List 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 lst = new ArrayList<>(); lst.add(7); lst.add(8); Integer o = lst.stream().collect( new CollectSingle<>()); } }
Questa implementazione non è protetta da bug.
Questo è il modo più semplice e flessibile che ho trovato (basato sulla risposta @prunge)
Optional user = users.stream() .filter(user -> user.getId() == 1) .reduce((a, b) -> { throw new IllegalStateException("Multiple elements: " + a + ", " + b); })
In questo modo ottieni:
Hai provato questo
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.