Filtern Sie Java Stream auf 1 und nur 1 Element

227

Ich versuche Java 8 Streams zu verwenden, um Elemente in a zu finden LinkedList. Ich möchte jedoch garantieren, dass es nur eine Übereinstimmung mit den Filterkriterien gibt.

Nehmen Sie diesen 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;
    }
}

Dieser Code findet eine Userbasierend auf ihrer ID. Es gibt jedoch keine Garantie dafür, wie viele Users zum Filter passen.

Ändern der Filterzeile in:

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

Wird ein NoSuchElementException(gut!) Werfen

Ich möchte, dass es einen Fehler gibt, wenn es mehrere Übereinstimmungen gibt. Gibt es eine Möglichkeit, dies zu tun?

Ryvantage
quelle
count()ist eine Terminaloperation, also können Sie das nicht tun. Der Stream kann danach nicht mehr verwendet werden.
Alexis C.
Ok, danke @ZouZou. Ich war mir nicht ganz sicher, was diese Methode tat. Warum gibt es keine Stream::size?
Ryvantage
7
@ryvantage Da ein Stream nur einmal verwendet werden kann: Wenn Sie seine Größe berechnen, müssen Sie darüber "iterieren". Danach können Sie den Stream nicht mehr verwenden.
Assylias
3
Beeindruckend. Dieser eine Kommentar hat mir geholfen, Streams so viel besser zu verstehen als zuvor ...
ryvantage
2
In diesem LinkedHashSetFall stellen Sie fest, dass Sie eine (vorausgesetzt, Sie möchten, dass die Einfügereihenfolge beibehalten wird) oder eine HashSetganze Zeit verwenden mussten. Wenn Ihre Sammlung nur zum Auffinden einer einzelnen Benutzer-ID verwendet wird, warum sammeln Sie dann alle anderen Elemente? Wenn es ein Potenzial gibt, dass Sie immer eine Benutzer-ID finden müssen, die ebenfalls eindeutig sein muss, warum dann eine Liste und keine Menge verwenden? Sie programmieren rückwärts. Verwenden Sie die richtige Sammlung für den Job und sparen Sie sich diese Kopfschmerzen
smac89

Antworten:

189

Erstellen Sie eine benutzerdefinierte 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);
            }
    );
}

Wir verwenden Collectors.collectingAndThen, um unsere gewünschten Collectorvon zu konstruieren

  1. Sammeln Sie unsere Objekte in einem Listmit dem Collectors.toList()Sammler.
  2. Anwenden eines zusätzlichen Finishers am Ende, der das einzelne Element zurückgibt - oder ein IllegalStateExceptionif auslöst list.size != 1.

Benutzt als:

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

Sie können dies dann beliebig anpassen Collector, z. B. die Ausnahme als Argument im Konstruktor angeben, sie so anpassen , dass zwei Werte zulässig sind, und mehr.

Eine alternative - wohl weniger elegante - Lösung:

Sie können eine 'Problemumgehung' verwenden, die eine peek()und eine beinhaltet AtomicInteger, aber das sollten Sie wirklich nicht verwenden.

Was Sie tun können, ist, es einfach in einem zu sammeln List, wie folgt :

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);
Skiwi
quelle
23
Guaven Iterables.getOnlyElementwürden diese Lösungen verkürzen und bessere Fehlermeldungen liefern. Nur als Tipp für andere Leser, die bereits Google Guava verwenden.
Tim Büthe
2
Ich habe diese Idee in eine Klasse eingepackt
gist.github.com/denov/a7eac36a3cda041f8afeabcef09d16fc
1
@LonelyNeuron Bitte bearbeiten Sie meinen Code nicht. Es bringt mich in eine Situation, in der ich meine gesamte Antwort, die ich vor vier Jahren geschrieben habe, validieren muss und für die ich momentan einfach keine Zeit habe.
Skiwi
2
@skiwi: Lonelys Bearbeitung war hilfreich und korrekt, daher habe ich sie nach Überprüfung erneut installiert. Personen, die diese Antwort heute besuchen, ist es egal, wie Sie zu der Antwort gekommen sind. Sie müssen nicht die alte und die neue Version sowie einen aktualisierten Abschnitt sehen. Das macht Ihre Antwort verwirrender und weniger hilfreich. Es ist viel besser, Beiträge in einen endgültigen Zustand zu versetzen , und wenn die Leute sehen möchten, wie sich alles abgespielt hat, können sie den Beitragsverlauf anzeigen.
Martijn Pieters
1
@skiwi: Der Code in der Antwort ist absolut das, was Sie geschrieben haben. Der Redakteur alle tat , waren sauber auf Ihre Post, nur das Entfernen eine frühere Version der singletonCollector()Definition der Version verworfen wird, dass Reste in der Post, und Umbenennung zu toSingleton(). Meine Java-Stream-Kenntnisse sind etwas verrostet, aber das Umbenennen scheint mir hilfreich zu sein. Das Überprüfen dieser Änderung hat 2 Minuten gedauert. Wenn Sie keine Zeit haben, Änderungen zu überprüfen, kann ich vorschlagen, dass Sie in Zukunft jemanden bitten, dies zu tun, möglicherweise im Java-Chatroom ?
Martijn Pieters
118

Der Vollständigkeit halber ist hier der 'Einzeiler', der der hervorragenden Antwort von @ prunge entspricht:

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

Dies erhält das einzige passende Element aus dem Strom, das wirft

  • NoSuchElementException falls der Stream leer ist, oder
  • IllegalStateException falls der Stream mehr als ein übereinstimmendes Element enthält.

Eine Variation dieses Ansatzes vermeidet das frühzeitige Auslösen einer Ausnahme und stellt das Ergebnis stattdessen so dar Optional, dass es entweder das einzige Element oder nichts (leer) enthält, wenn null oder mehrere Elemente vorhanden sind:

Optional<User> user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.reducing((a, b) -> null));
glänzt
quelle
3
Ich mag den ersten Ansatz in dieser Antwort. Für Anpassungszwecke ist es möglich, den letzten get()inorElseThrow()
arin
1
Ich mag die Kürze dieser und die Tatsache, dass es vermieden wird, bei jedem Aufruf eine unnötige List-Instanz zu erstellen.
LordOfThePigs
83

Die anderen Antworten, die das Schreiben eines Brauchs beinhalten, Collectorsind wahrscheinlich effizienter (wie die von Louis Wasserman , +1), aber wenn Sie Kürze wünschen, würde ich Folgendes vorschlagen:

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

Überprüfen Sie dann die Größe der Ergebnisliste.

if (result.size() != 1) {
  throw new IllegalStateException("Expected exactly one user but got " + result);
User user = result.get(0);
}
Stuart Marks
quelle
5
Was bringt limit(2)diese Lösung? Welchen Unterschied würde es machen, ob die resultierende Liste 2 oder 100 war? Wenn es größer als 1.
Ryvantage
18
Es stoppt sofort, wenn es eine zweite Übereinstimmung findet. Dies ist, was alle ausgefallenen Sammler tun, nur um mehr Code zu verwenden. :-)
Stuart Marks
10
Wie wäre es mit HinzufügenCollectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
Lukas Eder
1
Javadoc sagt dies über Limits Parameter : maxSize: the number of elements the stream should be limited to. Also sollte es nicht .limit(1)statt sein .limit(2)?
Alexbt
5
@alexbt Die Problemstellung besteht darin, sicherzustellen, dass genau ein (nicht mehr und nicht weniger) übereinstimmendes Element vorhanden ist. Nach meinem Code kann man testen result.size(), ob er gleich 1 ist. Wenn es 2 ist, gibt es mehr als eine Übereinstimmung, also ist es ein Fehler. Wenn der Code dies stattdessen tun würde limit(1), würde mehr als eine Übereinstimmung zu einem einzelnen Element führen, das nicht von genau einer Übereinstimmung unterschieden werden kann. Dies würde einen Fehlerfall übersehen, über den das OP besorgt war.
Stuart Marks
67

Guave bietet, MoreCollectors.onlyElement()was hier das Richtige tut. Aber wenn Sie es selbst tun müssen, können Sie Ihre eigenen Collectordafür rollen :

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

... oder verwenden Sie Holderstattdessen Ihren eigenen Typ AtomicReference. Sie können das Collectorso oft wiederverwenden, wie Sie möchten.

Louis Wasserman
quelle
@ skiwis singletonCollector war kleiner und leichter zu verfolgen, deshalb habe ich ihm den Scheck gegeben. Aber es ist gut, Konsens in der Antwort zu sehen: Ein Brauch Collectorwar der richtige Weg.
Ryvantage
1
Meinetwegen. Ich strebte hauptsächlich nach Geschwindigkeit, nicht nach Prägnanz.
Louis Wasserman
1
Ja? Warum ist deins schneller?
Ryvantage
3
Meistens, weil das Zuweisen eines All-Ups Listteurer ist als eine einzelne veränderbare Referenz.
Louis Wasserman
1
@ LouisWasserman, der letzte Update-Satz über MoreCollectors.onlyElement()sollte eigentlich der erste (und vielleicht der einzige :) sein
Piotr Findeisen
46

Verwenden Sie GuavasMoreCollectors.onlyElement() ( JavaDoc ).

Es macht, was Sie wollen und wirft ein, IllegalArgumentExceptionwenn der Stream aus zwei oder mehr Elementen besteht, und ein, NoSuchElementExceptionwenn der Stream leer ist.

Verwendung:

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

User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());
Trevorade
quelle
2
Hinweis für andere Benutzer: MoreCollectorsist Teil der noch nicht veröffentlichten (Stand 2016-12) unveröffentlichten Version 21.
Qerub
2
Diese Antwort sollte nach oben gehen.
Emdadul Sawon
31

Die "Escape Hatch" -Operation, mit der Sie seltsame Dinge tun können, die sonst nicht von Streams unterstützt werden, besteht darin, nach einem zu fragen 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();
}

Guave hat eine bequeme Methode, um ein Iteratorund das einzige Element zu erhalten, das geworfen wird, wenn es null oder mehrere Elemente gibt, die die unteren n-1-Zeilen hier ersetzen könnten.

Brian Goetz
quelle
4
Guavas Methode: Iterators.getOnlyElement (Iterator <T> -Iterator).
Anre
23

Aktualisieren

Netter Vorschlag im Kommentar von @Holger:

Optional<User> match = users.stream()
              .filter((user) -> user.getId() > 1)
              .reduce((u, v) -> { throw new IllegalStateException("More than one ID found") });

Ursprüngliche Antwort

Die Ausnahme wird von ausgelöst Optional#get, aber wenn Sie mehr als ein Element haben, hilft das nicht. Sie können die Benutzer in einer Sammlung sammeln, die nur ein Element akzeptiert, z. B.:

User match = users.stream().filter((user) -> user.getId() > 1)
                  .collect(toCollection(() -> new ArrayBlockingQueue<User>(1)))
                  .poll();

das wirft ein java.lang.IllegalStateException: Queue full, aber das fühlt sich zu hacky an.

Oder Sie können eine Ermäßigung in Kombination mit einer optionalen Option verwenden:

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

Die Reduzierung ergibt im Wesentlichen:

  • null, wenn kein Benutzer gefunden wird
  • der Benutzer, wenn nur einer gefunden wird
  • löst eine Ausnahme aus, wenn mehr als eine gefunden wird

Das Ergebnis wird dann in eine Option eingeschlossen.

Aber die einfachste Lösung wäre wahrscheinlich, einfach in einer Sammlung zu sammeln, zu überprüfen, ob ihre Größe 1 ist, und das einzige Element zu erhalten.

Assylien
quelle
1
Ich würde ein Identitätselement ( null) hinzufügen , um die Verwendung zu verhindern get(). Leider reducefunktioniert Ihr nicht so, wie Sie denken, denken Sie daran, Streamdass es nullElemente enthält, vielleicht denken Sie, dass Sie es abgedeckt haben, aber ich kann es sein [User#1, null, User#2, null, User#3], jetzt wird es keine Ausnahme werfen, denke ich, es sei denn, ich irre mich hier.
Skiwi
2
@Skiwi Wenn es Nullelemente gibt, löst der Filter zuerst eine NPE aus.
Assylias
2
Da Sie wissen, dass der Stream nicht nullan die Reduktionsfunktion übergeben werden kann, würde das Entfernen des Identitätswertarguments den gesamten Umgang mit nullder Funktion überflüssig machen: Erledigt den reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )Job und noch besser, er gibt bereits eine zurück Optional, sodass die Notwendigkeit des Aufrufs Optional.ofNullableder nicht mehr besteht Ergebnis.
Holger
15

Eine Alternative ist die Verwendung der Reduzierung: (In diesem Beispiel werden Zeichenfolgen verwendet, sie können jedoch problemlos auf alle Objekttypen angewendet werden, einschließlich 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);};
}

Also für den Fall mit UserIhnen hätte:

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

Verwenden Sie reduzieren

Dies ist der einfachere und flexiblere Weg, den ich gefunden habe (basierend auf der Antwort von @prunge).

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

Auf diese Weise erhalten Sie:

  • das Optionale - wie immer mit Ihrem Objekt oder Optional.empty()wenn nicht vorhanden
  • die Ausnahme (mit eventuell IHREM benutzerdefinierten Typ / Ihrer Nachricht), wenn mehr als ein Element vorhanden ist
Fabio Bonfante
quelle
6

Ich denke, dieser Weg ist einfacher:

User resultUser = users.stream()
    .filter(user -> user.getId() > 0)
    .findFirst().get();
pilladooo
quelle
4
Es findet nur zuerst, aber der Fall war auch, Ausnahme zu werfen, wenn es mehr als eins ist
lczapski
5

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

Verwendung:

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

Wir geben ein zurück Optional, da wir normalerweise nicht davon ausgehen können, dass das Collectiongenau ein Element enthält. Wenn Sie bereits wissen, dass dies der Fall ist, rufen Sie an:

User user = result.orElseThrow();

Dies belastet den Anrufer mit der Behandlung des Fehlers - wie es sollte.

Neuron
quelle
1

Wir können RxJava (sehr leistungsfähige reaktive Erweiterungsbibliothek ) verwenden.

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

Der einzelne Operator löst eine Ausnahme aus, wenn kein Benutzer oder mehr als ein Benutzer gefunden wird.

frhack
quelle
Die richtige Antwort, die Initialisierung eines blockierenden Streams oder einer blockierenden Sammlung, ist jedoch wahrscheinlich nicht sehr billig (in Bezug auf Ressourcen).
Karl Richter
1

Da Collectors.toMap(keyMapper, valueMapper)eine Wurffusion verwendet wird, um mehrere Einträge mit demselben Schlüssel zu verarbeiten, ist es einfach:

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

Sie erhalten einen IllegalStateExceptionfür doppelte Schlüssel. Aber am Ende bin ich mir nicht sicher, ob der Code mit einem nicht noch besser lesbar wäre if.

Arne Burmeister
quelle
1
Feine Lösung! Und wenn Sie dies tun .collect(Collectors.toMap(user -> "", Function.identity())).get(""), haben Sie ein allgemeineres Verhalten.
glglgl
1

Ich benutze diese beiden Sammler:

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);
}
Xavier Dury
quelle
Ordentlich! onlyOne()wirft IllegalStateExceptionfür> 1 Elemente und NoSuchElementException` (in Optional::get) für 0 Elemente.
Simon04
@ simon04 Sie könnten die Methoden überladen, um eine Suppliervon zu nehmen (Runtime)Exception.
Xavier Dury
1

Wenn es Ihnen nichts ausmacht, eine Bibliothek eines Drittanbieters zu verwenden, haben beide SequenceMaus Cyclops-Streams (und LazyFutureStreamaus Simple- React-Operatoren) einen und einen einzigen Operator.

singleOptional()löst eine Ausnahme aus, wenn das Element mindestens 0mehrere 1Elemente enthält Stream, andernfalls wird der einzelne Wert zurückgegeben.

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()Gibt zurück, Optional.empty()wenn der Wert keine oder mehr als einen Wert enthält 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

Offenlegung - Ich bin der Autor beider Bibliotheken.

John McClean
quelle
0

Ich habe mich für den direkten Ansatz entschieden und das Ding einfach umgesetzt:

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


} 

mit dem JUnit-Test:

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

}

Diese Implementierung ist nicht threadsicher.

gerardw
quelle
0
User match = users.stream().filter((user) -> user.getId()== 1).findAny().orElseThrow(()-> new IllegalArgumentException());
Nitin
quelle
5
Während dieser Code die Frage lösen kann, einschließlich einer Erklärung, wie und warum dies das Problem löst, würde dies wirklich dazu beitragen, die Qualität Ihres Beitrags zu verbessern, und wahrscheinlich zu mehr Up-Votes führen. Denken Sie daran, dass Sie in Zukunft die Frage für die Leser beantworten, nicht nur für die Person, die jetzt fragt. Bitte bearbeiten Sie Ihre Antwort, um Erklärungen hinzuzufügen und anzugeben, welche Einschränkungen und Annahmen gelten.
David Buck
-2

Hast du das versucht?

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.

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

pardeep131085
quelle
3
Es wurde gesagt, dass count()die Verwendung nicht gut ist, da es sich um eine Terminaloperation handelt.
Ryvantage
Wenn dies wirklich ein Zitat ist, fügen Sie bitte Ihre Quellen hinzu
Neuron