it-roy-ru.com

Как проверить, что поток 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();
}
79
Cephalopod

Если вы можете жить с ограниченными параллельными возможностями, будет работать следующее решение:

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, должен понять, что он последний (и он также не может продвинуться), чтобы выдать соответствующее исключение. Так что я не добавил поддержку разделения здесь.

18
Holger

Другие ответы и комментарии верны в том, что для проверки содержимого потока необходимо добавить терминальную операцию, тем самым "потребляя" поток. Тем не менее, можно сделать это и превратить результат обратно в поток, без буферизации всего содержимого потока. Вот пара примеров:

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() для него, и если true, превратить Iterator обратно в Stream. Это неэффективно в том смысле, что все последующие операции в потоке будут проходить через методы hasNext() и next() итератора, что также означает, что поток эффективно обрабатывается последовательно (даже если впоследствии он станет параллельным). Однако это позволяет вам тестировать поток без буферизации всех его элементов.

Вероятно, есть способ сделать это, используя Spliterator вместо Iterator. Это потенциально позволяет возвращаемому потоку иметь те же характеристики, что и входной поток, в том числе работать параллельно.

30
Stuart Marks

Вы должны выполнить терминальную операцию в потоке, чтобы применить любой из фильтров. Поэтому вы не можете знать, будет ли он пустым, пока вы его не потребите.

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

Это поможет вам только в том случае, если во входном списке много элементов, и один из первых пропустит фильтры, поскольку потребуется только небольшое подмножество списка, прежде чем вы узнаете, что поток не пуст.

Конечно, вам все равно придется создать новый поток, чтобы создать список вывода.

11
Eran

Следуя идее Стюарта, это можно сделать с помощью 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 по умолчанию, а не значение по умолчанию. это довольно легко изменить, если это не то, что вам нужно

3
phoenix7360

Я думаю, должно быть достаточно, чтобы отобразить логическое

В коде это:

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
0
Luis Roberto