Як перевірити, чи потік Java 8 порожній?


95

Як я можу перевірити, чи Streamпорожнє a , і видати виняток, якщо ні, як нетермінальну операцію?

В основному, я шукаю щось еквівалентне коду нижче, але без матеріалізації потоку між ними. Зокрема, перевірка не повинна відбуватися до того, як потік фактично спожитий операцією терміналу.

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 (лінощі).
Марко Топольник

Зрештою він буде спожитий, в цей момент повинна відбутися перевірка
Головоногий

11
Щоб перевірити, чи не є потік порожнім, потрібно спробувати спожити хоча б один елемент. У цей момент потік втратив "незайманість" і не може бути спожитий знову з самого початку.
Марко Топольник

Відповіді:


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

В основному перетворіть потік в a Iterator, щоб зателефонувати hasNext()йому, і якщо це правда, перетворіть Iteratorназад в a Stream. Це неефективно, оскільки всі наступні операції над потоком будуть проходити через ітератор hasNext()та next()методи, що також означає, що потік ефективно обробляється послідовно (навіть якщо згодом він перетворюється паралельно). Однак це дозволяє перевірити потік без буферизації всіх його елементів.

Ймовірно, є спосіб зробити це, використовуючи Spliteratorзамість Iterator. Це потенційно дозволяє поверненому потоку мати ті самі характеристики, що і вхідний потік, включаючи паралельний запуск.


1
Я не думаю, що існує рішення, яке можна підтримувати, яке підтримувало б ефективну паралельну обробку, оскільки важко підтримувати розбиття, однак маючи estimatedSizeі characteristicsнавіть покращивши однопотокові характеристики. Просто трапилось, що я написав Spliteratorрішення, коли ви публікували Iteratorрішення ...
Холгер,

3
Ви можете попросити в потоку Spliterator, викликати tryAdvance (лямбда), де ваша лямбда фіксує все передане йому, а потім повертати Spliterator, який делегує майже все до базового Spliterator, за винятком того, що він склеює перший елемент назад на перший шматок ( і фіксує результат оцінки розміру).
Брайан Гетц,

1
@BrianGoetz Так, це була моя думка, я просто ще не потрудився пройти повну роботу з обробки всіх цих деталей.
Стюарт Маркс

3
@Brian Goetz: Це те, що я мав на увазі під словом "занадто складно". Зателефонувавши tryAdvanceперед цим Stream, він перетворює ледачу природу Streamна «частково ледачий» потік. Це також означає, що пошук першого елемента вже не є паралельною операцією, оскільки вам доведеться розділити спочатку і tryAdvanceодночасно робити розділені частини, щоб зробити справжню паралельну операцію, наскільки я зрозумів. Якщо операція єдиного терміналу є findAnyабо подібною, це призведе до знищення всього parallel()запиту.
Holger

2
Отже, для повної паралельної підтримки ви не повинні телефонувати tryAdvanceдо того, як це зробить потік, і вам доведеться обертати кожну розділену частину в проксі-сервер та збирати інформацію “hasAny” про всі паралельні операції самостійно та переконатися, що остання паралельна операція видає бажаний виняток, якщо потік був порожній. Багато речей ...
Холгер,

18

Цього може бути достатньо у багатьох випадках

stream.findAny().isPresent()

15

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

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

Це допоможе вам лише у тому випадку, якщо вхідний список містить багато елементів і один із перших декількох передає фільтри, оскільки лише невелика підмножина списку повинна бути використана, перш ніж ви дізнаєтесь, що потік не порожній.

Звичайно, вам все одно доведеться створити новий потік, щоб створити вихідний список.


7
Там anyMatch(alwaysTrue()), я думаю, це найближче до hasAny.
Марко Топольник

1
@MarkoTopolnik Щойно перевірив посилання - я мав на увазі findAny (), хоча anyMatch () також буде працювати.
Еран

3
anyMatch(alwaysTrue())ідеально відповідає задуманій семантиці вашої hasAny, даючи вам booleanзамість Optional<T>--- але ми розділяємо волосся тут :)
Марко Топольник

1
Примітка alwaysTrue- предикат гуави.
Жан-Франсуа Савард

10
anyMatch(e -> true)тоді.
FBB

5

Я думаю, цього повинно бути достатньо для відображення логічного значення

У коді це:

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


Я не можу зрозуміти, чи це суттєво вплине на продуктивність паралельних потоків. Мабуть, слід перевірити це, якщо це є вимога
phoenix7360

Вибачте, не зрозумів, що у @Holger теж було рішення, Spliteratorцікаво, як вони порівнюються.
phoenix7360
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.