Операції проміжного потоку не оцінюються за підрахунком


33

Здається, у мене виникають проблеми з розумінням того, як Java створює потокові операції в трубопровід потоку.

При виконанні наступного коду

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

На консолі друкується лише друк 4. StringBuilderОб'єкт все ще має значення "".

Коли я додаю операцію фільтра: filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

Вихід змінюється на:

4
1234

Як ця, здавалося б, надмірна робота фільтра змінює поведінку складеного потокового трубопроводу?


2
Цікаво !!!
uneq95

3
Я б міг уявити, що це специфічна поведінка; можливо, це тому, що перший потік має відомий розмір, але другий - ні, і розмір-ness визначає, чи виконуються проміжні операції.
Енді Тернер

Що з інтересу, що станеться, якщо ви перевернете фільтр і карту?
Енді Тернер

Трохи запрограмувавшись у Haskell, це трохи пахне схожим на деяку ледачу оцінку. Пошуковий пошук у Google повернувся, що потоки справді мають лінь. Можливо, це так? І без фільтра, якщо Java досить розумна, немає необхідності реально виконувати відображення.
Фредерік

@AndyTurner Це дає той самий результат, навіть на розвороті
uneq95

Відповіді:


39

Операція count()терміналу в моїй версії JDK закінчується виконанням наступного коду:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

Якщо filter()в конвеєрі операцій є операція, розмір потоку, який відомий спочатку, більше не може бути відомий (оскільки filterможе відхилити деякі елементи потоку). Таким чином, ifблок не виконується, проміжні операції виконуються і StringBuilder таким чином модифікується.

З іншого боку, якщо у вас є лише map()трубопровід, кількість елементів у потоці гарантовано буде таким самим, як і початкове число елементів. Отже блок if виконується, а розмір повертається безпосередньо без оцінки проміжних операцій.

Зауважте, що лямбда, яка перейшла до map()порушення договору, визначеного в документації: це повинна бути операція, яка не заважає, без громадянства, але це не без громадянства. Тож наявність різного результату в обох випадках не може розглядатися як помилка.


Оскільки, flatMap()можливо, вдалося б змінити кількість елементів, чи це було причиною того, що вона спочатку прагнула (тепер ледача)? Отже, альтернативою було б використовувати forEach()і рахувати окремо, якщо map()в його нинішній формі порушується договір, я думаю.
Фредерік

3
Щодо плоскої карти, я не думаю. Це було, AFAIK, тому що це було простішим початком, щоб зробити це нетерплячим. Так, використання потоку з картою () для створення побічних ефектів - погана ідея.
JB Nizet

Чи є у вас пропозиція, як досягти повного виходу 4 1234без використання додаткового фільтра або створення побічних ефектів в операції map ()?
аталатус

1
int count = array.length; String result = String.join("", array);
JB Nizet

1
або ви можете використовувати forEach, якщо ви дійсно хочете використовувати StringBuilder, або ви можете використовуватиCollectors.joining("")
njzk2

19

У jdk-9 це було чітко зафіксовано у документах Java

Стікання побічних ефектів також може дивувати. За винятком термінальних операцій дляEach і forEachOrряд, побічні ефекти поведінкових параметрів не завжди можуть виконуватися, коли реалізація потоку може оптимізувати виконання параметрів поведінки, не впливаючи на результат обчислення. (Для конкретного прикладу дивіться примітку API, задокументовану про операцію підрахунку .)

Примітка API:

Реалізація може вирішити не виконувати конвеєр потоку (послідовно або паралельно), якщо він здатний обчислювати підрахунок безпосередньо з джерела потоку. У таких випадках жодні вихідні елементи не будуть пройдені і проміжні операції не будуть оцінені. На поведінкові параметри з побічними ефектами, які сильно перешкоджають, за винятком нешкідливих випадків, таких як налагодження. Наприклад, розглянемо такий потік:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

Відомо кількість елементів, охоплених джерелом потоку, Список, і проміжна операція, зазирнути, не вводить і не видаляє елементи з потоку (як це може бути в операціях flatMap або фільтра). Таким чином, кількість вважається розміром списку, і немає необхідності виконувати конвеєр і, як побічний ефект, роздрукувати елементи списку.


0

Це не те, для чого .map. Він повинен використовуватися для перетворення потоку "Щось" у потік "Щось інше". У цьому випадку ви використовуєте map, щоб додати рядок до зовнішнього Stringbuilder, після чого у вас є потік "Stringbuilder", кожен з яких був створений операцією з картою, додаючи один номер до оригінального Stringbuilder.

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

Фільтр змушує запускати вміст потоку, оскільки тепер він повинен робити щось значуще з кожним елементом потоку.

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