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