Filtra Java Stream su 1 e solo 1 elemento

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?

Crea un servizio di 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

  1. Raccolta dei nostri oggetti in una List con il Collectors.toList() .
  2. Applicando un finisher extra alla fine, questo restituisce il singolo elemento o genera un 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.

Un’alternativa – probabilmente meno elegante – soluzione:

È 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.

Uso:

 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.

Aggiornare

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") }); 

Risposta originale

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:

  • null se nessun utente viene trovato
  • l’utente se ne viene trovato uno solo
  • genera un’eccezione se ne viene trovata più di una

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

Utilizzando un Collector :

 public static  Collector> toSingleton() { return Collectors.collectingAndThen( Collectors.toList(), list -> list.size() == 1 ? Optional.of(list.get(0)) : Optional.empty() ); } 

Uso:

 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.

Utilizzando ridurre

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:

  • Opzionale – come sempre con il tuo object o Optional.empty () se non presente
  • l’Eccezione (con eventualmente il TUO tipo / messaggio personalizzato) se c’è più di un elemento

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.