it-swarm.com.ru

Выбор элементов списка до тех пор, пока условие не будет выполнено с помощью 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;
    }
}

Заранее благодарю за ваш вклад

ПРИМЕЧАНИЕ: Перед публикацией этого я прочитал Ограничить поток предикатом но я не мог понять, как я могу адаптировать этот ответ к моей проблеме. Любая помощь будет оценена спасибо.

15
Julian

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

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

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

13
Tagir Valeev

Если вам действительно нужно использовать 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 из подсписка, если вам нужна независимая копия, которая не поддерживается исходным списком.

9
Misha

Использование строго только 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);
}
1
smac89

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

Для моих потребностей , поэтому я адаптировал ответ Луи к вопросу, указанному Джулианом, и адаптировал его, чтобы сохранить элемент остановки/прерывания. Смотрите параметр 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);
    }

Disclaimer: я не уверен на 100%, что это будет работать на параллельных (новый поток, конечно, не параллельный) или непоследовательных потоках. Пожалуйста, комментируйте/редактируйте, если у вас есть намеки на это.

1
Benoît