Выбор элементов списка до тех пор, пока не будет выполнено условие Java 8 Lambdas

Я пытаюсь передумать, чтобы подумать об этом, и недавно столкнулся с ситуацией, в которой мне нужно было отобрать элементы из списка до тех пор, пока не будет выполнено условие, и я не мог найти простой естественный способ достижения этого. Очевидно, я все еще учусь.

Скажем, у меня есть этот список:

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

Заранее благодарю за ваши входы

Примечание: Прежде чем опубликовать это, я прочитал Ограничить поток предикатом, но я не мог понять, как я могу адаптировать этот ответ к моей проблеме. Любая помощь была бы оценена благодарностью.

Ответ 1

В одной опции используется коллекционер, требующий две функции, которые добавляют строки в списки, а другую, которая объединяет списки, которые потенциально создаются параллельно. Для каждого он добавляет строку или весь список только в том случае, если предыдущий частичный вывод не заканчивается элементом, который заканчивается с помощью 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);
});

Ответ 2

Если вы действительно должны использовать Streams API, сохраните его просто и используйте поток индексов:

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

Или сделайте новый List из подписок, если вы хотите создать независимую копию, не поддерживаемую исходным списком.

Ответ 3

В JDK9 появится новая операция Stream, называемая takeWhile, которая делает то же самое, что вам нужно. Я передал эту операцию в свою библиотеку StreamEx, поэтому вы можете использовать ее даже в Java-8:

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

К сожалению, он не принимает сам элемент "STOP", поэтому для его добавления требуется второй проход:

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

Обратите внимание, что как takeWhile, так и findFirst являются операциями короткого замыкания (они не будут обрабатывать весь входной поток, если это необязательно), поэтому вы можете использовать их с очень длинными или даже бесконечными потоками.

Однако используя StreamEx, вы можете решить его за один проход, используя трюк с groupRuns. Метод groupRuns группирует соседние элементы Stream в List на основе поставленного предиката, который указывает, следует ли сгруппировать два заданных смежных элемента или нет. Мы можем считать, что группа заканчивается элементом, содержащим "STOP". Тогда нам просто нужно взять первую группу:

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

Это решение также не будет выполнять дополнительную работу, когда первая группа будет завершена.

Ответ 4

Несмотря на то, что вышеприведенные ответы совершенно верны, они требуют собирать и/или предварительные выборки элементов перед их обработкой (оба могут быть проблемой, если поток очень долго).

Для моих потребностей я поэтому адаптировал ответ Луи к вопросу, указанному Джулианом, и адаптировал его для сохранения позиции остановки/останова. См. Параметр keepBreak::

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

Применение:

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

Отказ от ответственности. Я не уверен на 100%, что это будет работать параллельно (новый поток, конечно, не параллелен) или не последовательные потоки. Прокомментируйте/отредактируйте, если у вас есть какие-то намеки на это.

Ответ 5

Использование строго Java 8 API:

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

Затем вы можете специализировать его следующим образом:

Для потоков

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

Для коллекций

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