it-swarm.com.ru

Почему Java Потоки разовые?

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

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

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

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

IEnumerable<int> QuickSort(IEnumerable<int> ints)
{
  if (!ints.Any()) {
    return Enumerable.Empty<int>();
  }

  int pivot = ints.First();

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

  return QuickSort(lt).Concat(new int[] { pivot }).Concat(QuickSort(gt));
}

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

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

223
Vitaliy

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

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

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

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

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

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

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

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

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

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

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

[полужирный в оригинале]

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

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

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

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

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

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

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

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

(Операция into теперь пишется collect(toList()).)

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

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

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

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

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

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

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


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

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

Мне кажется - и еще раз, я не эксперт по C # или .NET - что это приведет к тому, что некоторые вызовы безобидного вида, такие как выбор сводки через 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 может использоваться таким образом для построения сложных структур вычислений. Но если это действительно увеличивает вычислительную сложность настолько, насколько я думаю, то, казалось бы, программирование таким способом - это то, чего следует избегать, если только вы не будете чрезвычайно осторожны.

352
Stuart Marks

Фон

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

Выберите точку сравнения - основные функции

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

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

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

Выберите точку сравнения - расширенные функциональные возможности

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

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

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

Таким образом, сравнение IEnumerable с концепцией Java Stream является неполным. Вам необходимо сравнить его с объединенными API потоков и коллекций в Java.

В Java потоки не совпадают с итераторами или итераторами

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

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

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

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

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

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

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

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

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

Сравнение C #

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

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

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


Заключение

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

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

Почему Java Потоки разовые?

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

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

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

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

Первое утверждение в некотором смысле верно. Заявление "отнимает власть" - нет. Вы все еще сравниваете потоки это IEnumerables. Терминальная операция в потоке похожа на условие break в цикле for. Вы всегда можете иметь другой поток, если хотите, и если вы можете повторно предоставить нужные вам данные. Опять же, если вы считаете, что IEnumerable больше похоже на Iterable, для этого оператора Java делает это просто отлично.

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

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

Пример быстрой сортировки

Ваш пример быстрой сортировки имеет подпись:

IEnumerable<int> QuickSort(IEnumerable<int> ints)

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

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

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

Stream<Integer> quickSort(List<Integer> 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<Integer> lt = ints.stream().filter(i -> i < pivot).collect(Collectors.toList());
    List<Integer> gt = ints.stream().filter(i -> i > pivot).collect(Collectors.toList());

    return Stream.concat(Stream.concat(quickSort(lt), Stream.of(pivot)),quickSort(gt));
}    

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

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

120
rolfl

Streams построены вокруг Spliterators, которые являются изменяемыми объектами с изменяемым состоянием. У них нет действия по "перезагрузке", и, фактически, требование поддержать такое действие по перемотке "отнимает много сил". Как Random.ints() должен обрабатывать такой запрос?

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

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


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


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

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


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

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

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


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

static Stream<Integer> quickSort(Supplier<Stream<Integer>> ints) {

  final Optional<Integer> optPivot = ints.get().findAny();
  if(!optPivot.isPresent()) return Stream.empty();

  final int pivot = optPivot.get();

  Supplier<Stream<Integer>> lt = ()->ints.get().filter(i -> i < pivot);
  Supplier<Stream<Integer>> gt = ()->ints.get().filter(i -> i > pivot);

  return Stream.of(quickSort(lt), Stream.of(pivot), quickSort(gt)).flatMap(s->s);
}

Может использоваться как

List<Integer> 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(", ")));

Вы можете написать его еще более компактным, как

static Stream<Integer> quickSort(Supplier<Stream<Integer>> 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());
}
20
Holger

Я думаю, что между ними очень мало различий, если присмотреться.

На первый взгляд, IEnumerable выглядит как многократно используемая конструкция:

IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };

foreach (var n in numbers) {
    Console.WriteLine(n);
}

Тем не менее, компилятор фактически выполняет небольшую работу, чтобы выручить нас; он генерирует следующий код:

IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };

IEnumerator<int> enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext()) {
    Console.WriteLine(enumerator.Current);
}

Каждый раз, когда вы фактически перебираете перечислимое, компилятор создает перечислитель. Перечислитель не может быть использован повторно; последующие вызовы MoveNext просто вернут false, и невозможно восстановить его в начале. Если вы хотите снова выполнить итерации по числам, вам нужно будет создать еще один экземпляр перечислителя.


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

class Generator : IEnumerator<int> {
    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<int> {
    Random _r = new Random();
    public IEnumerator<int> GetEnumerator() {
        return new Generator(_r);
    }
    public IEnumerator IEnumerable.GetEnumerator() {
        return this.GetEnumerator();
    }
}

Теперь у нас есть код, очень похожий на предыдущий перечисляемый на основе массива, но со второй итерацией над numbers:

IEnumerable<int> numbers = new RandomNumberStream();

foreach (var n in numbers) {
    Console.WriteLine(n);
}
foreach (var n in numbers) {
    Console.WriteLine(n);
}

Во второй раз, когда мы перебираем numbers, мы получим другую последовательность чисел, которая не может использоваться повторно в том же смысле. Или мы могли бы написать RandomNumberStream, чтобы выдать исключение, если вы попытаетесь повторить его несколько раз, делая перечисляемое фактически непригодным для использования (например, Java Stream).

Кроме того, что означает ваша быстрая сортировка на основе перечисления при применении к RandomNumberStream?


Заключение

Таким образом, самое большое отличие состоит в том, что .NET позволяет вам повторно использовать IEnumerable, неявно создавая новое IEnumerator в фоновом режиме всякий раз, когда ему потребуется доступ к элементам в последовательности.

Это неявное поведение часто полезно (и "мощно", как вы заявляете), потому что мы можем многократно перебирать коллекцию.

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

8
Andrew Vermie

Можно обойти некоторые из защит "запустить один раз" в Stream API; например, мы можем избежать исключений Java.lang.IllegalStateException (с сообщением "поток уже обработан или закрыт"), ссылаясь и повторно используя Spliterator (а не Stream напрямую).

Например, этот код будет работать без исключения:

    Spliterator<String> split = Stream.of("hello","world")
                                      .map(s->"prefix-"+s)
                                      .spliterator();

    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);


    replayable1.forEach(System.out::println);
    replayable2.forEach(System.out::println);

Однако вывод будет ограничен

prefix-hello
prefix-world

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

У нас есть несколько вариантов решения этой проблемы:

  1. Мы могли бы использовать метод создания Stream без сохранения состояния, такой как Stream#generate(). Нам пришлось бы управлять состоянием извне в нашем собственном коде и выполнять сброс между Stream "replays":

    Spliterator<String> split = Stream.generate(this::nextValue)
                                      .map(s->"prefix-"+s)
                                      .spliterator();
    
    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);
    
    
    replayable1.forEach(System.out::println);
    this.resetCounter();
    replayable2.forEach(System.out::println);
    
  2. Другое (немного лучшее, но не идеальное) решение этой проблемы заключается в написании нашего собственного ArraySpliterator (или аналогичного Stream источника), который включает в себя некоторую способность сбрасывать текущий счетчик. Если бы мы использовали его для генерации Stream, мы могли бы успешно воспроизвести их.

    MyArraySpliterator<String> arraySplit = new MyArraySpliterator("hello","world");
    Spliterator<String> split = StreamSupport.stream(arraySplit,false)
                                            .map(s->"prefix-"+s)
                                            .spliterator();
    
    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);
    
    
    replayable1.forEach(System.out::println);
    arraySplit.reset();
    replayable2.forEach(System.out::println);
    
  3. Лучшее решение этой проблемы (на мой взгляд) состоит в создании новой копии любых сохраняющих состояние Spliterators, используемых в конвейере Stream, когда новые операторы вызываются для Stream. Это сложнее и сложнее реализовать, но если вы не возражаете против использования сторонних библиотек, cyclops-реагировать имеет реализацию Stream, которая делает именно это. (Раскрытие информации: я ведущий разработчик этого проекта.)

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

Это напечатает

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

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

1
John McClean