TakeWhile() работает по-разному с плоской таблицей

Я создаю фрагменты с 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

ExpectedOutput:

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

Здесь фактический вывод соответствует ожидаемому результату.

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

Update: Ошибка JDK-8193856: исправление будет доступно как часть JDK 10. Изменение будет состоять в том, чтобы исправить whileOps Раковина:: принять

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

Ответ 1

Это ошибка в JDK 9 - из issue # 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;
}

Все, что он делает, это:

  • проверить, был ли протокол отменен.
  • если нет, продвиньте приемник на один элемент
  • остановить, если это был последний элемент

Выглядит многообещающе, верно?

Без flatMap

В "хорошем случае" (без flatMap, ваш второй пример) forEachWithCancel напрямую работает с WhileOp как sink, и вы можете видеть, как это выглядит:

  • ReferencePipeline::forEachWithCancel делает свой цикл:
    • WhileOps::accept предоставляется каждый элемент потока
    • WhileOps::cancellationRequested запрашивается после каждого элемента
  • в какой-то момент "Sample4" не выполняется предикат, и поток отменяется

Yay!

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

Ответ 2

Это ошибка, как бы я ни смотрел на нее, и спасибо Хольгеру за ваши комментарии. Я не хотел вставлять этот ответ здесь (серьезно!), Но ни один из ответов не говорит о том, что это ошибка.

Люди говорят, что это должно быть с упорядоченным/неуказанным, и это не так, поскольку это сообщит true 3 раза:

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.

Ответ 3

Причиной этого является операция 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);

Кроме того, похоже, что связанный Bug # JDK-8075939 отслеживает уже зарегистрированное поведение.

Изменить. Это можно отследить далее в JDK-8193856, принятом как ошибка.

Ответ 4

Если вы посмотрите документацию для 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));