Filtra Java Stream su 1 e solo 1 elemento


230

Sto cercando di usare Java 8 Streams per trovare elementi in a LinkedList. Voglio garantire, tuttavia, che esiste una e una sola corrispondenza ai criteri di filtro.

Prendi questo codice:

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

Questo codice trova un in Userbase al loro ID. Ma non ci sono garanzie sul numero di Users corrispondenti al filtro.

Modifica della linea del filtro in:

User match = users.stream().filter((user) -> user.getId() < 0).findAny().get();

Lancerà un NoSuchElementException(buono!)

Vorrei che generasse un errore se ci sono più partite, però. C'è un modo per fare questo?


count()è un'operazione terminale quindi non puoi farlo. Lo stream non può essere utilizzato dopo.
Alexis C.

Ok, grazie @ZouZou. Non ero del tutto sicuro di cosa facesse quel metodo. Perché non c'è Stream::size?
ryvantage

7
@ryvantage Perché uno stream può essere usato solo una volta: calcolarne le dimensioni significa "iterare" su di esso e successivamente non è più possibile utilizzare lo stream.
Assylias,

3
Wow. Quel commento mi ha aiutato a capire Streammolto di più di quello che ho fatto prima ...
svantaggio del

2
Questo è quando ti rendi conto che avevi bisogno di usare un LinkedHashSet(supponendo che tu voglia conservare l'ordine di inserzione) o un HashSettutto insieme. Se la tua raccolta viene utilizzata solo per trovare un singolo ID utente, perché raccogli tutti gli altri elementi? Se esiste un potenziale che dovrai sempre trovare un ID utente che deve anche essere univoco, perché usare un elenco e non un set? Stai programmando al contrario. Usa la collezione giusta per il lavoro e risparmia questo mal di testa
smac89,

Risposte:


192

Crea un'abitudine 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);
            }
    );
}

Usiamo Collectors.collectingAndThenper costruire il nostro desiderato Collectorda

  1. Collezionare i nostri oggetti in a Listcon il Collectors.toList()collezionista.
  2. Applicando un finisher aggiuntivo alla fine, che restituisce il singolo elemento - o genera un IllegalStateExceptionif list.size != 1.

Usato come:

User resultUser = users.stream()
        .filter(user -> user.getId() > 0)
        .collect(toSingleton());

È quindi possibile personalizzare ciò Collectorquanto si desidera, ad esempio fornire l'eccezione come argomento nel costruttore, modificarlo per consentire due valori e altro.

Un'alternativa - probabilmente meno elegante - soluzione:

Puoi usare una "soluzione alternativa" che coinvolge peek()e un AtomicInteger, ma in realtà non dovresti usarlo.

Quello che potresti fare è semplicemente raccoglierlo in un List, come questo:

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

24
Guava Iterables.getOnlyElementaccorcerebbe queste soluzioni e fornirebbe migliori messaggi di errore. Proprio come un suggerimento per i colleghi lettori che già utilizzano Google Guava.
Tim Büthe

2
ho racchiuso questa idea in una classe - gist.github.com/denov/a7eac36a3cda041f8afeabcef09d16fc
denov

1
@LonelyNeuron Per favore, non modificare il mio codice. Mi mette in una situazione in cui ho bisogno di convalidare la mia intera risposta, che ho scritto quattro anni fa, e semplicemente non ho tempo per farlo proprio ora.
Skiwi,

2
@skiwi: la modifica di Lonely è stata utile e corretta, quindi l'ho ripristinata dopo la revisione. Le persone che visitano questa risposta oggi non si preoccupano di come sei arrivato alla risposta, non hanno bisogno di vedere la vecchia versione, la nuova versione e una sezione aggiornata . Ciò rende la tua risposta più confusa e meno utile. È molto meglio mettere i post in uno stato finale e se le persone vogliono vedere come è andato tutto, possono visualizzare la cronologia dei post.
Martijn Pieters

1
@skiwi: il codice nella risposta è assolutamente quello che hai scritto. Tutto quello che l'editor ha fatto è stato ripulire il tuo post, rimuovendo solo una versione precedente della singletonCollector()definizione obsoleta dalla versione che rimane nel post e rinominandolo toSingleton(). La mia esperienza nel flusso Java è un po 'arrugginita, ma la ridenominazione mi sembra utile. Revisionare questa modifica mi ha richiesto 2 minuti, al massimo. Se non hai tempo per rivedere le modifiche, posso suggerire di chiedere a qualcun altro di farlo in futuro, magari nella chat room di Java ?
Martijn Pieters

118

Per completezza, ecco il 'one-liner' corrispondente alla risposta eccellente 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 dallo stream, lanciando

  • NoSuchElementException nel caso in cui il flusso sia vuoto, oppure
  • IllegalStateException nel caso in cui il flusso contenga più di un elemento corrispondente.

Una variante di questo approccio evita di lanciare un'eccezione in anticipo e rappresenta invece il risultato come Optionalcontenente il solo elemento o nulla (vuoto) se ci sono zero o più elementi:

Optional<User> user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.reducing((a, b) -> null));

3
Mi piace l'approccio iniziale in questa risposta. Ai fini della personalizzazione, è possibile convertire l'ultimo get()inorElseThrow()
arin

1
Mi piace la brevità di questo, e il fatto che eviti di creare un'istanza di List non necessaria ogni volta che viene chiamata.
LordOfThePigs il

83

Le altre risposte che comportano la scrittura di un'abitudine Collectorsono probabilmente più efficienti (come Louis Wasserman , +1), ma se si desidera la brevità, suggerirei quanto segue:

List<User> result = users.stream()
    .filter(user -> user.getId() == 1)
    .limit(2)
    .collect(Collectors.toList());

Quindi verificare la dimensione dell'elenco dei risultati.

if (result.size() != 1) {
  throw new IllegalStateException("Expected exactly one user but got " + result);
User user = result.get(0);
}

5
Qual è lo scopo di limit(2)questa soluzione? Che differenza farebbe se l'elenco risultante fosse 2 o 100? Se è maggiore di 1.
svantaggio

18
Si ferma immediatamente se trova una seconda corrispondenza. Questo è ciò che fanno tutti i collezionisti di fantasia, usando solo più codice. :-)
Stuart segna il

10
Che ne dici di aggiungereCollectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
Lukas Eder l'

1
Javadoc dice param di questo limite su: maxSize: the number of elements the stream should be limited to. Quindi, non dovrebbe essere .limit(1)invece di .limit(2)?
alexbt

5
@alexbt L'affermazione del problema è assicurarsi che esista esattamente un (non più, non meno) elemento corrispondente. Dopo il mio codice, si può provare result.size()per assicurarsi che sia uguale a 1. Se è 2, allora c'è più di una corrispondenza, quindi è un errore. Se invece lo facesse il codice limit(1), più di una corrispondenza comporterebbe un singolo elemento, che non può essere distinto dal fatto che esiste esattamente una corrispondenza. Ciò perderebbe un caso di errore di cui l'OP era preoccupato.
Stuart Marks,

67

Guava fornisce ciò MoreCollectors.onlyElement()che fa la cosa giusta qui. Ma se devi farlo da solo, puoi farlo da solo 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);
}

... o usando il tuo Holdertipo invece di AtomicReference. Puoi riutilizzarlo Collectorquanto vuoi.


Il singletonCollector di @ skiwi era più piccolo e più facile da seguire di questo, ecco perché gli ho dato il conto. Ma è bello vedere il consenso nella risposta: un'usanza Collectorera la strada da percorrere.
ryvantage

1
Giusto. Miravo principalmente alla velocità, non alla concisione.
Louis Wasserman,

1
Si? Perché il tuo è più veloce?
ryvantage

3
Principalmente perché l'allocazione di un all-up Listè più costosa di un singolo riferimento modificabile.
Louis Wasserman,

1
@LouisWasserman, la frase di aggiornamento finale MoreCollectors.onlyElement()dovrebbe essere effettivamente la prima (e forse l'unica :))
Piotr Findeisen,

46

Usa Guava MoreCollectors.onlyElement()( JavaDoc ).

Fa quello che vuoi e genera un IllegalArgumentExceptionse il flusso è composto da due o più elementi e un NoSuchElementExceptionse il flusso è vuoto.

Uso:

import static com.google.common.collect.MoreCollectors.onlyElement;

User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());

2
Nota per gli altri utenti: MoreCollectorsfa parte della versione inedita 21. (2016-12) inedita 21.
qerub

2
Questa risposta dovrebbe andare in alto.
Emdadul Sawon,

31

L'operazione "tratteggio di escape" che ti consente di fare cose strane che altrimenti non sono supportate dagli stream è di chiedere 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 ha un metodo pratico per prendere un Iteratore ottenere l'unico elemento, lanciando se ci sono zero o più elementi, che potrebbero sostituire qui le righe n-1 inferiori.


4
Metodo di Guava: Iterators.getOnlyElement (Iterator <T> iteratore).
anre,

23

Aggiornare

Bel suggerimento nel commento di @Holger:

Optional<User> 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 hai più di un elemento che non ti aiuterà. È possibile 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<User>(1)))
                  .poll();

che genera un java.lang.IllegalStateException: Queue full, ma sembra troppo confuso.

Oppure potresti usare una riduzione combinata con un optional:

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 restituisce:

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

Il risultato viene quindi racchiuso in un facoltativo.

Ma la soluzione più semplice sarebbe probabilmente quella di raccogliere solo in una collezione, verificare che la sua dimensione sia 1 e ottenere l'unico elemento.


1
Vorrei aggiungere un elemento di identità ( null) per impedire l'utilizzo get(). Sfortunatamente il tuo reducenon funziona come pensi che pensi, considera un Streamche ha nullelementi in esso, forse pensi di averlo coperto, ma posso esserlo [User#1, null, User#2, null, User#3], ora non proverò un'eccezione, a meno che non mi sbagli qui.
Skiwi,

2
@Skiwi se ci sono elementi null il filtro lancerà prima un NPE.
Assylias,

2
Dato che sai che il flusso non può passare nullalla funzione di riduzione, la rimozione dell'argomento del valore di identità renderebbe nullobsoleta l'intera gestione della funzione: reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )fa il lavoro e, meglio ancora, restituisce un Optional, eliminando la necessità di chiamare Optional.ofNullableil risultato.
Holger,

15

Un'alternativa è usare la riduzione: (questo esempio usa stringhe ma potrebbe facilmente applicarsi a qualsiasi tipo di oggetto incluso 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);};
}

Quindi per il caso con Userte avresti:

User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();

8

Utilizzando ridurre

Questo è il modo più semplice e flessibile che ho trovato (basato sulla risposta di @prunge)

Optional<User> user = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })

In questo modo ottieni:

  • l'Opzionale - come sempre con il tuo oggetto o Optional.empty()se non presente
  • l'eccezione (con eventualmente il TUO tipo / messaggio personalizzato) se c'è più di un elemento

6

Penso che in questo modo sia più semplice:

User resultUser = users.stream()
    .filter(user -> user.getId() > 0)
    .findFirst().get();

4
Trova solo per primo, ma il caso era anche quello di lanciare Eccezione quando è più di uno
lczapski,

5

Utilizzando 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()
    );
}

Uso:

Optional<User> result = users.stream()
        .filter((user) -> user.getId() < 0)
        .collect(toSingleton());

Restituiamo un Optional, poiché di solito non possiamo supporre Collectionche contenga esattamente un elemento. Se sai già che è così, chiama:

User user = result.orElseThrow();

Ciò comporta l'onere di gestire l'errore sul chiamante, come dovrebbe.



1

Possiamo usare RxJava ( libreria di estensioni reattive molto potente )

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

Il singolo operatore genera un'eccezione se non viene trovato nessun utente o più di un utente.


Tuttavia, la risposta corretta, l'inizializzazione di un flusso o di una raccolta bloccati non è probabilmente molto economica (in termini di risorse).
Karl Richter,

1

Poiché Collectors.toMap(keyMapper, valueMapper)utilizza una fusione di lancio per gestire più voci con la stessa chiave, è 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();

Riceverai una IllegalStateExceptionchiave duplicata. Ma alla fine non sono sicuro che il codice non sarebbe ancora più leggibile usando un if.


1
Ottima soluzione! E se lo fai .collect(Collectors.toMap(user -> "", Function.identity())).get(""), hai un comportamento più generico.
glglgl,

1

Sto usando quei due collezionisti:

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

! Neat onlyOne()genera IllegalStateException> 1 elementi e NoSuchElementException` (in Optional::get) per 0 elementi.
simon04

@ simon04 Potresti sovraccaricare i metodi per prendere un Supplierdi (Runtime)Exception.
Xavier Dury,

1

Se non ti dispiace usare una libreria di terze parti, SequenceMdai flussi di ciclopi (e LazyFutureStreamdalla reazione semplice ) entrambi hanno operatori single e singleOptional.

singleOptional()genera un'eccezione se ci sono 0o più di 1elementi in Stream, altrimenti restituisce il singolo valore.

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()ritorna Optional.empty()se non ci sono valori o più di un valore in 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

Divulgazione - Sono l'autore di entrambe le biblioteche.


0

Sono andato con l'approccio diretto e ho appena implementato la cosa:

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


} 

con il 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<>());
}

}

Questa implementazione non è sicura.


0
User match = users.stream().filter((user) -> user.getId()== 1).findAny().orElseThrow(()-> new IllegalArgumentException());

5
Sebbene questo codice possa risolvere la domanda, inclusa una spiegazione di come e perché questo risolva il problema, aiuterebbe davvero a migliorare la qualità del tuo post e probabilmente comporterebbe più voti positivi. Ricorda che stai rispondendo alla domanda per i lettori in futuro, non solo per la persona che chiede ora. Modifica la tua risposta per aggiungere spiegazioni e fornire un'indicazione di quali limitazioni e ipotesi si applicano.
David Buck,

-2

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.

Fonte: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html


3
È stato detto che count()non è buono da usare perché è un'operazione terminale.
ryvantage

Se questa è davvero una citazione, aggiungi le tue fonti
Neuron,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.