Pytanie Wybranie elementów listy, dopóki warunek nie zostanie spełniony w Javie 8 Lambdas


Próbuję przestawić się na myślenie o funkcjonalności i ostatnio stanąłem przed sytuacją, w której potrzebowałem zbierać elementy z listy, dopóki warunek nie zostanie spełniony i nie mogłem znaleźć łatwego, naturalnego sposobu osiągnięcia tego. Oczywiście nadal się uczę.

Powiedz, że mam tę listę:

List<String> tokens = Arrays.asList("pick me", "Pick me", "pick Me",
    "PICK ME", "pick me and STOP", "pick me", "pick me and Stop", "pick me");

// In a non lambdas was you would do it like below
List<String> myTokens = new ArrayList<>();
for (String token : tokens) {
    myTokens.add(token);
    if (token.toUpperCase().endsWith("STOP")) {
        break;
    }
}

Z góry dziękuję za dane wejściowe

UWAGA: Przed opublikowaniem tego przeczytałem Ogranicz strumień przez predykat ale nie widziałem, jak mogę dostosować tę odpowiedź do mojego problemu. Każda pomoc zostanie doceniona dzięki.


14
2017-08-29 20:15


pochodzenie


Przeczytałem to pytanie, zanim opublikowałem moje, ale pomyślałem i nadal myślę, że java miałby trochę z tego powodu. Powiedz coś takiego myTokens = tokens.stream().collect(toListWhile(...))Lub miły i łatwy sposób na wdrożenie toListWhile() niestandardowy kolektor - Julian
Nie sądzę, aby w strumieniu API istniała pojedyncza operacja, która zakładałaby strumień uporządkowany, a niepowodzenie dla nieuporządkowanego strumienia. Taka jest twoja wyobrażona operacja. - Marko Topolnik
@AnidMonsur, To nie jest dokładny duplikat, ponieważ OP chce również przyjąć element STOP. To trochę inny problem. - Tagir Valeev
@MarkoTopolnik, takeWhile jest w rzeczywistości czymś takim. The Instrukcja JavaDoc dla nieuporządkowanych strumieni jest szczególnie wspaniały! Dowolny podzbiór, sic! Moim zdaniem byłoby lepiej, gdyby rzucił wyjątek ... - Tagir Valeev
@tagir Ah, więc dodają go ponownie - wiele, jeśli były planowane przed wydaniem wersji Java 8, ale potem zostały usunięte. Java zazwyczaj ma szybką semantykę w przeciwieństwie do GIGO. To z pewnością wygeneruje wiele zdezorientowanych pytań SO. - Marko Topolnik


Odpowiedzi:


Jedna opcja używa kolektora, który wymaga dwóch funkcji, które dodaje ciągi do list, a drugi łączy listy wcześniej potencjalnie utworzone równolegle. Dla każdego dodaje ciąg lub całą listę tylko wtedy, gdy poprzednie częściowe wyjście nie kończy się na elemencie, który kończy się na STOP:

tokens.stream().collect(() -> new ArrayList<String>(), (l, e) -> {
    if(l.isEmpty() || !l.get(l.size()-1).toUpperCase().endsWith("STOP"))
        l.add(e);
}, (l1, l2) -> {
    if(l1.isEmpty() || !l1.get(l1.size()-1).toUpperCase().endsWith("STOP"))
        l1.addAll(l2);
});

1
2017-08-29 22:02



Zauważ, że to rozwiązanie nie jest zwarciem: nadal będziesz musiał powtórzyć cały strumień wejściowy, ignorując wszystko po "STOP". Może to być problematyczne, jeśli masz bardzo długi (lub nieograniczony) strumień. - Tagir Valeev
Właśnie zdałem sobie sprawę, że to rozwiązanie byłoby bardzo powolne nawet dla niezbyt długich danych wejściowych, ponieważ kopiuje całą listę, gdy każdy nowy element jest przetwarzany. Jeśli podoba Ci się rozwiązanie nie powodujące zwarć, lepiej jest zaimplementować Collector zamiast. - Tagir Valeev
Zdecydowanie nie zadowalam się tutaj rozwiązaniem O (n2). - Marko Topolnik
@WillShackleford, kolekcjoner wykonuje zmienny redukcja, dzięki czemu może modyfikować istniejący akumulator. Nie musisz niczego kopiować, po prostu sprawdź aktualny akumulator i dodaj nowy element, jeśli to konieczne. Rozwiązanie z linkami do listy w aktualizacji jest z pewnością przesadzone. - Tagir Valeev
@ WillShackleford, oto jest rozwiązanie oparte na zbiorze. Ja też tego nie lubię, bo nie ma zwarcia, ale jest co najmniej krótsza niż twoja i O (n). Użyj go do odpowiedzi, jeśli chcesz :-) - Tagir Valeev


W JDK9 pojawi się nowy Stream operacja o nazwie takeWhile co jest podobne do tego, czego potrzebujesz. Przesyłałem tę operację do mojej StreamEx biblioteka, dzięki czemu możesz jej używać nawet w Javie-8:

List<String> list = StreamEx.of(tokens)
                            .takeWhile(t -> !t.toUpperCase().endsWith("STOP"))
                            .toList();

Niestety nie bierze "STOP" sam element, więc drugie przejście jest konieczne, aby dodać je ręcznie:

list.add(StreamEx.of(tokens).findFirst(t -> t.toUpperCase().endsWith("STOP")).get());

Zauważ, że oba takeWhile i findFirst są operacjami zwarcia (nie będą przetwarzać całego strumienia wejściowego, jeśli nie są potrzebne), dzięki czemu można ich używać z bardzo długimi lub nawet nieskończonymi strumieniami.

Jednak za pomocą StreamEx można rozwiązać go w trybie pojedynczego przejścia, wykorzystując sztuczkę groupRuns. The groupRuns grupy metod sąsiadujące z elementami Stream do List na podstawie dostarczonego predykatu, który określa, czy dwa sąsiednie elementy powinny zostać zgrupowane, czy nie. Możemy uznać, że grupa kończy się elementem zawierającym "STOP". Następnie musimy wziąć pierwszą grupę:

List<String> list = StreamEx.of(tokens)
                            .groupRuns((a, b) -> !a.toUpperCase().endsWith("STOP"))
                            .findFirst().get();

To rozwiązanie również nie będzie wykonywać dodatkowej pracy po ukończeniu pierwszej grupy.


12
2017-08-30 02:40



@Julian, Stream API został opracowany do obsługi zarówno przetwarzania sekwencyjnego i równoległego, ale takeWhile operacja jest sekwencyjna z natury. Nie jest łatwo zrównoleglić go poprawnie. Prawdopodobnie był to powód, dla którego nie był zawarty w JDK 8. - Tagir Valeev
@TagirValeev Możliwe jest równoległe, ale potencjalnie wiąże się to z dużym buforowaniem. Zasadniczo zabrakło czasu, aby zrobić to w Javie 8. - Stuart Marks
@StuartMarks, tak, studiowałem wdrożenie. Niemniej jednak w przypadku uporządkowanego strumieniowania równoległość rzadko poprawiłaby wydajność (z wyjątkiem przypadków, gdy warunek nie powiódł się w pobliżu końca strumienia lub zawsze jest prawdziwy). W przypadku nieuporządkowanego strumienia równoległego operacja ta jest mało prawdopodobna. Nie mogę sobie wyobrazić żadnego scenariusza, w którym "dowolny podzbiór" byłby satysfakcjonujący. W końcu możesz użyć filter która również zwróci pewien podzbiór. Moja implementacja backportu jest całkiem kiepska AbstractSpliterator podklasa z domyślną trySplit: Zdecydowałem, że specjalna obsługa równoległa to strata czasu. - Tagir Valeev
@TagirValeev Unordered takeWhile jest rzeczywiście bezużyteczny w przypadku konwencjonalnych problemów związanych z gromadzeniem danych, które widzimy bardzo często. Okazuje się przydatne, jeśli predykat jest oparty na stanie zewnętrznym, np. przetwarzanie strumienia zdarzeń przez ustalony czas. - Stuart Marks
@StuartMarks, dobry punkt! Nigdy nie myślałem o tym, żeby używać Stream API w ten sposób. Prawdopodobnie dobrze byłoby dodać taki przykład takeWhile JavaDoc ... - Tagir Valeev


Jeśli naprawdę musisz korzystać z API Streams, zachowaj prostotę i używaj strumienia indeksów:

int lastIdx = IntStream.range(0, tokens.size())
        .filter(i -> tokens.get(i).toUpperCase().endsWith("STOP"))
        .findFirst()
        .orElse(-1);

List<String> myTokens = tokens.subList(0, lastIdx + 1);

Lub zrób nowy List poza podlistą, jeśli chcesz mieć niezależną kopię, która nie jest poparta oryginalną listą.


9
2017-08-30 00:11



Zamiast tego bardzo czyste i przyjemne rozwiązanie - Julian


Mimo że powyższe odpowiedzi są całkowicie prawidłowe, wymagają zebrać i / lub wstępne pobranie elementy przed ich przetworzeniem (oba mogą stanowić problem, jeśli Strumień jest bardzo długi).

Dla mnie wymagania, Dlatego dostosowałem się Odpowiedź Louisa na pytanie wskazane przez Juliana i dostosował go do zachowania pozycji stop / break. Zobacz keepBreak parametr ::

public static <T> Spliterator<T> takeWhile(final Spliterator<T> splitr, final Predicate<? super T> predicate, final boolean keepBreak) {
    return new Spliterators.AbstractSpliterator<T>(splitr.estimateSize(), 0) {
        boolean stillGoing = true;

        @Override
        public boolean tryAdvance(final Consumer<? super T> consumer) {
            if (stillGoing) {
                final boolean hadNext = splitr.tryAdvance(elem -> {
                    if (predicate.test(elem)) {
                        consumer.accept(elem);
                    } else {
                        if (keepBreak) {
                            consumer.accept(elem);
                        }
                        stillGoing = false;
                    }
                });
                return hadNext && (stillGoing || keepBreak);
            }
            return false;
        }
    };
}

public static <T> Stream<T> takeWhile(final Stream<T> stream, final Predicate<? super T> predicate, final boolean keepBreak) {
    return StreamSupport.stream(takeWhile(stream.spliterator(), predicate, keepBreak), false);
}

Stosowanie:

public List<String> values = Arrays.asList("some", "words", "before", "BREAK", "AFTER");

    @Test
    public void testStopAfter() {
        Stream<String> stream = values.stream();
        //how to filter stream to stop at the first BREAK
        stream = stream.filter(makeUntil(s -> "BREAK".equals(s)));
        final List<String> actual = stream.collect(Collectors.toList());

        final List<String> expected = Arrays.asList("some", "words", "before", "BREAK");
        assertEquals(expected, actual);
    }

Zrzeczenie się: Nie jestem w 100% pewien, czy to działa równolegle (nowy strumień z pewnością nie jest równoległy) czy niesekwencyjne. Prosimy o komentarz / edycję, jeśli masz jakieś wskazówki na ten temat.


1
2017-10-06 08:04



Pytanie jest pytaniem takeUntil nie takeWhile. Są kompletnymi przeciwieństwami - smac89


Używanie wyłącznie API Java 8:

public static <R> Stream<? extends R> takeUntil(Iterator<R> iterator, Predicate<? super R> stopFilter) {

    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new Iterator<R>() {
        private R next = null;
        private boolean hasTaken = true;
        private boolean stopIteration = !iterator.hasNext();
        @Override
        public boolean hasNext() {
            if (stopIteration) {
                return false;
            }

            if (!hasTaken) {
                return true;
            }

            if (!iterator.hasNext()) {
                stopIteration = true;
                return false;
            }

            next = iterator.next();
            stopIteration = stopFilter.test(next);
            hasTaken = stopIteration;
            return !stopIteration;
        }

        @Override
        public R next() {
            if (!hasNext()) {
                throw new NoSuchElementException("There are no more items to consume");
            }
            hasTaken = true;
            return next;
        }
    }, 0), false);
}

Następnie możesz specjalizować się w następujący sposób:

Dla strumieni

public static <R> Stream<? extends R> takeUntil(Stream<R> stream, Predicate<? super R> stopFilter) {
    return takeUntil(stream.iterator(), stopFilter);
}

Do kolekcji

public static <R> Stream<? extends R> takeUntil(Collection<R> col, Predicate<? super R> stopFilter) {
    return takeUntil(col.iterator(), stopFilter);
}

0
2017-11-21 03:25