Почему Java-streamи отключены?

В отличие от IEnumerable от C #, где конвейер выполнения может выполняться столько раз, сколько требуется, в Java stream может быть «итерирован» только один раз.

Любой вызов операции терминала закрывает stream, что делает его непригодным. Эта «особенность» отнимает много энергии.

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

Изменить: чтобы продемонстрировать, о чем я говорю, рассмотрим следующую реализацию Quick-Sort в C #:

 IEnumerable QuickSort(IEnumerable ints) { if (!ints.Any()) { return Enumerable.Empty(); } int pivot = ints.First(); IEnumerable lt = ints.Where(i => i < pivot); IEnumerable gt = ints.Where(i => i > pivot); return QuickSort(lt).Concat(new int[] { pivot }).Concat(QuickSort(gt)); } 

Теперь, конечно, я не сторонник того, что это хорошая реализация быстрой сортировки! Это, однако, отличный пример выразительной способности lambda-выражения в сочетании с работой streamа.

И это невозможно сделать на Java! Я даже не могу спросить stream, пустой ли он, не делая его непригодным.

У меня есть некоторые воспоминания о раннем дизайне API Streams, который может пролить свет на обоснование дизайна.

Еще в 2012 году мы добавляли lambdas на этот язык, и мы хотели, чтобы набор операций, ориентированный на коллекции, или «объемные данные», запрограммированный с использованием lambdas, что облегчило бы параллелизм. К этому моменту была создана идея ленивых операций цепочки. Мы также не хотели, чтобы промежуточные операции сохраняли результаты.

Основными проблемами, которые нам нужно было решить, были то, что объекты в цепочке выглядели в API и как они подключались к источникам данных. Источники часто были коллекциями, но мы также хотели поддерживать данные, поступающие из файла или сети, или данные, созданные «на лету», например, из генератора случайных чисел.

Было много влияний на существующую работу над дизайном. Среди наиболее влиятельных были Google Guava и библиотека коллекций Scala. (Если кто-то удивляется влиянию Гуавы, обратите внимание, что Кевин Бурриллион , ведущий разработчик Guava, был в группе экспертов JSR-335 Lambda .) В коллекциях Scala мы нашли этот разговор Мартином Одерским, представляющим особый интерес: Подтверждение коллекций Scala: от Mutable to Persistent to Parallel . (Stanford EE380, 2011 1 июня).

Наш прототип дизайна в то время был основан на Iterable . Известный filter операций, map и т. Д. Были методами расширения (по умолчанию) в Iterable . Вызов одного добавили операцию в цепочку и вернул другой Iterable . Операция терминала, такая как count , iterator() бы iterator() вверх по цепочке к источнику, и операции выполнялись в Iterator каждого этапа.

Поскольку это Iterables, вы можете вызвать метод iterator() более одного раза. Что тогда должно произойти?

Если источником является коллекция, это в основном работает нормально. Коллекции Iterable, и каждый вызов iterator() создает отдельный экземпляр Iterator, который не зависит от каких-либо других активных экземпляров и каждый обходит коллекцию независимо. Отлично.

Теперь, если источник является одним выстрелом, например, чтение строк из файла? Возможно, первый Итератор должен получить все значения, но второй и последующие должны быть пустыми. Возможно, значения должны быть чередующимися между Итераторами. Или, может быть, каждый Итератор должен получать одинаковые значения. Тогда, что, если у вас есть два iteratorа, а один дальше впереди другого? Кому-то придется буферизовать значения во втором iteratorе до тех пор, пока они не будут прочитаны. Хуже того, что, если вы получите один Итератор и прочитаете все значения, и только тогда получите второй Итератор. Откуда берутся ценности? Есть ли потребность в том, чтобы все их забуферировали на всякий случай, если кто-то хочет второй Итератор?

Ясно, что использование нескольких iteratorов над источником с одним выстрелом вызывает много вопросов. У нас не было хороших ответов. Мы хотели последовательного, предсказуемого поведения для того, что произойдет, если вы дважды назовете iterator() . Это подтолкнуло нас к отказу от многочисленных обходов, что сделало трубопроводы одним выстрелом.

Мы также наблюдали, как другие сталкиваются с этими проблемами. В JDK большинство Iterables – это коллекции или объекты, подобные коллекциям, которые допускают множественный обход. Он нигде не указан, но, похоже, было неписаное ожидание того, что Iterables допускают множественный обход. Заметным исключением является интерфейс NIO DirectoryStream . Его спецификация включает это интересное предупреждение:

Хотя DirectoryStream расширяет Iterable, это не универсальный Iterable, поскольку он поддерживает только один Iterator; вызов метода iteratorа для получения второго или последующего iteratorа вызывает IllegalStateException.

[жирный шрифт]

Это показалось нам необычным и неприятным, что мы не хотели создавать целую кучу новых Iterables, которые могли бы быть только один раз. Это оттолкнуло нас от использования Iterable.

Примерно в это же время появилась статья Брюса Эккела, в которой описывалась проблема, с которой он столкнулся со Скалой. Он написал этот код:

 // Scala val lines = fromString(data).getLines val registrants = lines.map(Registrant) registrants.foreach(println) registrants.foreach(println) 

Это довольно просто. Он анализирует строки текста в объектах Registrant и распечатывает их дважды. За исключением того, что он на самом деле только распечатывает их один раз. Оказывается, он считал, что registrants были коллекцией, когда на самом деле это iterator. Второй вызов foreach встречает пустой iterator, из которого все значения исчерпаны, поэтому он ничего не печатает.

Такой опыт убедил нас в том, что очень важно иметь четко предсказуемые результаты при попытке множественного обхода. В нем также подчеркивается важность различения ленивых структур, подобных конвейеру, из фактических коллекций, в которых хранятся данные. Это, в свою очередь, привело к разделению ленивых операций с трубопроводами в новый интерфейс Stream и поддержанию только активных, мутирующих операций непосредственно в коллекциях. Брайан Гетц объяснил это обоснованием.

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

Но давайте рассмотрим возможность многократного прохождения от конвейеров, основанных на коллекциях. Предположим, вы сделали это:

 Iterable it = source.filter(...).map(...).filter(...).map(...); it.into(dest1); it.into(dest2); 

( collect(toList()) теперь записывается collect(toList()) .)

Если источником является коллекция, то первый вызов into() вызовет цепочку iteratorов назад к исходному, выполнит операции конвейера и отправит результаты в пункт назначения. Второй вызов to into() создаст другую цепочку iteratorов и снова выполнит операции конвейера. Это, очевидно, не так, но у него есть эффект выполнения всех операций фильтра и карты второй раз для каждого элемента. Я думаю, что многие программисты были бы удивлены этим поведением.

Как я уже говорил выше, мы говорили с разработчиками Guava. Одна из classных вещей, которые у них есть, – это кладбище идей, где они описывают функции, которые они решили не реализовывать вместе с причинами. Идея ленивых коллекций звучит довольно круто, но вот что они должны сказать об этом. Рассмотрим List.filter() которая возвращает List :

Самая большая проблема здесь заключается в том, что слишком много операций становятся дорогостоящими, линейными предложениями. Если вы хотите отфильтровать список и получить список обратно, а не только коллекцию или итерацию, вы можете использовать ImmutableList.copyOf(Iterables.filter(list, predicate)) , который «указывает вперед», что он делает и как это дорого.

Чтобы взять конкретный пример, какова стоимость get(0) или size() в списке? Для обычно используемых classов, таких как ArrayList , они O (1). Но если вы назовете один из них в лениво отфильтрованном списке, он должен запустить фильтр по списку поддержки, и внезапно эти операции – O (n). Хуже того, он должен пересекать список поддержки при каждой операции.

Это казалось нам слишком лень. Одно дело – настроить некоторые операции и отложить фактическое выполнение до тех пор, пока вы не перейдете к «Go». Другое дело, чтобы настроить ситуацию таким образом, чтобы скрывать потенциально большую сумму пересчета.

Предлагая запретить streamи с нелинейным или «без повторного использования», Пол Сандос описал потенциальные последствия, позволяющие им порождать «неожиданные или запутанные результаты». Он также упомянул, что параллельное исполнение сделает вещи еще более сложными. Наконец, я бы добавил, что операция конвейера с побочными эффектами приведет к сложным и неясным ошибкам, если операция была неожиданно выполнена несколько раз или, по крайней мере, в разное количество раз, чем ожидал программист. (Но Java-программисты не пишут lambda-выражения с побочными эффектами, не так ли?)

Таким образом, это основное обоснование дизайна Java 8 Streams API, которое позволяет обход одного кадра и требует строго линейного (без ветвления) конвейера. Он обеспечивает последовательное поведение в нескольких разных источниках streamа, он четко отделяет ленивые от нетерпеливых операций и обеспечивает простую модель исполнения.


Что касается IEnumerable , я далек от эксперта по C # и .NET, поэтому я был бы признателен за исправление (мягко), если я сделаю неверные выводы. Однако представляется, что IEnumerable допускает, чтобы множественный обход вел себя по-разному с разными источниками; и он допускает ветвящуюся структуру вложенных операций IEnumerable , что может привести к некоторой значительной перерасчеты. Хотя я понимаю, что разные системы делают разные компромиссы, это две характеристики, которые мы стремились избежать в разработке Java 8 Streams API.

Пример quicksort, данный OP, интересен, озадачен, и мне жаль говорить, что это ужасно. Вызов QuickSort принимает IEnumerable и возвращает IEnumerable , поэтому сортировка фактически не выполняется до тех пор, пока не IEnumerable окончательный IEnumerable . Однако, по-видимому, этот вызов создает древовидную структуру IEnumerables которая отражает секционирование, которое будет выполнять quicksort, без фактического выполнения. (Это, в конце концов, ленивое вычисление.) Если источник имеет N элементов, дерево будет иметь N элементов шириной в самом широком, и это будет lg (N) уровней глубоко.

Мне кажется, и еще раз, я не эксперт по C # или .NET, что это вызовет некоторые безобидные вызовы, такие как выбор по ints.First() через ints.First() , чтобы быть более дорогостоящим, чем они выглядят , На первом уровне, конечно, это O (1). Но рассмотрите раздел, расположенный глубоко в дереве, на правом краю. Чтобы вычислить первый элемент этого раздела, нужно пройти весь источник, операцию O (N). Но так как вышеперечисленные разделы являются ленивыми, они должны быть пересчитаны, что требует O (lg N) сравнений. Таким образом, выбор стержня был бы операцией O (N lg N), которая столь же дорогая, как и весь вид.

Но мы фактически не сортируем, пока не пересекаем возвращаемый IEnumerable . В стандартном алгоритме быстрой сортировки каждый уровень разбиения удваивает количество разделов. Каждый раздел имеет лишь половину размера, поэтому каждый уровень остается на уровне O (N). Дерево разделов – O (lg N) высокое, поэтому общая работа O (N lg N).

С деревом ленивых IEnumerables в нижней части дерева есть N разделов. Для вычисления каждого раздела требуется обход N элементов, каждый из которых требует lg (N) сравнения дерева. Чтобы вычислить все разделы в нижней части дерева, необходимо выполнить сравнение O (N ^ 2 lg N).

(Это правда? Я с трудом могу поверить в это. Кто-нибудь, пожалуйста, проверьте это для меня.)

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

Задний план

Хотя вопрос кажется простым, фактический ответ требует некоторого фона, чтобы иметь смысл. Если вы хотите перейти к завершению, прокрутите вниз …

Выберите пункт сравнения – Основные функции

Используя основные понятия, концепция IEnumerable C # более тесно связана с Iterable Java , которая способна создавать столько iteratorов, сколько захотите. IEnumerables создает IEnumerators . Java Iterable создает Iterators

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

Давайте сравним эту функцию: на обоих языках, если class реализует IEnumerable / Iterable , тогда этот class должен реализовать хотя бы один метод (для C # это GetEnumerator а для Java – iterator() ). В каждом случае экземпляр, возвращаемый из этого ( IEnumerator / Iterator ), позволяет вам получить доступ к текущему и последующим элементам данных. Эта функция используется в синтаксисе для каждого языка.

Выберите точку сравнения – Расширенная функциональность

IEnumerable в C # был расширен, чтобы разрешить ряд других языковых функций (в основном связанных с Linq ). Добавленные функции include в себя выбор, outlookы, агрегации и т. Д. Эти расширения имеют сильную мотивацию от использования в теории множеств, подобно понятиям SQL и Relational Database.

Java 8 также добавила функциональность, чтобы обеспечить степень функционального программирования с использованием Streams и Lambdas. Обратите внимание, что streamи Java 8 не в первую очередь мотивированы теорией множеств, а функциональным программированием. Несмотря на это, есть много параллелей.

Итак, это второй пункт. Усовершенствования, сделанные для C #, были реализованы как усовершенствование концепции IEnumerable . Однако на Java усовершенствования были реализованы путем создания новых базовых концепций Lambdas и Streams, а затем также создания относительно тривиального способа конвертировать из Iterators и Iterables в Streams и наоборот.

Таким образом, сравнение IEnumerable с концепцией Stream Java не является полным. Вам нужно сравнить его с объединенными API-интерфейсами Streams and Collections в Java.

В Java streamи не совпадают с Iterables или Iterators

Потоки не предназначены для решения проблем так же, как iteratorы:

  • Итераторы – это способ описания последовательности данных.
  • Потоки – это способ описания последовательности преобразований данных.

С помощью Iterator вы получаете значение данных, обрабатываете его, а затем получаете другое значение данных.

С Streams вы объединяете последовательность функций, затем вы подаете входное значение в stream и получаете выходное значение из объединенной последовательности. Обратите внимание, что в терминах Java каждая функция инкапсулируется в одном экземпляре Stream . API Streams позволяет связывать последовательность экземпляров Stream таким образом, что цепочка последовательности выражений преобразования.

Чтобы завершить концепцию Stream , вам нужен источник данных для подачи streamа и функция терминала, которая потребляет stream.

Способ подачи значений в stream может фактически быть из Iterable , но сама последовательность Stream не является Iterable , это составная функция.

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

Обратите внимание на эти существенные предположения и особенности streamов:

  • Stream в Java – это механизм преобразования, он преобразует элемент данных в одном состоянии и переходит в другое состояние.
  • streamи не имеют понятия о порядке или позиции данных, просто преобразуют все, что они просят.
  • streamи могут быть снабжены данными из многих источников, включая другие streamи, iteratorы, итерации, коллекции,
  • вы не можете «перезагрузить» stream, это будет похоже на «перепрограммирование преобразования». Сброс источника данных, вероятно, вы хотите.
  • логически только 1 элемент данных «в полете» в streamе в любое время (если stream не является параллельным streamом, в этот момент в streamе есть 1 элемент). Это не зависит от источника данных, который может иметь больше, чем текущие элементы «готовы» к streamу, или сборщик streamов, который может потребоваться для агрегирования и уменьшения нескольких значений.
  • Потоки могут быть несвязаны (бесконечны), ограничены только источником данных или сборщиком (что также может быть бесконечным).
  • Потоки «цепочки», выход фильтрации одного streamа – это другой stream. Значения, вводимые и преобразованные streamом, в свою очередь могут быть переданы другому streamу, который выполняет другое преобразование. Данные в преобразованном состоянии перетекают из одного streamа в другой. Вам не нужно вмешиваться и извлекать данные из одного streamа и подключать его к следующему.

Сравнение C #

Когда вы считаете, что stream Java является частью системы поставки, streamа и сбора, и что streamи и iteratorы часто используются вместе с коллекциями, то неудивительно, что трудно связать те же концепции, которые почти все встроенные в одну концепцию IEnumerable в C #.

Части IEnumerable (и близкие связанные понятия) очевидны во всех концепциях Java Iterator, Iterable, Lambda и Stream.

Есть небольшие вещи, которые могут сделать Java-концепции, которые сложнее в IEnumerable и наоборот.


Вывод

  • Здесь нет проблем с дизайном, просто проблема совпадения понятий между языками.
  • Потоки решают проблемы по-другому
  • Потоки добавляют функциональность Java (они добавляют другой способ делать вещи, они не отвлекают функциональность)

Добавление streamов дает вам больше возможностей при решении проблем, которые справедливо classифицировать как «усиление власти», а не «сокращение», «отнятие» или «ограничение».

Почему Java-streamи отключены?

Этот вопрос ошибочен, потому что streamи представляют собой последовательности функций, а не данные. В зависимости от источника данных, который передает stream, вы можете сбросить источник данных и подать один и тот же или другой stream.

В отличие от IEnumerable от C #, где конвейер выполнения может выполняться столько раз, сколько требуется, в Java stream может быть «итерирован» только один раз.

Сравнение IEnumerable с Stream ошибочно. Контекст, который вы используете, чтобы сказать, что IEnumerable может выполняться столько раз, сколько вам нужно, лучше всего сравнить с Java Iterables , который можно повторить столько раз, сколько вы хотите. Stream Java представляет собой подмножество концепции IEnumerable , а не подмножество, которое поставляет данные, и, следовательно, не может быть «повторно запущено».

Любой вызов операции терминала закрывает stream, что делает его непригодным. Эта «особенность» отнимает много энергии.

Первое утверждение верно в некотором смысле. Утверждение «отнять власть» – нет. Вы по-прежнему сравниваете Streams it IEnumerables. Операция терминала в streamе похожа на предложение «break» в цикле for. Вы всегда можете иметь другой stream, если хотите, и если вы можете перенаправить нужные данные. Опять же, если вы считаете, что IEnumerable больше похож на Iterable , для этого утверждения Java делает это просто отлично.

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

Причина техническая, и по той простой причине, что Stream – это подмножество того, что кажется. Подмножество streamа не контролирует подачу данных, поэтому вы должны сбросить подачу, а не stream. В этом контексте это не так странно.

Пример QuickSort

Пример вашего quicksort имеет подпись:

 IEnumerable QuickSort(IEnumerable ints) 

Вы обрабатываете входной IEnumerable в качестве источника данных:

 IEnumerable lt = ints.Where(i => i < pivot); 

Кроме того, возвращаемое значение также равно IEnumerable , которое является источником данных, и поскольку это операция сортировки, порядок этого предложения является значительным. Если вы считаете class Java Iterable подходящим для этого, в частности, спецификацией List Iterable , поскольку List - это источник данных, который имеет гарантированный порядок или итерацию, то эквивалентный код Java для вашего кода будет выглядеть следующим образом:

 Stream quickSort(List ints) { // Using a stream to access the data, instead of the simpler ints.isEmpty() if (!ints.stream().findAny().isPresent()) { return Stream.of(); } // treating the ints as a data collection, just like the C# final Integer pivot = ints.get(0); // Using streams to get the two partitions List lt = ints.stream().filter(i -> i < pivot).collect(Collectors.toList()); List gt = ints.stream().filter(i -> i > pivot).collect(Collectors.toList()); return Stream.concat(Stream.concat(quickSort(lt), Stream.of(pivot)),quickSort(gt)); } 

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

Также обратите внимание, как Java-код использует источник данных ( List ) и streamовые концепции в разных точках, а в C # эти две «личности» могут быть выражены только в IEnumerable . Кроме того, хотя я использую List как базовый тип, я мог бы использовать более общую Collection , и с небольшим преобразованием iterator-to-Stream я мог бы использовать еще более общий Iterable

Spliterator s строятся вокруг Spliterator s, которые являются Spliterator с изменением состояния, изменяемыми. У них нет действия «перезагрузки», и на самом деле, требуя поддержки такого действия перемотки, «будет отнимать много сил». Каким образом Random.ints() должен обрабатывать такой запрос?

С другой стороны, для Stream s, которые имеют обратимое происхождение, легко построить эквивалентный Stream который будет использоваться снова. Просто поставьте шаги, чтобы построить Stream в метод многократного использования. Имейте в виду, что повторение этих шагов не является дорогостоящей операцией, так как все эти шаги – ленивые операции; фактическая работа начинается с операции терминала, и в зависимости от фактической операции терминала может выполняться совершенно другой код.

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


Кстати, во избежание путаницы терминальная операция потребляет Stream который отличается от закрытия Stream поскольку вызов close() в streamе делает (что требуется для streamов, имеющих связанные ресурсы, например, например, созданные Files.lines() ) ,


Похоже, что большая путаница проистекает из ошибочного сравнения IEnumerable с Stream . IEnumerable представляет возможность предоставить фактический IEnumerator , поэтому он похож на Iterable в Java. Напротив, Stream является своего рода iteratorом и сопоставим с IEnumerator поэтому неправильно утверждать, что такой тип данных можно использовать несколько раз в .NET, поддержка IEnumerator.Reset является необязательной. Приведенные здесь примеры скорее используют тот факт, что IEnumerable может использоваться для извлечения нового IEnumerator s и который также работает с Collection Java; вы можете получить новый Stream . Если разработчики Java решили добавить операции Stream в Iterable напрямую, при промежуточных операциях, возвращающих другой Iterable , это было действительно сопоставимо, и оно могло работать одинаково.

Тем не менее, разработчики решили против него, и решение обсуждается в этом вопросе . Самым большим моментом является путаница в отношении нетерпеливых операций с коллекциями и ленивых операций Stream. Посмотрев на .NET API, я (да, лично) считаю это обоснованным. Хотя выглядит разумно, глядя только на IEnumerable , определенная коллекция будет иметь множество методов, управляющих Collection напрямую, и множество методов, возвращающих ленивый IEnumerable , в то время как конкретный характер метода не всегда интуитивно узнаваем. Самый худший пример, который я нашел (в течение нескольких минут, когда я смотрел на него), – это List.Reverse() чье имя точно совпадает с именем унаследованного (это правильный конец для методов расширения?) Enumerable.Reverse() , имея полностью противоречащее поведению.


Конечно, это два разных решения. Первый, чтобы сделать Stream типом, отличным от Iterable / Collection и вторым, чтобы сделать Stream своего рода одноразовым iteratorом, а не другим типом iterable. Но это решение было принято вместе, и это может быть так, что разделение этих двух решений никогда не рассматривалось. Он не был создан, будучи сопоставимым с .NET.

Фактическое решение по проектированию API состояло в том, чтобы добавить улучшенный тип iteratorа – Spliterator . Spliterator s может быть предоставлен старыми Iterable s (каким образом они были модифицированы) или полностью новыми реализациями. Затем Stream был добавлен как высокоуровневый интерфейс к довольно низкоуровневому Spliterator s. Вот и все. Вы можете обсудить, будет ли другой дизайн лучше, но это неэффективно, оно не изменится, учитывая то, как они разрабатываются сейчас.

Существует еще один аспект реализации, который вы должны рассмотреть. Stream не являются неизменяемыми структурами данных. Каждая промежуточная операция может возвращать новый экземпляр Stream инкапсулирующий старый, но он может также манипулировать собственным экземпляром и возвращать себя (что не исключает возможности делать даже оба для одной и той же операции). Общеизвестными примерами являются такие операции, как parallel или unordered которые не добавляют другого шага, а манипулируют всем конвейером). Наличие такой изменчивой структуры данных и попытки повторного использования (или, что еще хуже, использование ее несколько раз в одно и то же время) не играют хорошо …


Для полноты, вот ваш пример quicksort, переведенный в API Java Stream . Это показывает, что на самом деле это не «отнимает много сил».

 static Stream quickSort(Supplier> ints) { final Optional optPivot = ints.get().findAny(); if(!optPivot.isPresent()) return Stream.empty(); final int pivot = optPivot.get(); Supplier> lt = ()->ints.get().filter(i -> i < pivot); Supplier> gt = ()->ints.get().filter(i -> i > pivot); return Stream.of(quickSort(lt), Stream.of(pivot), quickSort(gt)).flatMap(s->s); } 

It can be used like

 List l=new Random().ints(100, 0, 1000).boxed().collect(Collectors.toList()); System.out.println(l); System.out.println(quickSort(l::stream) .map(Object::toString).collect(Collectors.joining(", "))); 

You can write it even more compact as

 static Stream quickSort(Supplier> ints) { return ints.get().findAny().map(pivot -> Stream.of( quickSort(()->ints.get().filter(i -> i < pivot)), Stream.of(pivot), quickSort(()->ints.get().filter(i -> i > pivot))) .flatMap(s->s)).orElse(Stream.empty()); } 

I think there are very few differences between the two when you look closely enough.

At it’s face, an IEnumerable does appear to be a reusable construct:

 IEnumerable numbers = new int[] { 1, 2, 3, 4, 5 }; foreach (var n in numbers) { Console.WriteLine(n); } 

However, the compiler is actually doing a little bit of work to help us out; it generates the following code:

 IEnumerable numbers = new int[] { 1, 2, 3, 4, 5 }; IEnumerator enumerator = numbers.GetEnumerator(); while (enumerator.MoveNext()) { Console.WriteLine(enumerator.Current); } 

Each time you would actually iterate over the enumerable, the compiler creates an enumerator. The enumerator is not reusable; further calls to MoveNext will just return false, and there is no way to reset it to the beginning. If you want to iterate over the numbers again, you will need to create another enumerator instance.


To better illustrate that the IEnumerable has (can have) the same ‘feature’ as a Java Stream, consider a enumerable whose source of the numbers is not a static collection. For example, we can create an enumerable object which generates a sequence of 5 random numbers:

 class Generator : IEnumerator { Random _r; int _current; int _count = 0; public Generator(Random r) { _r = r; } public bool MoveNext() { _current= _r.Next(); _count++; return _count <= 5; } public int Current { get { return _current; } } } class RandomNumberStream : IEnumerable { Random _r = new Random(); public IEnumerator GetEnumerator() { return new Generator(_r); } public IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } 

Now we have very similar code to the previous array-based enumerable, but with a second iteration over numbers :

 IEnumerable numbers = new RandomNumberStream(); foreach (var n in numbers) { Console.WriteLine(n); } foreach (var n in numbers) { Console.WriteLine(n); } 

The second time we iterate over numbers we will get a different sequence of numbers, which isn’t reusable in the same sense. Or, we could have written the RandomNumberStream to thrown an exception if you try to iterate over it multiple times, making the enumerable actually unusable (like a Java Stream).

Also, what does your enumerable-based quick sort mean when applied to a RandomNumberStream ?


Вывод

So, the biggest difference is that .NET allows you to reuse an IEnumerable by implicitly creating a new IEnumerator in the background whenever it would need to access elements in the sequence.

This implicit behavior is often useful (and ‘powerful’ as you state), because we can repeatedly iterate over a collection.

But sometimes, this implicit behavior can actually cause problems. If your data source is not static, or is costly to access (like a database or web site), then a lot of assumptions about IEnumerable have to be discarded; reuse is not that straight-forward

It is possible to bypass some of the “run once” protections in the Stream API; for example we can avoid java.lang.IllegalStateException exceptions (with message “stream has already been operated upon or closed”) by referencing and reusing the Spliterator (rather than the Stream directly).

For example, this code will run without throwing an exception:

  Spliterator split = Stream.of("hello","world") .map(s->"prefix-"+s) .spliterator(); Stream replayable1 = StreamSupport.stream(split,false); Stream replayable2 = StreamSupport.stream(split,false); replayable1.forEach(System.out::println); replayable2.forEach(System.out::println); 

However the output will be limited to

 prefix-hello prefix-world 

rather than repeating the output twice. This is because the ArraySpliterator used as the Stream source is stateful and stores its current position. When we replay this Stream we start again at the end.

We have a number of options to solve this challenge:

  1. We could make use of a stateless Stream creation method such as Stream#generate() . We would have to manage state externally in our own code and reset between Stream “replays”:

     Spliterator split = Stream.generate(this::nextValue) .map(s->"prefix-"+s) .spliterator(); Stream replayable1 = StreamSupport.stream(split,false); Stream replayable2 = StreamSupport.stream(split,false); replayable1.forEach(System.out::println); this.resetCounter(); replayable2.forEach(System.out::println); 
  2. Another (slightly better but not perfect) solution to this is to write our own ArraySpliterator (or similar Stream source) that includes some capacity to reset the current counter. If we were to use it to generate the Stream we could potentially replay them successfully.

     MyArraySpliterator arraySplit = new MyArraySpliterator("hello","world"); Spliterator split = StreamSupport.stream(arraySplit,false) .map(s->"prefix-"+s) .spliterator(); Stream replayable1 = StreamSupport.stream(split,false); Stream replayable2 = StreamSupport.stream(split,false); replayable1.forEach(System.out::println); arraySplit.reset(); replayable2.forEach(System.out::println); 
  3. The best solution to this problem (in my opinion) is to make a new copy of any stateful Spliterator s used in the Stream pipeline when new operators are invoked on the Stream . This is more complex and involved to implement, but if you don’t mind using third party libraries, cyclops-react has a Stream implementation that does exactly this. (Disclosure: I am the lead developer for this project.)

     Stream replayableStream = ReactiveSeq.of("hello","world") .map(s->"prefix-"+s); replayableStream.forEach(System.out::println); replayableStream.forEach(System.out::println); 

This will print

 prefix-hello prefix-world prefix-hello prefix-world 

как и ожидалось.

  • Преобразование Iterable в Stream с использованием Java 8 JDK
  • Почему Stream не имеет метода toList ()?
  • Java 8 Streams: несколько фильтров или сложное условие
  • Сбор последовательных пар из streamа
  • Группировать по и суммировать объекты, как в SQL с Java lambdas?
  • Ограничить stream предикатом
  • Параллельные streamи, коллекторы и безопасность streamов
  • Пользовательский пул streamов в параллельном streamе Java 8
  • Java 8 Iterable.forEach () vs foreach loop
  • В streamах Java просматривается действительно только для отладки?
  • java.util.stream с ResultSet
  • Давайте будем гением компьютера.