Filtruj strumień Java do 1 i tylko 1 elementu


229

Próbuję użyć Java 8 Streamdo znalezienia elementów w LinkedList. Chcę jednak zagwarantować, że istnieje jedno i tylko jedno dopasowanie do kryteriów filtru.

Weź ten kod:

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

Ten kod znajduje się Userna podstawie ich identyfikatora. Ale nie ma gwarancji, ile Users pasuje do filtra.

Zmiana linii filtra na:

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

Rzuci NoSuchElementException(dobrze!)

Chciałbym jednak, aby generowało błąd, jeśli występuje wiele dopasowań. Czy jest na to sposób?


count()jest operacją terminalową, więc nie możesz tego zrobić. Nie można później użyć strumienia.
Alexis C.

Ok, dzięki @ZouZou. Nie byłem do końca pewien, co zrobiła ta metoda. Dlaczego nie ma Stream::size?
ryvantage

7
@ryvantage Ponieważ strumienia można użyć tylko raz: obliczenie jego rozmiaru oznacza „iterację” nad nim, a potem nie można już dłużej używać strumienia.
assylias

3
Łał. Ten jeden komentarz pomógł mi zrozumieć Streamo wiele więcej niż wcześniej ...
ryvantage

2
To wtedy zdajesz sobie sprawę, że musiałeś użyć LinkedHashSet(zakładając, że chcesz zachować porządek wstawiania) lub HashSetcały czas. Jeśli twoja kolekcja służy tylko do znalezienia jednego identyfikatora użytkownika, to dlaczego zbierasz wszystkie pozostałe elementy? Jeśli istnieje potencjał, że zawsze będziesz musiał znaleźć identyfikator użytkownika, który również musi być unikalny, to po co używać listy, a nie zestawu? Programujesz wstecz. Użyj odpowiedniej kolekcji do pracy i oszczędzaj sobie bólu głowy
smac89

Odpowiedzi:


190

Utwórz niestandardowy 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);
            }
    );
}

Używamy Collectors.collectingAndThendo budowy naszych pożądanych Collectorprzez

  1. Zbieranie naszych obiektów w Listz Collectors.toList()kolektora.
  2. Zastosowanie dodatkowego finiszera na końcu, który zwraca pojedynczy element - lub rzuca IllegalStateExceptionif list.size != 1.

Użyty jako:

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

Następnie możesz dostosować to Collectortyle, ile chcesz, na przykład podać wyjątek jako argument w konstruktorze, dostosować go, aby pozwolić na dwie wartości i więcej.

Alternatywne - prawdopodobnie mniej eleganckie - rozwiązanie:

Możesz użyć „obejścia” obejmującego peek()i AtomicInteger, ale tak naprawdę nie powinieneś tego używać.

To, co możesz zrobić, to po prostu zebranie go w następujący sposób List:

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

23
Guava Iterables.getOnlyElementskróci te rozwiązania i zapewni lepsze komunikaty o błędach. Podobnie jak wskazówka dla czytelników, którzy już korzystają z Google Guava.
Tim Büthe

2
zapakowałem ten pomysł w klasę - gist.github.com/denov/a7eac36a3cda041f8afeabcef09d16fc
denov

1
@LonelyNeuron Proszę nie edytować mojego kodu. Stawia mnie to w sytuacji, w której muszę zweryfikować całą odpowiedź, którą napisałem cztery lata temu i po prostu nie mam teraz na to czasu.
skiwi

2
@skiwi: Edycja Lonely'a była pomocna i poprawna, więc przywróciłem ją po sprawdzeniu. Osoby odwiedzające tę odpowiedź dzisiaj nie dbają o to, jak do niej dotarłaś, nie muszą widzieć starej wersji i nowej wersji oraz sekcji Uaktualniona . To sprawia, że ​​twoja odpowiedź jest bardziej myląca i mniej pomocna. Znacznie lepiej jest umieścić posty w końcowym stanie , a jeśli ludzie chcą zobaczyć, jak to wszystko się potoczyło, mogą przeglądać historię postów.
Martijn Pieters

1
@skiwi: Kod w odpowiedzi jest absolutnie tym, co napisałeś. Wszystko, co zrobił redaktor, to oczyszczenie posta, usunięcie jedynie wcześniejszej wersji singletonCollector()definicji przestarzałej dla wersji, która pozostaje w poście, i zmiana jego nazwy na toSingleton(). Moja wiedza na temat strumieni Java jest nieco zardzewiała, ale zmiana nazwy wydaje mi się pomocna. Przejrzenie tej zmiany zajęło mi 2 minuty, szczyty. Jeśli nie masz czasu na przeglądanie zmian, czy mogę zasugerować, aby poprosić kogoś innego o zrobienie tego w przyszłości, być może na czacie Java ?
Martijn Pieters

118

Dla kompletności, oto „linijka” odpowiadająca doskonałej odpowiedzi @ prunge:

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

Uzyskuje to jedyny pasujący element ze strumienia, rzucając

  • NoSuchElementException w przypadku gdy strumień jest pusty, lub
  • IllegalStateException w przypadku gdy strumień zawiera więcej niż jeden pasujący element.

Odmiana tego podejścia pozwala uniknąć wcześniejszego wyjątku i zamiast tego reprezentuje wynik jako Optionalzawierający jedyny element lub nic (pusty), jeśli jest zero lub wiele elementów:

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

3
Podoba mi się wstępne podejście w tej odpowiedzi. Dla celów personalizacji można przekonwertować ostatni get()orElseThrow()
plik

1
Podoba mi się zwięzłość tego i unikanie tworzenia niepotrzebnej instancji List przy każdym wywołaniu.
LordOfThePigs

83

Inne odpowiedzi dotyczące pisania zwyczaju Collectorsą prawdopodobnie bardziej wydajne (takie jak odpowiedź Louisa Wassermana , +1), ale jeśli chcesz mieć zwięzłość, proponuję następujące:

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

Następnie sprawdź rozmiar listy wyników.

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

5
Jaki jest sens limit(2)tego rozwiązania? Jaką różnicę miałoby to, czy wynikowa lista to 2, czy 100? Jeśli jest większy niż 1.
ryvantage

18
Zatrzymuje się natychmiast, jeśli znajdzie drugie dopasowanie. To właśnie robią wszyscy wymyślni kolekcjonerzy, używając tylko więcej kodu. :-)
Stuart Marks

10
Co powiesz na dodanieCollectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
Lukas Eder

1
Javadoc mówi param This limitu za: maxSize: the number of elements the stream should be limited to. Więc nie powinno być .limit(1)zamiast .limit(2)?
alexbt,

5
@alexbt Stwierdzenie problemu polega na upewnieniu się, że istnieje dokładnie jeden (nie więcej, nie mniej) pasujący element. Po moim kodzie można sprawdzić, result.size()czy jest równy 1. Jeśli jest to 2, oznacza to, że występuje więcej niż jedno dopasowanie, więc jest to błąd. Jeśli kod tak zrobił limit(1), więcej niż jedno dopasowanie spowodowałoby pojedynczy element, którego nie można odróżnić od dokładnie jednego dopasowania. Pominąłoby to przypadek błędu, który dotyczył PO.
Stuart Marks,

67

Guawa zapewnia MoreCollectors.onlyElement(), że robi to dobrze. Ale jeśli musisz to zrobić sam, możesz rzucić na to własne 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);
}

... lub używając własnego Holdertypu zamiast AtomicReference. Możesz użyć tego Collectortyle, ile chcesz.


SingletonCollector @ skiwi był mniejszy i łatwiejszy do naśladowania niż to, dlatego dałem mu czek. Ale dobrze jest zobaczyć konsensus w odpowiedzi: zwyczaj Collectorbył drogą.
ryvantage

1
Słusznie. Dążyłem przede wszystkim do szybkości, a nie zwięzłości.
Louis Wasserman

1
Tak? Dlaczego twój jest szybszy?
ryvantage

3
Głównie dlatego, że przydzielenie all-up Listjest droższe niż pojedyncza zmienna odniesienia.
Louis Wasserman

1
@LouisWasserman, ostatnie zdanie na temat aktualizacji MoreCollectors.onlyElement()powinno faktycznie być pierwsze (i być może jedyne :))
Piotr Findeisen,

46

Użyj Guava's MoreCollectors.onlyElement()( JavaDoc ).

Robi to, co chcesz i rzuca, IllegalArgumentExceptionjeśli strumień składa się z dwóch lub więcej elementów, a NoSuchElementExceptionjeśli strumień jest pusty.

Stosowanie:

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

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

2
Uwaga dla innych użytkowników: MoreCollectorsjest częścią jeszcze nieopublikowanej (od 2016-12 r.) Niepublikowanej wersji 21.
qerub

2
Ta odpowiedź powinna być wyższa.
Emdadul Sawon

31

Operacja „włazu ewakuacyjnego”, która pozwala robić dziwne rzeczy, które nie są obsługiwane przez strumienie, polega na proszeniu o 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();
}

Guawa ma wygodną metodę, aby wziąć Iteratori zdobyć jedyny element, rzucając, jeśli jest zero lub wiele elementów, które mogłyby zastąpić tutaj dolne linie n-1.


4
Metoda Guava: Iterators.getOnlyElement (iterator Iterator <T>).
przed

23

Aktualizacja

Niezła sugestia w komentarzu @Holger:

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

Oryginalna odpowiedź

Zgłaszany jest wyjątek Optional#get, ale jeśli masz więcej niż jeden element, to nie pomoże. Możesz zebrać użytkowników w kolekcji, która akceptuje tylko jeden element, na przykład:

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

co rzuca java.lang.IllegalStateException: Queue full, ale wydaje się zbyt hacking.

Lub możesz użyć redukcji w połączeniu z opcjonalnym:

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

Zmniejszenie zasadniczo zwraca:

  • zero, jeśli nie znaleziono użytkownika
  • użytkownik, jeśli tylko jeden zostanie znaleziony
  • zgłasza wyjątek, jeśli znaleziono więcej niż jeden

Wynik jest następnie pakowany w opcjonalny.

Ale najprostszym rozwiązaniem byłoby po prostu zebranie do kolekcji, sprawdzenie, czy jej rozmiar to 1 i uzyskanie jedynego elementu.


1
Dodałbym element tożsamości ( null), aby zapobiec użyciu get(). Niestety twój reducenie działa tak, jak myślisz, weź pod uwagę, Streamże ma nullw nim elementy, może uważasz, że to zakryłeś, ale ja mogę [User#1, null, User#2, null, User#3], teraz nie rzucę wyjątku, chyba że się tutaj mylę.
skiwi

2
@ Skiki, jeśli istnieją elementy zerowe, filtr najpierw wyrzuci NPE.
assylias

2
Ponieważ wiesz, że strumień nie może przejść nulldo funkcji redukcji, usuwając wartość tożsamość argumentu uczyniłoby całą zajmujących się nullw funkcji przestarzałego: reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )spełnia swoje zadanie, a nawet lepiej, to już zwraca Optional, eliding konieczność za telefon Optional.ofNullablena wynik.
Holger,

15

Alternatywą jest użycie redukcji: (w tym przykładzie użyto ciągów znaków, ale można je łatwo zastosować do dowolnego typu obiektu, w tym 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);};
}

Więc w przypadku Userciebie miałbyś:

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

8

Korzystanie z redukcji

To jest prostszy i elastyczny sposób, jaki znalazłem (na podstawie odpowiedzi @prunge)

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

W ten sposób otrzymujesz:

  • Opcjonalny - jak zawsze z twoim przedmiotem lub Optional.empty()jeśli nie jest obecny
  • wyjątek (w końcu TWÓJ niestandardowy typ / komunikat), jeśli istnieje więcej niż jeden element

6

Myślę, że ten sposób jest prostszy:

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


5

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

Stosowanie:

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

Zwracamy Optional, ponieważ zwykle nie możemy założyć, że Collectionzawiera dokładnie jeden element. Jeśli już wiesz, że tak jest, zadzwoń:

User user = result.orElseThrow();

Obciąża to osobę wywołującą błąd - tak jak powinno.



1

Możemy użyć RxJava (bardzo rozbudowanej biblioteki rozszerzeń reaktywnych )

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

Pojedynczy operator zgłasza wyjątek, jeżeli nie użytkownika lub więcej niż jeden użytkownik znajduje.


Prawidłowa odpowiedź, inicjowanie blokującego strumienia lub kolekcji prawdopodobnie nie jest zbyt tanie (pod względem zasobów).
Karl Richter,

1

Ponieważ Collectors.toMap(keyMapper, valueMapper)wykorzystuje rzutowanie fuzji do obsługi wielu wpisów za pomocą tego samego klucza, jest to łatwe:

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

Dostaniesz IllegalStateExceptionza duplikaty kluczy. Ale na koniec nie jestem pewien, czy kod nie byłby jeszcze bardziej czytelny przy użyciu if.


1
Świetne rozwiązanie! A jeśli tak .collect(Collectors.toMap(user -> "", Function.identity())).get(""), masz bardziej ogólne zachowanie.
glglgl

1

Używam tych dwóch kolektorów:

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

Schludny! onlyOne()zgłasza IllegalStateExceptiondla> 1 elementów, a NoSuchElementException` (in Optional::get) dla 0 elementów.
simon04

@ simon04 Można przeciążać metody wziąć Supplierz (Runtime)Exception.
Xavier Dury

1

Jeśli nie masz nic przeciwko korzystaniu z biblioteki innej firmy, SequenceMz cyklop-strumieni (i LazyFutureStreamod prostej reakcji ) oba mają operatory pojedyncze i pojedynczeOpcjonalne.

singleOptional()zgłasza wyjątek, jeśli są w nim elementy 0lub więcej 1, w Streamprzeciwnym razie zwraca pojedynczą wartość.

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()zwraca, Optional.empty()jeśli nie ma wartości lub więcej niż jedną wartość w 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

Ujawnienie - jestem autorem obu bibliotek.


0

Poszedłem z podejściem bezpośrednim i właśnie wdrożyłem rzecz:

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


} 

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

}

Ta implementacja nie jest wątkowo bezpieczna.


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

5
Chociaż ten kod może rozwiązać pytanie, w tym wyjaśnienie, w jaki sposób i dlaczego to rozwiązuje problem, naprawdę pomógłby poprawić jakość twojego postu i prawdopodobnie doprowadziłby do większej liczby głosów. Pamiętaj, że odpowiadasz na pytanie czytelników w przyszłości, a nie tylko osoby zadającej teraz pytanie. Edytuj swoją odpowiedź, aby dodać wyjaśnienia i wskazać, jakie ograniczenia i założenia mają zastosowanie.
David Buck

-2

Próbowałeś tego?

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.

Źródło: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html


3
Mówiono, że count()nie jest dobry w użyciu, ponieważ jest to operacja terminalowa.
ryvantage

Jeśli to naprawdę cytat, dodaj swoje źródła
Neuron
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.