Как проверить, пуст ли поток Java 8?
Как я могу проверить, если Stream
пусто и выдает исключение, если это не так, как нетерминальная операция?
По сути, я ищу что-то эквивалентное приведенному ниже коду, но без материализации промежуточного потока. В частности, проверка не должна происходить до того, как поток фактически будет использован терминальной операцией.
public Stream<Thing> getFilteredThings() {
Stream<Thing> stream = getThings().stream()
.filter(Thing::isFoo)
.filter(Thing::isBar);
return nonEmptyStream(stream, () -> {
throw new RuntimeException("No foo bar things available")
});
}
private static <T> Stream<T> nonEmptyStream(Stream<T> stream, Supplier<T> defaultValue) {
List<T> list = stream.collect(Collectors.toList());
if (list.isEmpty()) list.add(defaultValue.get());
return list.stream();
}
8 ответов
Если вы можете жить с ограниченными параллельными возможностями, будет работать следующее решение:
private static <T> Stream<T> nonEmptyStream(
Stream<T> stream, Supplier<RuntimeException> e) {
Spliterator<T> it=stream.spliterator();
return StreamSupport.stream(new Spliterator<T>() {
boolean seen;
public boolean tryAdvance(Consumer<? super T> action) {
boolean r=it.tryAdvance(action);
if(!seen && !r) throw e.get();
seen=true;
return r;
}
public Spliterator<T> trySplit() { return null; }
public long estimateSize() { return it.estimateSize(); }
public int characteristics() { return it.characteristics(); }
}, false);
}
Вот пример кода, использующего его:
List<String> l=Arrays.asList("hello", "world");
nonEmptyStream(l.stream(), ()->new RuntimeException("No strings available"))
.forEach(System.out::println);
nonEmptyStream(l.stream().filter(s->s.startsWith("x")),
()->new RuntimeException("No strings available"))
.forEach(System.out::println);
Проблема с (эффективным) параллельным выполнением состоит в том, что поддержка разделения Spliterator
требует поточно-ориентированного способа, чтобы заметить, видел ли какой-либо из фрагментов какое-либо значение в поточно-ориентированном виде. Тогда последний из фрагментов исполняется tryAdvance
должен понять, что это последний (и он также не мог продвинуться), чтобы бросить соответствующее исключение. Поэтому я не добавил поддержку разделения здесь.
Во многих случаях этого может быть достаточно
stream.findAny().isPresent()
Другие ответы и комментарии верны в том, что для проверки содержимого потока необходимо добавить терминальную операцию, тем самым "потребляя" поток. Тем не менее, можно сделать это и превратить результат обратно в поток, не буферизуя все содержимое потока. Вот пара примеров:
static <T> Stream<T> throwIfEmpty(Stream<T> stream) {
Iterator<T> iterator = stream.iterator();
if (iterator.hasNext()) {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
} else {
throw new NoSuchElementException("empty stream");
}
}
static <T> Stream<T> defaultIfEmpty(Stream<T> stream, Supplier<T> supplier) {
Iterator<T> iterator = stream.iterator();
if (iterator.hasNext()) {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
} else {
return Stream.of(supplier.get());
}
}
В основном превратить поток в Iterator
для того, чтобы позвонить hasNext()
на нем, и если это правда, включите Iterator
обратно в Stream
, Это неэффективно, поскольку все последующие операции в потоке будут проходить через итератор. hasNext()
а также next()
методы, что также подразумевает, что поток эффективно обрабатывается последовательно (даже если позже он станет параллельным). Однако это позволяет вам протестировать поток без буферизации всех его элементов.
Вероятно, есть способ сделать это, используя Spliterator
вместо Iterator
, Это потенциально позволяет возвращаемому потоку иметь те же характеристики, что и входной поток, в том числе работать параллельно.
Вы должны выполнить терминальную операцию в потоке, чтобы применить любой из фильтров. Поэтому вы не можете знать, будет ли он пустым, пока вы его не потребите.
Лучшее, что вы можете сделать, это прекратить поток с findAny()
Терминальная операция, которая остановится, когда он найдет какой-либо элемент, но если его нет, ему придется перебрать весь список ввода, чтобы выяснить это.
Это поможет вам только в том случае, если во входном списке много элементов, и один из первых пропустит фильтры, поскольку потребуется только небольшое подмножество списка, прежде чем вы узнаете, что поток не пуст.
Конечно, вам все равно придется создать новый поток, чтобы создать список вывода.
Я думаю, этого должно быть достаточно, чтобы сопоставить логическое значение
В коде это:
boolean isEmpty = anyCollection.stream()
.filter(p -> someFilter(p)) // Add my filter
.map(p -> Boolean.TRUE) // For each element after filter, map to a TRUE
.findAny() // Get any TRUE
.orElse(Boolean.FALSE); // If there is no match return false
Следуя идее Стюарта, это можно сделать с Spliterator
как это:
static <T> Stream<T> defaultIfEmpty(Stream<T> stream, Stream<T> defaultStream) {
final Spliterator<T> spliterator = stream.spliterator();
final AtomicReference<T> reference = new AtomicReference<>();
if (spliterator.tryAdvance(reference::set)) {
return Stream.concat(Stream.of(reference.get()), StreamSupport.stream(spliterator, stream.isParallel()));
} else {
return defaultStream;
}
}
Я думаю, что это работает с параллельными потоками как stream.spliterator()
операция прервет поток, а затем перестроит его при необходимости
В моем случае использования мне нужно было по умолчанию Stream
а не значение по умолчанию. это довольно легко изменить, если это не то, что вам нужно
Лучшее простое решение, которое я смог найти, которое не потребляет поток и не преобразует его в итераторы:
public Stream<Thing> getFilteredThings() {
AtomicBoolean found = new AtomicBoolean(false);
Stream<Thing> stream = getThings().stream()
.filter(Thing::isFoo)
.filter(Thing::isBar)
.forEach(x -> {
found.set(true);
// do useful things
})
;
if (!found.get()) {
throw new RuntimeException("No foo bar things available");
}
}
Не стесняйтесь предлагать улучшения ..