Как проверить, пуст ли Java 8 Stream?

100

Как я могу проверить, является ли a 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();
}
Головоногие моллюски
источник
23
Вы не можете съесть свой торт и съесть его - и в этом контексте это буквально так. Вы должны использовать поток, чтобы узнать, пуст ли он. В этом суть семантики Stream (лень).
Марко Топольник
В конце концов, он будет израсходован, на этом этапе должна произойти проверка
Cephalopod
12
Чтобы проверить, что поток не пуст, вы должны попытаться использовать хотя бы один элемент. В этот момент поток потерял свою «девственность» и не может быть снова использован с самого начала.
Марко Топольник

Ответы:

24

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

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

Хольгер
источник
33

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

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

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

Стюарт Маркс
источник
1
Я не думаю , что есть ремонтопригодно решение , которое будет поддерживать эффективную обработку параллельно , как это трудно поддерживать разделение, однако имеющий estimatedSizeи characteristicsмогу даже улучшить однопоточную производительность. Просто так получилось, что я написал Spliteratorрешение, пока вы публиковали Iteratorрешение…
Хольгер
3
Вы можете запросить у потока Spliterator, вызвать tryAdvance (lambda), где ваша лямбда захватит все, что ему передано, а затем вернуть Spliterator, который делегирует почти все базовому Spliterator, за исключением того, что он приклеивает первый элемент обратно к первому фрагменту ( и исправляет результат EstimationSize).
Брайан Гетц
1
@BrianGoetz: Да, это была моя мысль, я просто еще не удосужился проделать большую работу по обработке всех этих деталей.
Стюарт Маркс
3
@Brian Goetz: Это то, что я имел в виду, говоря «слишком сложно». Вызов tryAdvanceдо того, Streamкак это делает, превращает ленивую природу класса Streamв «частично ленивый» поток. Это также подразумевает, что поиск первого элемента больше не является tryAdvanceпараллельной операцией, поскольку, насколько я понял, вам нужно сначала разделить и сделать на разделенных частях одновременно, чтобы выполнить настоящую параллельную операцию. Если единственная операция терминала является findAnyили аналогичной, это уничтожит весь parallel()запрос.
Хольгер
2
Таким образом, для полной параллельной поддержки вы не должны вызывать tryAdvanceдо того, как это сделает поток, и должны заключить каждую разделенную часть в прокси и собрать информацию «hasAny» обо всех параллельных операциях самостоятельно и убедиться, что последняя параллельная операция выдает желаемое исключение, если поток был пуст. Много всего…
Хольгер
25

Во многих случаях этого может быть достаточно

stream.findAny().isPresent()
Kenglxn
источник
15

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

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

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

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

Эран
источник
7
Там же anyMatch(alwaysTrue()), я думаю , что это ближе всего к hasAny.
Марко Топольник
1
@MarkoTopolnik Только что проверил ссылку - я имел в виду findAny (), хотя anyMatch () тоже подойдет.
Эран
3
anyMatch(alwaysTrue())идеально соответствует предполагаемой семантике вашего hasAny, давая вам booleanвместо Optional<T>--- но мы тут
коснемся
1
Примечание alwaysTrue- это предикат Guava.
Jean-François Savard
11
anyMatch(e -> true)тогда.
FBB
6

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

В коде это:

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
Луис Роберто
источник
1
Если это все, что вам нужно, ответ kenglxn лучше.
Доминикас Мостаускис,
это бесполезно, он дублирует Collection.isEmpty ()
Krzysiek
@Krzysiek это не бесполезно, если вам нужно фильтровать коллекцию. Однако я согласен с Доминикасом в том, что ответ
kenglxn
Это потому, что он также дублируетStream.anyMatch()
Кшишек
4

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

феникс7360
источник
Я не могу понять, сильно ли это повлияет на производительность с параллельными потоками. Вероятно, стоит проверить это, если это требование
phoenix7360
Извините, не понял, что у @Holger также было решение, и Spliteratorмне интересно, как они сравниваются.
phoenix7360
0

Я бы просто использовал:

stream.count()>0
Дэниел Сп
источник