takeWhile () працює по-різному з flatmap


75

Я створюю фрагменти з takeWhile, щоб дослідити його можливості. При використанні спільно з flatMap поведінка не відповідає очікуванням. Знайдіть фрагмент коду нижче.

String[][] strArray = {{"Sample1", "Sample2"}, {"Sample3", "Sample4", "Sample5"}};

Arrays.stream(strArray)
        .flatMap(indStream -> Arrays.stream(indStream))
        .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
        .forEach(ele -> System.out.println(ele));

Фактичний результат:

Sample1
Sample2
Sample3
Sample5

Очікуваний вихід:

Sample1
Sample2
Sample3

Причиною сподівання є те, що takeWhile повинен виконуватися до тих пір, поки умова всередині не стане істинною. Я також додав оператори роздруківки в flatmap для налагодження. Потоки повертаються лише двічі, що відповідає очікуванням.

Однак це чудово працює без планової карти в ланцюжку.

String[] strArraySingle = {"Sample3", "Sample4", "Sample5"};
Arrays.stream(strArraySingle)
        .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
        .forEach(ele -> System.out.println(ele));

Фактичний результат:

Sample3

Тут фактичний результат збігається з очікуваним результатом.

Застереження. Ці фрагменти призначені лише для практики коду і не служать жодним дійсним випадкам використання.

Оновлення: Помилка JDK-8193856 : виправлення буде доступним як частина JDK 10. Зміною буде виправлення whileOps Sink :: accept

@Override 
public void accept(T t) {
    if (take = predicate.test(t)) {
        downstream.accept(t);
    }
}

Змінено реалізацію:

@Override
public void accept(T t) {
    if (take && (take = predicate.test(t))) {
        downstream.accept(t);
    }
}

Відповіді:


54

Це помилка в JDK 9 - з випуску # 8193856 :

takeWhileнеправильно припускає, що попередня операція підтримує та відзначає скасування, що, на жаль, не стосується flatMap.

Пояснення

Якщо потік упорядкований, takeWhileслід показати очікувану поведінку. Це не зовсім так у вашому коді, оскільки ви використовуєте forEach, який відмовляється від замовлення. Якщо ви піклуєтесь про це, що ви робите в цьому прикладі, forEachOrderedзамість цього вам слід скористатися . Кумедна річ: це нічого не змінює. 🤔

То, може, потік спочатку не замовлений? (У цьому випадку поведінка нормальна .) Якщо ви створите тимчасову змінну для потоку, створеного з, strArrayі перевірите, чи впорядковано це, виконавши вираз ((StatefulOp) stream).isOrdered();у точці розриву, ви виявите, що він справді впорядкований:

String[][] strArray = {{"Sample1", "Sample2"}, {"Sample3", "Sample4", "Sample5"}};

Stream<String> stream = Arrays.stream(strArray)
        .flatMap(indStream -> Arrays.stream(indStream))
        .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"));

// breakpoint here
System.out.println(stream);

Це означає, що це, швидше за все, помилка впровадження.

У кодекс

Як підозрювали інші, я тепер також думаю, що це може бути пов’язано з flatMapбажанням. Точніше, обидві проблеми можуть мати однакову першопричину.

Заглядаючи у джерело WhileOps, ми можемо побачити такі методи:

@Override
public void accept(T t) {
    if (take = predicate.test(t)) {
        downstream.accept(t);
    }
}

@Override
public boolean cancellationRequested() {
    return !take || downstream.cancellationRequested();
}

Цей код використовується takeWhileдля перевірки для заданого елемента потоку, tчи predicateвиконується:

  • Якщо так, він передає елемент downstreamоперації, в даному випадку System.out::println.
  • Якщо ні, він встановлює takeзначення false, тому, коли наступного разу запитується, чи слід скасовувати конвеєр (тобто це зроблено), він повертається true.

Це охоплює takeWhileоперацію. Інше, що вам потрібно знати, це те, що forEachOrderedпризводить до операції терміналу, що виконує метод ReferencePipeline::forEachWithCancel:

@Override
final boolean forEachWithCancel(Spliterator<P_OUT> spliterator, Sink<P_OUT> sink) {
    boolean cancelled;
    do { } while (
            !(cancelled = sink.cancellationRequested())
            && spliterator.tryAdvance(sink));
    return cancelled;
}

Все це робить:

  1. перевірити, чи не було скасовано газопровід
  2. якщо ні, пересуньте раковину на один елемент
  3. зупинити, якщо це був останній елемент

Виглядає багатообіцяюче, правда?

Без flatMap

У "хорошому випадку" (без flatMap; ваш другий приклад) forEachWithCancelбезпосередньо діє на WhileOpas, sinkі ви можете бачити, як це відбувається:

  • ReferencePipeline::forEachWithCancel робить свій цикл:
    • WhileOps::accept дається кожен елемент потоку
    • WhileOps::cancellationRequested запитується після кожного елемента
  • в якийсь момент "Sample4"не вдається предикат, і потік скасовується

Ага!

С flatMap

У «поганий випадок» (з flatMap, ваш перший приклад), forEachWithCancelдіє на flatMapоперації, хоча ,, який просто викликає forEachRemainingна ArraySpliteratorпротягом {"Sample3", "Sample4", "Sample5"}, який робить це:

if ((a = array).length >= (hi = fence) &&
    (i = index) >= 0 && i < (index = hi)) {
    do { action.accept((T)a[i]); } while (++i < hi);
}

Ігноруючи все це hiта fenceінше, що використовується лише в тому випадку, якщо обробка масиву розділена на паралельний потік, це простий forцикл, який передає кожен елемент takeWhileоперації, але ніколи не перевіряє, чи скасовано його . Отже, він буде охоче курсувати по всіх елементах цього "підпотоку" перед зупинкою, ймовірно, навіть через решту потоку .


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

10
Він не перебирає весь потік. Якщо останній елемент підпотоку відповідає предикату, спрацьовуватиме підтримка скасування зовнішнього потоку, наприклад, використовувати String[][] strArray = { {"Sample1", "Sample2"}, {"Sample3", "Sample4"}, {"Sample5", "Sample6"}, };як вхід, і він, здається, працює. Якщо збігається лише проміжний елемент, flatMapнезнання щодо скасування спричиняє перезапис прапора з наступною оцінкою елемента.
Holger

@Holger Я мав на увазі лише "підпотік" (що не було зрозуміло з моєї фрази) і навіть не думав про те, щоб слідувати за "підпотоком". Змінено формулювання та пов’язано з вашим коментарем із роз’яснень.
Ніколай Парлог,

16
Здається, вони вас чули: bugs.openjdk.java.net/browse/JDK-8193856
Stefan Zobel

20

Це таке помилка , незалежно від того , як я дивлюся на нього - і спасибі Хольгер за ваші коментарі. Я не хотів розміщувати цю відповідь тут (серйозно!), Але жодна з відповідей чітко не говорить, що це помилка.

Люди кажуть, що це має бути із замовленим / невпорядкованим, і це неправда, оскільки це буде повідомлятися true3 рази:

Stream<String[]> s1 = Arrays.stream(strArray);
System.out.println(s1.spliterator().hasCharacteristics(Spliterator.ORDERED));

Stream<String> s2 = Arrays.stream(strArray)
            .flatMap(indStream -> Arrays.stream(indStream));
System.out.println(s2.spliterator().hasCharacteristics(Spliterator.ORDERED));

Stream<String> s3 = Arrays.stream(strArray)
            .flatMap(indStream -> Arrays.stream(indStream))
            .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"));
System.out.println(s3.spliterator().hasCharacteristics(Spliterator.ORDERED));

Дуже цікаво також, що якщо ви зміните його на:

String[][] strArray = { 
         { "Sample1", "Sample2" }, 
         { "Sample3", "Sample5", "Sample4" }, // Sample4 is the last one here
         { "Sample7", "Sample8" } 
};

тоді Sample7і Sample8не будуть частиною результату, інакше вони будуть. Схоже, що flatmap ігнорує прапор скасування, який буде введено користувачем dropWhile.


11

Якщо ви подивитесь на документацію щодоtakeWhile :

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

якщо цей потік невпорядкований, [повертає] потік, що складається з підмножини елементів, взятих з цього потоку, які відповідають даному предикату.

Ваш потік випадково впорядкований, але takeWhile не знає, що він є. Як такий, він повертає другу умову - підмножину. Ти takeWhileпросто поводишся як filter.

Якщо ви додасте дзвінок sortedраніше takeWhile, ви побачите очікуваний результат:

Arrays.stream(strArray)
      .flatMap(indStream -> Arrays.stream(indStream))
      .sorted()
      .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
      .forEach(ele -> System.out.println(ele));

17
Чому це не замовлено, або чому воно не знає, що воно є? "конкатенацію" впорядкованих потоків слід замовляти, чи не так?
JB Nizet

9
@JBNizet, але тоді, якщо ви зробите кожен окремий крок Stream<String[]> s1 = Arrays.stream(strArray); System.out.println(s1.spliterator().hasCharacteristics(Split‌​erator.ORDERED))і так далі для кожного кроку - всі вони видадуть ORDEREDпотік, це виглядає як помилка, про яку ще не повідомляється
Євген

8
@Michael, як я бачу (згідно з попереднім коментарем) - ваш висновок для мене неправильний
Євген

10
« Але TakeWhile не знає , що це " ... ну чому ж він не знає , коли потік і його підпотоків будуть впорядковані і чому до .sorted().unordered() .takeWhile(…)цих пір робить правильні речі тоді? Я б сказав, це тому sorted, що це операція, що визначає стан, яка буферизує весь вхід, за яким слідує справді лінива ітерація.
Holger

2
"Ваш потік випадково впорядкований, але takeWhile не знає, що він є. Як такий, він повертає другу умову - підмножину. Ваш takeWhile просто діє як фільтр.": Але це звучить насправді неправильно. Якщо потік не упорядкований, він поверне свої елементи в якомусь непередбачуваному порядку. Тепер takeWhileслід діяти на елементи, які він фактично отримує, у тому порядку, в якому їх отримує, і зупинятися, як тільки елемент не задовольняє своєму предикату. Якщо хтось хоче фільтрувати в невпорядкованому потоці, він повинен використовувати filter.
Джорджо

9

Причиною цього є те, що flatMapоперація також є проміжною операцією, з якою використовується (одна з) проміжна операція з коротким замиканням takeWhile .

Поведінка, на flatMapяку вказує Холгер у цій відповіді , безумовно, є посиланням, яке не слід пропустити, щоб зрозуміти несподіваний результат для таких операцій короткого замикання.

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

List<String> sampleList = Arrays.stream(strArray).flatMap(Arrays::stream).collect(Collectors.toList());
sampleList.stream().takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
            .forEach(System.out::println);

Також, схоже, існує пов'язана помилка # JDK-8075939 для відстеження цієї поведінки, вже зареєстрованої.

Редагувати : Це можна відслідковувати далі на JDK-8193856, прийнятому як помилку.


8
Я не розумію вашого пояснення. Мені така поведінка здається помилкою. А запропонована вами альтернатива вимагає двох конвеєрів Stream, що може бути менш бажаним.
Еран

2
@Eran Дійсно, поведінка здається помилкою. Запропонований варіант - просто ввести операцію терміналу для завершення (вихлопної) flatMapоперації, а потім обробити потік для виконання takeWhile.
Наман
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.