Почему Java-потоки отключены?
В отличие от С# IEnumerable
, где конвейер выполнения может выполняться столько раз, сколько мы хотим, в Java поток может быть "итерирован" только один раз.
Любой вызов операции терминала закрывает поток, что делает его непригодным.
Эта "особенность" отнимает много энергии.
Я предполагаю, что причина этого не техническая. Каковы были соображения дизайна за этим странным ограничением?
Изменить: чтобы продемонстрировать, о чем я говорю, рассмотрим следующую реализацию Quick-Sort в С#:
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!
Я даже не могу спросить поток, пустой ли он, не делая его непригодным.
Ответы
Ответ 1
У меня есть некоторые воспоминания из раннего дизайна API Streams, которые могут пролить свет на обоснование дизайна.
Еще в 2012 году мы добавляли lambdas на этот язык, и нам хотелось, чтобы набор операций, ориентированный на коллекции или "объемные данные", был запрограммирован с использованием lambdas, что облегчило бы parallelism. К этому моменту была создана идея ленивых операций цепочки. Мы также не хотели, чтобы промежуточные операции сохраняли результаты.
Основными проблемами, которые нам нужно было решить, были то, что объекты в цепочке выглядели в API и как они подключались к источникам данных. Источники часто были коллекциями, но мы также хотели поддерживать данные, поступающие из файла или сети, или данные, созданные "на лету", например, из генератора случайных чисел.
Было много влияний на существующую работу над дизайном. Среди наиболее влиятельных были Google Guava библиотека и библиотека коллекций Scala. (Если кто-то удивлен влиянием Гуавы, обратите внимание, что Кевин Бурриллион, ведущий разработчик Guava, был на JSR-335 Lambda.) В коллекциях Scala мы обнаружили, что этот разговор Мартина Одерского представляет особый интерес: Future- Доказательство Scala Коллекции: от Mutable to Persistent to Parallel. (Stanford EE380, 2011 1 июня.)
Наш прототип в то время был основан на Iterable
. Известные операции filter
, map
и т.д. Были методами расширения (по умолчанию) на Iterable
. Вызов один добавил операцию в цепочку и вернул еще один Iterable
. Операция терминала, такая как count
, вызывала бы iterator()
вверх по цепочке к источнику, а операции выполнялись в каждом итераторе этапа.
Так как это Iterables, вы можете вызвать метод iterator()
более одного раза. Что тогда должно произойти?
Если источником является коллекция, это в основном работает нормально. Коллекции Iterable, и каждый вызов iterator()
создает отдельный экземпляр Iterator, который не зависит от каких-либо других активных экземпляров, и каждый обходит коллекцию независимо. Отлично.
Теперь, если источник является одним выстрелом, например, чтение строк из файла? Возможно, первый Итератор должен получить все значения, но второй и последующие должны быть пустыми. Возможно, значения должны быть чередующимися между Итераторами. Или, может быть, каждый Итератор должен получать одинаковые значения. Тогда, что, если у вас есть два итератора, а один дальше впереди другого? Кому-то придется буферизовать значения во втором итераторе до тех пор, пока они не будут прочитаны. Хуже того, что, если вы получите один Итератор и прочитаете все значения, и только тогда получите второй Итератор. Откуда берутся ценности? Есть ли потребность в том, чтобы все они были забуферированы на всякий случай, если кто-то хочет второго Итератора?
Ясно, что использование нескольких итераторов над источником с одним выстрелом вызывает много вопросов. У нас не было хороших ответов. Мы хотели последовательного, предсказуемого поведения для того, что произойдет, если вы дважды назовете iterator()
. Это подтолкнуло нас к отказу от нескольких обходов, что сделало трубопроводы одним выстрелом.
Мы также наблюдали, как другие сталкиваются с этими проблемами. В JDK большинство Iterables - это коллекции или объекты, подобные коллекциям, которые допускают множественный обход. Он нигде не указан, но, похоже, было неписаное ожидание того, что Iterables допускают множественный обход. Заметным исключением является интерфейс NIO DirectoryStream. В его спецификацию включено это интересное предупреждение:
В то время как DirectoryStream расширяет Iterable, это не универсальный Iterable, поскольку он поддерживает только один Iterator; вызывая метод итератора для получения второго или последующего итератора, выдает исключение IllegalStateException.
[жирный шрифт в оригинале]
Это показалось необычным и неприятным, что мы не хотели создавать целую кучу новых Iterables, которые могли бы быть только один раз. Это оттолкнуло нас от использования Iterable.
Примерно в это же время появилась статья Брайан Гетц объяснил обоснование этого.
Как разрешить множественный обход для конвейеров, основанных на коллекции, но запретить его для конвейеров, основанных на сборке? Это непоследовательно, но это разумно. Если вы читаете значения из сети, вы, конечно, не сможете их переправить. Если вы хотите пройти их несколько раз, вам нужно явно их вставить в коллекцию.
Но позвольте исследовать возможность многократного прохождения от конвейеров, основанных на коллекциях. Скажем, вы сделали это:
Iterable<?> it = source.filter(...).map(...).filter(...).map(...);
it.into(dest1);
it.into(dest2);
(Операция into
теперь написана collect(toList())
.)
Если источником является коллекция, тогда первый вызов into()
создаст цепочку итераторов обратно к источнику, выполнит операции конвейера и отправит результаты в пункт назначения. Второй вызов into()
создаст другую цепочку итераторов и снова выполнит операции конвейера . Это, очевидно, не так, но у него есть эффект выполнения всех операций фильтра и карты второй раз для каждого элемента. Я думаю, что многие программисты были бы удивлены этим поведением.
Как я уже говорил выше, мы говорили с разработчиками Guava. Одна из интересных вещей, которые у них есть, - это Idea Graveyard, где они описывают функции, которые они решили не для реализации вместе с причины. Идея ленивых коллекций звучит довольно круто, но вот что они должны сказать об этом. Рассмотрим операцию List.filter()
, которая возвращает List
:
Самая большая проблема здесь заключается в том, что слишком много операций становятся дорогостоящими, линейными предложениями. Если вы хотите отфильтровать список и получить список назад, а не только коллекцию или итерацию, вы можете использовать ImmutableList.copyOf(Iterables.filter(list, predicate))
, который "указывает вперед", что он делает и насколько это дорого.
Чтобы взять конкретный пример, какова стоимость get(0)
или size()
в списке? Для обычно используемых классов, таких как ArrayList
, они O (1). Но если вы назовете один из них в лениво отфильтрованном списке, он должен запустить фильтр по списку поддержки, и внезапно эти операции - O (n). Хуже того, он должен пересекать список поддержки в каждой операции.
Это казалось нам слишком лень. Одно дело - настроить некоторые операции и отложить фактическое исполнение до тех пор, пока вы не перейдете к "Go". Это другое, чтобы настроить ситуацию таким образом, чтобы скрывать потенциально большую сумму пересчета.
Предлагая запретить потоки нелинейного или "без повторного использования", Paul Sandoz описал потенциальные последствия, позволяющие им порождать "неожиданные или запутывающие результаты". Он также упомянул, что параллельное исполнение сделает вещи еще более сложными. Наконец, я бы добавил, что операция конвейера с побочными эффектами приведет к сложным и неясным ошибкам, если операция была неожиданно выполнена несколько раз или, по крайней мере, в разное количество раз, чем ожидал программист. (Но Java-программисты не записывают лямбда-выражения с побочными эффектами, не так ли? DOYY??)
Итак, базовое обоснование дизайна API Java 8 Streams, допускающее одноразовый обход и требующий строго линейного (без ветвления) конвейера. Он обеспечивает последовательное поведение в нескольких источниках потока, он четко отделяет ленивые от нетерпеливых операций и обеспечивает простоту выполнения.
Что касается IEnumerable
, я далек от эксперта по С# и .NET, поэтому я был бы признателен за исправление (мягко), если я сделаю неверные выводы. Однако представляется, что IEnumerable
допускает, чтобы множественный обход вел себя по-разному с разными источниками; и он допускает ветвящуюся структуру вложенных операций IEnumerable
, что может привести к некоторой значительной перерасчеты. Хотя я понимаю, что разные системы делают разные компромиссы, это две характеристики, которые мы стремились избежать при разработке API-интерфейсов Java 8 Streams.
Пример quicksort, заданный OP, интересен, озадачен, и мне жаль говорить, что это ужасно. Вызов QuickSort
принимает IEnumerable
и возвращает IEnumerable
, поэтому сортировка фактически не выполняется до тех пор, пока не пройден последний IEnumerable
. Однако, по-видимому, этот вызов создает древовидную структуру IEnumerables
, которая отражает секционирование, которое будет выполнять quicksort, без его фактического выполнения. (Это, в конце концов, ленивое вычисление.) Если источник имеет N элементов, дерево будет N элементов шириной в самом широком, и это будет lg (N) уровней глубоко.
Мне кажется - и еще раз, я не эксперт на С# или .NET - это приведет к тому, что некоторые безобидные вызовы, такие как выбор поворота через ints.First()
, будут более дорогими, чем они смотреть. На первом уровне, конечно, это O (1). Но рассмотрите раздел, расположенный глубоко в дереве, на правом краю. Чтобы вычислить первый элемент этого раздела, нужно пройти весь источник, операцию O (N). Но так как вышеперечисленные разделы являются ленивыми, они должны быть пересчитаны, что требует O (lg N) сравнений. Таким образом, выбирая стержень будет представлять собой O (N Л.Г. N), операция, которая дороже всего рода.
Но мы фактически не сортируем, пока не пройдем возвращенный IEnumerable
. В стандартном алгоритме быстрой сортировки каждый уровень разбиения удваивает количество разделов. Каждый раздел имеет лишь половину размера, поэтому каждый уровень остается на уровне O (N). Дерево разделов - O (lg N) высокое, поэтому общая работа - O (N lg N).
С деревом ленивых IEnumerables в нижней части дерева есть N разделов. Для вычисления каждого раздела требуется обход N элементов, каждый из которых требует lg (N) сравнения дерева. Чтобы вычислить все разделы в нижней части дерева, необходимо выполнить сравнение O (N ^ 2 lg N).
(Это правильно? Я с трудом могу поверить в это. Кто-нибудь, пожалуйста, проверьте это для меня.)
В любом случае, действительно здорово, что IEnumerable
можно использовать таким образом, чтобы создать сложные структуры вычислений. Но если это увеличит вычислительную сложность настолько, насколько я думаю, что это так, казалось бы, программирование таким образом - это то, чего следует избегать, если не быть очень осторожным.
Ответ 2
Фон
Пока вопрос кажется простым, фактический ответ требует некоторого фона, чтобы иметь смысл. Если вы хотите перейти к выводу, прокрутите вниз...
Выберите точку сравнения - Основные функции
Используя основные понятия, концепция С# IEnumerable
более тесно связана с Java Iterable
, которая способна создать как можно больше Итераторы, как вы хотите. IEnumerables
создать IEnumerators
. Java Iterable
create Iterators
История каждой концепции похожа на то, что как IEnumerable
, так и Iterable
имеют базовую мотивацию, позволяющую циклически перебирать элементы для каждого набора данных. Это упрощение, поскольку они оба позволяют больше, чем просто, и они также пришли на этот этап с помощью разных прогрессий, но это значительная общая функция независимо.
Давайте сравним эту функцию: на обоих языках, если класс реализует IEnumerable
/Iterable
, тогда этот класс должен реализовать хотя бы один метод (для С#, it GetEnumerator
и для Java it iterator()
). В каждом случае экземпляр, возвращаемый из этого (IEnumerator
/Iterator
), позволяет получить доступ к текущему и последующим элементам данных. Эта функция используется в синтаксисе для каждого языка.
Выберите точку сравнения - Расширенные функции
IEnumerable
в С# был расширен, чтобы разрешить ряд других языковых функций (в основном связанных с Linq). Добавленные функции включают в себя выбор, прогнозы, агрегации и т.д. Эти расширения имеют сильную мотивацию от использования в теории множеств, подобно понятиям SQL и Relational Database.
Java 8 также добавила функциональность, чтобы обеспечить возможность функционального программирования с использованием Streams и Lambdas. Обратите внимание, что потоки Java 8 не в первую очередь мотивированы теорией множеств, а функциональным программированием. Несмотря на это, существует много параллелей.
Итак, это второй пункт. Усовершенствования, сделанные для С#, были реализованы как усовершенствование концепции IEnumerable
. Однако на Java усовершенствования были реализованы путем создания новых базовых концепций Lambdas и Streams, а затем также создания относительно тривиального способа преобразования из Iterators
и Iterables
в потоки и наоборот.
Итак, сравнение IEnumerable с концепцией Java Stream неполно. Вам нужно сравнить его с объединенным API Streams and Collections в Java.
В Java потоки не совпадают с Iterables или Iterators
Потоки не предназначены для решения проблем так же, как итераторы:
- Итераторы - это способ описания последовательности данных.
- Потоки - это способ описания последовательности преобразований данных.
С помощью Iterator
вы получаете значение данных, обрабатываете его, а затем получаете другое значение данных.
С Streams вы объединяете последовательность функций, затем вы подаете входное значение в поток и получаете выходное значение из комбинированной последовательности. Обратите внимание, что в терминах Java каждая функция инкапсулируется в один экземпляр Stream
. API Streams позволяет связать последовательность экземпляров Stream
таким образом, что цепочки последовательности выражений преобразования.
Чтобы завершить концепцию Stream
, вам нужен источник данных для подачи потока и функция терминала, которая потребляет поток.
То, как вы передаете значения в поток, на самом деле может быть от Iterable
, но сама последовательность Stream
не является Iterable
, это составная функция.
A Stream
также должен быть ленивым, в том смысле, что он работает только тогда, когда вы запрашиваете у него значение.
Обратите внимание на эти существенные предположения и особенности потоков:
- A
Stream
в Java - это механизм преобразования, он преобразует элемент данных в одном состоянии, находясь в другом состоянии.
Потоки - не имеют представления о порядке или позиции данных, просто преобразуют все, что они просят.
- потоки могут поставляться с данными из многих источников, включая другие потоки, итераторы, Iterables, Collections,
- вы не можете "reset" поток, который будет "перепрограммировать преобразование". Сброс источника данных, вероятно, вы хотите.
- существует логически только 1 элемент данных "в полете" в потоке в любое время (если поток не является параллельным потоком, в этот момент в потоке есть 1 элемент). Это не зависит от источника данных, который может иметь больше, чем текущие элементы "готовы" к потоку, или сборщик потока, который может потребоваться для агрегирования и сокращения нескольких значений.
- Потоки могут быть несвязаны (бесконечны), ограничены только источником данных или коллекционером (что также может быть бесконечным).
- Потоки "цепочки", выход фильтрации одного потока - это другой поток. Значения, вводимые и преобразованные потоком, в свою очередь могут быть переданы другому потоку, который выполняет другое преобразование. Данные в преобразованном состоянии перетекают из одного потока в другой. Вам не нужно вмешиваться и извлекать данные из одного потока и подключать его к следующему.
Сравнение С#
Если вы считаете, что поток Java - это всего лишь часть системы поставки, потока и сбора, и что потоки и итераторы часто используются вместе с коллекциями, то неудивительно, что с этим трудно связать концепции, которые почти все встроены в одну концепцию IEnumerable
в С#.
Части IEnumerable (и близкие связанные понятия) очевидны во всех концепциях Java Iterator, Iterable, Lambda и Stream.
Есть небольшие вещи, которые могут сделать Java-концепции, которые сложнее в IEnumerable и наоборот.
Заключение
- Здесь нет проблем с дизайном, просто проблема совпадения понятий между языками.
- Потоки решают проблемы по-другому.
- Потоки добавляют функциональность Java (они добавляют другой способ делать что-то, они не отвлекают функциональность)
Добавление потоков дает вам больше возможностей при решении проблем, которые справедливо классифицировать как "усиление власти", а не "сокращение", "удаление" или "ограничение".
Почему потоки Java отключены?
Этот вопрос ошибочен, потому что потоки представляют собой последовательности функций, а не данные. В зависимости от источника данных, который передает поток, вы можете reset источник данных и подавать тот же или другой поток.
В отличие от С# IEnumerable, где конвейер выполнения может выполняться столько раз, сколько требуется, в Java поток можно "повторить" только один раз.
Сравнение IEnumerable
с a Stream
ошибочно. Контекст, который вы используете, чтобы сказать IEnumerable
, может выполняться столько раз, сколько вы хотите, лучше всего по сравнению с Java Iterables
, который можно повторять столько раз, сколько вы хотите. Java Stream
представляет собой подмножество понятия IEnumerable
, а не подмножество, которое поставляет данные, и, следовательно, не может быть "повторено".
Любой вызов операции терминала закрывает поток, что делает его непригодным. Эта "особенность" отнимает много энергии.
Первое утверждение верно в некотором смысле. Утверждение "отнять власть" - нет. Вы по-прежнему сравниваете Streams it IEnumerables. Операция терминала в потоке похожа на предложение "break" в цикле for. Вы всегда можете иметь другой поток, если хотите, и если вы можете перенаправить нужные данные. Опять же, если вы считаете, что IEnumerable
больше похож на Iterable
, для этого оператора Java делает это просто отлично.
Я предполагаю, что причина этого не техническая. Каковы были конструктивные соображения за этим странным ограничением?
Причина техническая, и по той простой причине, что Stream - это подмножество того, что кажется. Подмножество потока не контролирует подачу данных, поэтому вы должны reset поставлять, а не поток. В этом контексте это не так странно.
Пример QuickSort
В вашем примере quicksort есть подпись:
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
) и потоковые концепции в разных точках, а в С# эти две "личности" могут быть выражены только в IEnumerable
. Кроме того, хотя я использовал List
в качестве базового типа, я мог бы использовать более общий Collection
, и с небольшим преобразованием итератора в поток я мог бы использовать еще более общий Iterable
Ответ 3
Stream
построены вокруг Spliterator
, которые являются объектами с изменением состояния, изменяемыми. У них нет действия "reset", и на самом деле, требуя поддержки такого действия перемотки, "будет отнимать много сил". Как Random.ints()
должен обрабатывать такой запрос?
С другой стороны, для Stream
, которые имеют обратимое происхождение, легко построить эквивалентный Stream
, который будет использоваться снова. Просто поставьте шаги, чтобы построить Stream
в метод многократного использования. Имейте в виду, что повторение этих шагов не является дорогостоящей операцией, так как все эти шаги - ленивые операции; фактическая работа начинается с операции терминала и в зависимости от фактической операции терминала может выполняться совершенно другой код.
Вы, писатель такого метода, могли бы указать, что подразумевает вызов метода в два раза: воспроизводит ли он точно такую же последовательность, как потоки, созданные для немодифицированного массива или коллекции, или создает поток с аналогичной семантикой, но различные элементы, такие как поток случайных ints или поток консольных входных строк и т.д.
Кстати, во избежание путаницы терминальная операция потребляет Stream
, которая отличается от закрытия Stream
, поскольку вызов close()
в потоке (что требуется для потоков, имеющих связанные ресурсы, например, на Files.lines()
).
Похоже, что большая путаница проистекает из ошибочного сравнения IEnumerable
с Stream
. IEnumerable
представляет возможность предоставить фактический IEnumerator
, поэтому его как Iterable
в Java. Напротив, a Stream
является своего рода итератором и сопоставим с IEnumerator
, поэтому не следует утверждать, что такой тип данных можно использовать несколько раз в .NET, поддержка IEnumerator.Reset
является необязательной. Приведенные здесь примеры скорее используют тот факт, что IEnumerable
можно использовать для извлечения нового IEnumerator
и который работает с Javas Collection
; вы можете получить новый Stream
. Если разработчики Java решили добавить операции Stream
в Iterable
напрямую, а промежуточные операции возвращают другой Iterable
, это было действительно сопоставимо, и оно могло работать одинаково.
Однако разработчики решили против него, и решение обсуждается в этом вопросе. Самым большим моментом является путаница в отношении нетерпеливых операций с коллекциями и ленивых операций Stream. Посмотрев на .NET API, я (да, лично) считаю это обоснованным. Хотя выглядит разумно, глядя только на IEnumerable
, конкретная коллекция будет иметь множество методов, управляющих сборкой напрямую, и множество методов, возвращающих ленивый IEnumerable
, в то время как конкретный характер метода не всегда интуитивно узнаваем. Самый худший пример, который я нашел (в течение нескольких минут, на который я смотрел), List.Reverse()
, имя которого точно совпадает с именем унаследованного (is это правильный конец для методов расширения?) Enumerable.Reverse()
, имея совершенно противоречивое поведение.
Конечно, это два разных решения. Первый должен сделать Stream
тип, отличный от Iterable
/Collection
, а второй - сделать Stream
своего рода одноразовый итератор, а не другой тип итерации. Но это решение было принято вместе, и это может быть так, что разделение этих двух решений никогда не рассматривалось. Он не был создан с учетом того, что он сопоставим с .NET.
Фактическое решение по проектированию API состояло в том, чтобы добавить улучшенный тип итератора, Spliterator
. Spliterator
может быть предоставлено старым Iterable
(каким образом они были модифицированы) или полностью новыми реализациями. Затем Stream
был добавлен как высокоуровневый интерфейс на довольно низкий уровень Spliterator
s. Это оно. Вы можете обсудить, будет ли другой дизайн лучше, но он не продуктивен, он не изменится, учитывая то, как они сейчас разрабатываются.
Существует еще один аспект реализации, который вы должны рассмотреть. Stream
не являются неизменяемыми структурами данных. Каждая промежуточная операция может возвращать новый экземпляр Stream
, инкапсулирующий старый, но он может также управлять своим собственным экземпляром и возвращать себя (что не исключает выполнения обоих операций для одной и той же операции). Общеизвестными примерами являются операции типа parallel
или unordered
, которые не добавляют другого шага, а манипулируют всем конвейером). Наличие такой изменяемой структуры данных и попытки повторного использования (или, что еще хуже, использование ее несколько раз в одно и то же время) не играют хорошо...
Для полноты, вот ваш пример quicksort, переведенный в Java Stream
API. Это показывает, что на самом деле это не "отнимает много сил".
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());
}
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());
}
Ответ 4
Я думаю, что между этими двумя очень мало различий, когда вы достаточно внимательно смотрите.
На нем face, 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, и нет пути к reset к началу. Если вы хотите снова итерации по номерам, вам нужно будет создать другой экземпляр перечислителя.
Чтобы лучше проиллюстрировать, что 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-поток).
Кроме того, что означает, что ваш счетчик на основе перечислимого типа относится к RandomNumberStream
?
Заключение
Итак, самое большое различие заключается в том, что .NET позволяет повторно использовать IEnumerable
, неявно создавая новый IEnumerator
в фоновом режиме, когда ему потребуется доступ к элементам в последовательности.
Это неявное поведение часто полезно (и "мощно", как вы заявляете), потому что мы можем многократно перебирать коллекцию.
Но иногда это неявное поведение может вызвать проблемы. Если ваш источник данных не является статичным или является дорогостоящим для доступа (например, база данных или веб-сайт), тогда многие предположения о IEnumerable
должны быть отброшены; повторное использование не так прямолинейно
Ответ 5
В 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
, имеет статус stateful и сохраняет текущую позицию. Когда мы воспроизводим этот Stream
, мы начинаем снова в конце.
У нас есть несколько вариантов решения этой проблемы:
-
Мы могли бы использовать способ создания без сохранения Stream
, например Stream#generate()
. Нам нужно было бы управлять внешним состоянием в нашем собственном коде и reset между 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);
-
Другое (немного лучшее, но не идеальное) решение для этого - написать собственный ArraySpliterator
(или аналогичный источник Stream
), который включает в себя некоторую емкость для reset текущего счетчика. Если бы мы использовали его для создания 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);
-
Лучшим решением этой проблемы (на мой взгляд) является создание новой копии любого состояния Spliterator
, используемого в конвейере Stream
при вызове новых операторов в Stream
. Это сложнее и сложнее реализовать, но если вы не возражаете против использования сторонних библиотек, cyclops-react имеет реализацию 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
как ожидалось.