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 User
s 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?
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
List
mit dem Collectors.toList()
-Collector.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.
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);
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, oderIllegalStateException
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));
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.
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.
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.
import static com.google.common.collect.MoreCollectors.onlyElement;
User match =
users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());
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.
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") });
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:
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.
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();
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()
);
}
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.
Guava hat eine Collector
für diesen Namen MoreCollectors.onlyElement()
.
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.
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.
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);
}
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.
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.empty()
, falls nicht vorhandenIch 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.