it-swarm.com.de

Filtern Sie Java Stream nach 1 und nur 1 Element

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

Nimm 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 User basierend auf ihrer ID. Es gibt jedoch keine Garantie dafür, wie viele Users mit dem Filter übereinstimmen.

Ändern der Filterlinie in:

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

Werfen wir eine NoSuchElementException (gut!)

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

164
ryvantage

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 unser gewünschtes Collector durch zu konstruieren 

  1. Sammeln unserer Objekte in einem List mit dem Collectors.toList()-Collector.
  2. Wenn Sie am Ende einen zusätzlichen Finisher anwenden, wird das einzelne Element zurückgegeben - oder IllegalStateException, falls list.size != 1.

Benutzt als:

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

Sie können dieses Collector beliebig anpassen. Geben Sie beispielsweise die Ausnahme im Konstruktor als Argument an.

Eine Alternative - wohl weniger elegant - Lösung:

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

Was Sie tun können, ist es, es einfach in einer List zu sammeln:

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

Der Vollständigkeit halber sei hier der „One-Liner“ genannt, 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 zusammenpassende 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 Variante dieses Ansatzes vermeidet das frühzeitige Auslösen einer Ausnahme und stellt stattdessen das Ergebnis als Optional dar, die entweder das einzige Element enthält oder nichts (leer), wenn es null oder mehrere Elemente gibt:

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

Die anderen Antworten, bei denen eine benutzerdefinierte Collector geschrieben wird, sind wahrscheinlich effizienter (wie Louis Wassermans , +1). Wenn Sie jedoch 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.

77
Stuart Marks

Guave liefert MoreCollectors.onlyElement() die hier das Richtige tut. Aber wenn Sie es selbst tun müssen, können Sie Ihre eigene _Collector für diesen Fall 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 Ihren eigenen Holder-Typ anstelle von AtomicReference. Sie können Collector beliebig oft wiederverwenden.

46
Louis Wasserman

Verwenden Sie Guavas MoreCollectors.onlyElement() ( JavaDoc ).

Es macht, was Sie wollen, und wirft einen IllegalArgumentException, wenn der Stream aus zwei oder mehr Elementen besteht, und einen NoSuchElementException, wenn der Stream leer ist.

Verwendungszweck:

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

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

Die "Escape-Schraffur" -Operation, mit der Sie komische Dinge ausführen können, die sonst nicht von Streams unterstützt werden, besteht darin, nach einer Iterator zu fragen:

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 hat eine bequeme Methode, um eine Iterator zu nehmen und das einzige Element abzurufen, das bei null oder mehreren Elementen geworfen wird, die hier die unteren n-1-Zeilen ersetzen könnten.

28
Brian Goetz

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 Optional#get ausgelöst, 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. Beispiel:

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

was einen Java.lang.IllegalStateException: Queue full wirft, aber das fühlt sich zu hackig 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 Reduktion 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 optional verpackt.

Die einfachste Lösung wäre jedoch wahrscheinlich, eine Sammlung zu sammeln, die Größe 1 zu prüfen und das einzige Element zu erhalten.

19
assylias

Eine Alternative ist die Verwendung der Reduktion: (In diesem Beispiel werden Strings verwendet, die jedoch leicht auf jeden Objekttyp angewendet werden können, 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);};
}

Für den Fall mit User hätten Sie also:

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

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

Verwendungszweck:

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

Wir liefern eine Optional , da wir normalerweise nicht davon ausgehen können, dass die Collection genau ein Element enthält. Wenn Sie bereits wissen, dass dies der Fall ist, rufen Sie an:

User user = result.orElseThrow();

Dies hat die Aufgabe, den Fehler zu bearbeiten - wie es sein sollte.

5
Lonely Neuron

Guava hat eine Collector für diesen Namen MoreCollectors.onlyElement() .

4
Hans

Wenn es Ihnen nichts ausmacht, eine Drittanbieter-Bibliothek zu verwenden, haben SequenceM from cyclops-streams (und LazyFutureStream from simple-reag ) beide einen single & singleOptional-Operator. 

singleOptional() löst eine Exception aus, wenn 0 oder mehr als 1 Elemente in der Stream vorhanden sind, andernfalls wird der Einzelwert 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 Optional.empty() zurück, wenn die Stream keine oder mehr als einen Wert enthält.

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.

1
John McClean

Da Collectors.toMap(keyMapper, valueMapper) eine zusammenfassende Fusion verwendet, 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 eine IllegalStateException für doppelte Schlüssel. Aber am Ende bin ich nicht sicher, ob der Code mit einer if noch besser lesbar wäre.

1
Arne Burmeister

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);
}
1
Xavier Dury

Wir können RxJava (sehr leistungsfähige reaktive Erweiterung library) 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 singleOperator löst eine Ausnahme aus, wenn kein oder mehrere Benutzer gefunden werden. 

1
frhack

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:

  • optional - wie immer bei Ihrem Objekt oder Optional.empty(), falls nicht vorhanden
  • die Ausnahme (mit eventuell IHR benutzerdefiniertem Typ/Nachricht), wenn es mehr als ein Element gibt 
0
Fabio Bonfante

Ich bin mit dem Direktansatz gegangen und habe einfach die Sache 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 nicht threadsafe.

0
gerardw