it-swarm.com.ru

Java 8 Stream: разница между limit () и skip ()

Говоря о Streams, когда я выполняю этот кусок кода

public class Main {
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));
    }
}

Я получаю этот вывод 

A1B1C1
A2B2C2
A3B3C3

потому что ограничение моего потока первыми тремя компонентами заставляет действия A, B и C выполняться только три раза.

Попытка выполнить аналогичные вычисления для последних трех элементов с помощью метода skip(), показывает другое поведение:

public class Main {
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .skip(6)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));
    }
}

выводит это

A1
A2
A3
A4
A5
A6
A7B7C7
A8B8C8
A9B9C9

Почему в этом случае выполняются действия A1 - A6? Это должно быть как-то связано с тем, что limit является короткозамкнутой промежуточной операцией с состоянием , а skip - нет, но я не понимаю практических последствий этого свойства. Это просто, что «каждое действие до skip выполняется, а не все до limit»?

55
Luigi Cortese

То, что у вас есть, это два потоковых конвейера.

Каждый из этих потоковых конвейеров состоит из источника, нескольких промежуточных операций и операции терминала.

Но промежуточные операции ленивы. Это означает, что ничего не происходит, если для последующей операции не требуется элемент. Когда это происходит, промежуточная операция делает все необходимое для создания требуемого элемента, а затем снова ждет, пока не будет запрошен другой элемент, и так далее.

Терминальные операции обычно "нетерпеливы". То есть они запрашивают все элементы в потоке, необходимые для их завершения.

Таким образом, вы должны действительно думать о конвейере как о forEach, запрашивающем поток позади него для следующего элемента, и этот поток запрашивает поток позади него и так далее, вплоть до источника.

Имея это в виду, давайте посмотрим, что мы имеем с вашим первым конвейером:

Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));

Итак, forEach запрашивает первый элемент. Это означает, что «B» peek нуждается в элементе и запрашивает у него выходной поток limit, что означает, что limit потребуется запросить «A» peek, который идет к источнику. Предмет дается и проходит вплоть до forEach, и вы получаете свою первую строку:

A1B1C1

forEach запрашивает другой элемент, затем другой. И каждый раз запрос распространяется по потоку и выполняется. Но когда forEach запрашивает четвертый элемент, когда запрос достигает limit, он знает, что он уже передал все элементы, которые ему разрешено передавать.

Таким образом, он не запрашивает «A» для поиска другого элемента. Это немедленно указывает, что его элементы исчерпаны, и, таким образом, больше никаких действий не выполняется, и forEach завершается.

Что происходит во втором конвейере?

    Stream.of(1,2,3,4,5,6,7,8,9)
    .peek(x->System.out.print("\nA"+x))
    .skip(6)
    .peek(x->System.out.print("B"+x))
    .forEach(x->System.out.print("C"+x));

Опять же, forEach запрашивает первый элемент. Это распространяется обратно. Но когда он добирается до skip, он знает, что должен запросить 6 элементов из своего восходящего потока, прежде чем он сможет пройти один нисходящий. Таким образом, он делает запрос в восходящем направлении от «A» peek, использует его, не передавая его в нисходящем направлении, делает другой запрос и так далее. Таким образом, просмотр «A» получает 6 запросов на элемент и производит 6 отпечатков, но эти элементы не передаются.

A1
A2
A3
A4
A5
A6

По седьмому запросу, сделанному skip, элемент передается на экран «B», а от него к forEach, поэтому выполняется полная печать:

A7B7C7

Тогда это так же, как и раньше. Теперь skip всякий раз, когда получает запрос, запрашивает элемент в восходящем направлении и передает его в нисходящем направлении, поскольку он «знает», что он уже выполнил свою работу. Таким образом, остальные отпечатки проходят через всю трубу, пока источник не будет исчерпан.

84
RealSkeptic

Свободное обозначение потокового трубопровода - вот что вызывает эту путаницу. Подумайте об этом таким образом:

limit(3)

Все конвейерные операции оцениваются лениво, кроме forEach(), которая является терминальной операцией , которая запускает "выполнение конвейера".

Когда конвейер выполняется, определения промежуточного потока не будут делать никаких предположений о том, что происходит "до" или "после". Все, что они делают, это принимают входной поток и преобразуют его в выходной поток:

Stream<Integer> s1 = Stream.of(1,2,3,4,5,6,7,8,9);
Stream<Integer> s2 = s1.peek(x->System.out.print("\nA"+x));
Stream<Integer> s3 = s2.limit(3);
Stream<Integer> s4 = s3.peek(x->System.out.print("B"+x));

s4.forEach(x->System.out.print("C"+x));
  • s1 содержит 9 различных значений Integer.
  • s2 просматривает все передаваемые значения и печатает их.
  • s3 передает первые 3 значения s4 и прерывает конвейер после третьего значения. Никакие дальнейшие значения не создаются s3. Это не означает, что в конвейере больше нет значений. s2 по-прежнему будет генерировать (и печатать) больше значений, но никто не запрашивает эти значения, и поэтому выполнение останавливается.
  • s4 снова просматривает все передаваемые значения и печатает их.
  • forEach потребляет и печатает все, что s4 передает ему.

Подумайте об этом таким образом. Весь поток совершенно ленивый. Только операция терминала активно вытягивает новые значения из конвейера. После получения трех значений из s4 <- s3 <- s2 <- s1, s3 больше не будет генерировать новые значения и больше не будет извлекать какие-либо значения из s2 <- s1. Хотя s1 -> s2 по-прежнему сможет генерировать 4-9, эти значения просто никогда не извлекаются из конвейера и, следовательно, никогда не выводятся s2.

skip(6)

С skip() происходит то же самое:

Stream<Integer> s1 = Stream.of(1,2,3,4,5,6,7,8,9);
Stream<Integer> s2 = s1.peek(x->System.out.print("\nA"+x));
Stream<Integer> s3 = s2.skip(6);
Stream<Integer> s4 = s3.peek(x->System.out.print("B"+x));

s4.forEach(x->System.out.print("C"+x));
  • s1 содержит 9 различных значений Integer.
  • s2 просматривает все передаваемые значения и печатает их.
  • s3 использует первые 6 значений, «пропуск их», что означает, что первые 6 значений не передаются в s4, а только последующие значения.
  • s4 снова просматривает все передаваемые значения и печатает их.
  • forEach потребляет и печатает все, что s4 передает ему.

Здесь важно то, что s2 не знает о том, что оставшийся конвейер пропускает какие-либо значения. s2 просматривает все значения независимо от того, что происходит потом.

Другой пример:

Рассмотрим этот конвейер, который указан в этом сообщении в блоге

IntStream.iterate(0, i -> ( i + 1 ) % 2)
         .distinct()
         .limit(10)
         .forEach(System.out::println);

Когда вы выполните вышеуказанное, программа никогда не остановится. Зачем? Так как:

IntStream i1 = IntStream.iterate(0, i -> ( i + 1 ) % 2);
IntStream i2 = i1.distinct();
IntStream i3 = i2.limit(10);

i3.forEach(System.out::println);

Что значит:

  • i1 генерирует бесконечное количество переменных значений: 0, 1, 0, 1, 0, 1, ...
  • i2 использует все значения, с которыми встречались ранее, передавая только "new" значения, т. е. в общей сложности 2 значения выходят из i2.
  • i3 передает 10 значений, затем останавливается.

Этот алгоритм никогда не остановится, потому что i3 ожидает, пока i2 выдаст еще 8 значений после 0 и 1, но эти значения никогда не появятся, в то время как i1 никогда не прекращает подачу значений в i2.

Неважно, что в какой-то момент в конвейере было получено более 10 значений. Все, что имеет значение, это то, что i3 никогда не видел эти 10 значений.

Чтобы ответить на ваш вопрос:

Это просто, что «каждое действие перед пропуском выполняется, а не все до ограничения»?

Нету. Все операции перед skip() или limit() выполняются. В обоих ваших казнях вы получаете A1 - A3. Но limit() может привести к короткому замыканию в конвейере, прервав потребление стоимости после того, как произошло интересующее событие (предел достигнут).

10
Lukas Eder

Полностью кощунственно смотреть на операции Steam индивидуально, потому что поток оценивается не так. 

Говоря о limit (3) , это операция короткого замыкания, которая имеет смысл, потому что, если подумать, какая бы операция ни была до и после limit, наличие ограничения в потоке остановите итерацию после получения n elements till операции limit, но это не означает, что будут обработаны только n потоковых элементов. Возьмем эту другую потоковую операцию для примера

public class App 
{
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .filter(x -> x%2==0)
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));
    }
}

будет выводить 

A1
A2B2C2
A3
A4B4C4
A5
A6B6C6

что кажется правильным, потому что лимит ожидает 3 потоковых элемента для прохождения через цепочку операций, хотя 6 элементов потока обрабатываются.

8
Amm Sokun

Все потоки основаны на разделителях, которые в основном имеют две операции: продвижение (перемещение вперед на один элемент, аналогично итератору) и разделение (разделение в произвольной позиции, что подходит для параллельной обработки). Вы можете прекратить принимать входные элементы в любой момент, когда захотите (что делает limit), но вы не можете просто перейти на произвольную позицию (в интерфейсе Spliterator такой операции нет). Таким образом, операция skip должна фактически считывать первые элементы из источника, чтобы просто игнорировать их. Обратите внимание, что в некоторых случаях вы можете выполнить фактический переход:

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9);

list.stream().skip(3)... // will read 1,2,3, but ignore them
list.subList(3, list.size()).stream()... // will actually jump over the first three elements
4
Tagir Valeev

Возможно, эта небольшая диаграмма помогает получить некоторое естественное «ощущение» того, как обрабатывается поток.

Первая строка =>8=>=7=...=== отображает поток. Элементы 1..8 текут слева направо. Есть три «окна»:

  1. В первом окне (peek A) вы видите все
  2. Во втором окне (skip 6 или limit 3) выполняется своего рода фильтрация. Первый или последний элементы «исключены» - значит, не переданы для дальнейшей обработки.
  3. В третьем окне вы видите только те предметы, которые были переданы

┌────────────────────────────────────────────────────────────────────────────┐ │ │ │▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸ │ │ 8 7 6 5 4 3 2 1 │ │▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸ │ │ │ │ │ │ │ │ skip 6 │ │ │ peek A limit 3 peek B │ └────────────────────────────────────────────────────────────────────────────┘

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

0
yaccob