Java 8 Stream, получая головку и хвост
В Java 8 представлен класс Stream, который напоминает Scala Stream, мощный используя ленивую конструкцию, с помощью которой можно сделать что-то вроде этого очень кратко:
def from(n: Int): Stream[Int] = n #:: from(n+1)
def sieve(s: Stream[Int]): Stream[Int] = {
s.head #:: sieve(s.tail filter (_ % s.head != 0))
}
val primes = sieve(from(2))
primes takeWhile(_ < 1000) print // prints all primes less than 1000
Я задавался вопросом, возможно ли это сделать в Java 8, поэтому я написал что-то вроде этого:
IntStream from(int n) {
return IntStream.iterate(n, m -> m + 1);
}
IntStream sieve(IntStream s) {
int head = s.findFirst().getAsInt();
return IntStream.concat(IntStream.of(head), sieve(s.skip(1).filter(n -> n % head != 0)));
}
IntStream primes = sieve(from(2));
Довольно просто, но он создает java.lang.IllegalStateException: stream has already been operated upon or closed
, потому что findFirst()
и skip()
являются терминальными операциями на Stream
, которые могут выполняться только один раз.
Мне не нужно использовать поток дважды, потому что все, что мне нужно, это первое число в потоке, а остальное - как другой поток, т.е. эквивалент Scala Stream.head
и Stream.tail
. Есть ли способ в Java 8 Stream
, который я могу использовать для достижения этого?
Спасибо.
Ответы
Ответ 1
Даже если у вас не возникла проблема с тем, что вы не можете разделить IntStream
, ваш код не работал, потому что вы вызываете свой метод sieve
рекурсивно, а не лениво. Таким образом, у вас была бесконечная рекурсия, прежде чем вы могли запросить полученный результирующий поток для первого значения.
Разделение IntStream s
на голову и хвост IntStream
(который еще не был использован):
PrimitiveIterator.OfInt it = s.iterator();
int head = it.nextInt();
IntStream tail = IntStream.generate(it::next).filter(i -> i % head != 0);
В этом месте вам нужна конструкция вызова sieve
на хвосте лениво. Stream
не предусматривает этого; concat
ожидает, что существующие экземпляры потока в качестве аргументов и вы не можете сконструировать поток, вызывающий sieve
лениво с помощью выражения лямбда, поскольку ленивое создание работает с изменчивым состоянием, только эти лямбда-выражения не поддерживают. Если у вас нет реализации библиотеки, скрывающей изменчивое состояние, вы должны использовать изменяемый объект. Но как только вы примете требование изменчивого состояния, решение может быть еще проще, чем ваш первый подход:
IntStream primes = from(2).filter(i -> p.test(i)).peek(i -> p = p.and(v -> v % i != 0));
IntPredicate p = x -> true;
IntStream from(int n)
{
return IntStream.iterate(n, m -> m + 1);
}
Это будет рекурсивно создавать фильтр, но в конце концов не имеет значения, создаете ли вы дерево IntPredicate
или дерево IntStream
(например, с вашим подходом IntStream.concat
, если он работает). Если вам не нравится изменяемое поле экземпляра фильтра, вы можете скрыть его во внутреннем классе (но не в выражении лямбда...).
Ответ 2
Моя StreamEx теперь headTail()
, которая решает проблему:
public static StreamEx<Integer> sieve(StreamEx<Integer> input) {
return input.headTail((head, tail) ->
sieve(tail.filter(n -> n % head != 0)).prepend(head));
}
Метод headTail
принимает BiFunction
, который будет выполняться не более одного раза во время выполнения операции терминала. Таким образом, эта реализация является ленивой: она ничего не вычисляет до начала обхода и вычисляет только столько простых чисел, сколько было запрошено. BiFunction
получает первый элемент потока head
и поток остальных элементов tail
и может изменять tail
любым способом. Вы можете использовать его с предопределенным вводом:
sieve(IntStreamEx.range(2, 1000).boxed()).forEach(System.out::println);
Но бесконечный поток также работает
sieve(StreamEx.iterate(2, x -> x+1)).takeWhile(x -> x < 1000)
.forEach(System.out::println);
// Not the primes till 1000, but 1000 first primes
sieve(StreamEx.iterate(2, x -> x+1)).limit(1000).forEach(System.out::println);
Существует также альтернативное решение с использованием headTail
и конкатенации предикатов:
public static StreamEx<Integer> sieve(StreamEx<Integer> input, IntPredicate isPrime) {
return input.headTail((head, tail) -> isPrime.test(head)
? sieve(tail, isPrime.and(n -> n % head != 0)).prepend(head)
: sieve(tail, isPrime));
}
sieve(StreamEx.iterate(2, x -> x+1), i -> true).limit(1000).forEach(System.out::println);
Интересно сравнить рекурсивные решения: сколько простых чисел они способны генерировать.
@Решение Джона МакКлина (StreamUtils
)
Решения Джона МакКлина не ленивы: вы не можете кормить их бесконечным потоком. Поэтому я просто обнаружил методом проб и ошибок максимальную допустимую верхнюю границу (17793
) (после этого возникает StackOverflowError):
public void sieveTest(){
sieve(IntStream.range(2, 17793).boxed()).forEach(System.out::println);
}
@Решение Джона МакКлина (Streamable
)
public void sieveTest2(){
sieve(Streamable.range(2, 39990)).forEach(System.out::println);
}
Увеличение верхнего предела выше 39990
приводит к StackOverflowError.
Решение @frhack (LazySeq
)
LazySeq<Integer> ints = integers(2);
LazySeq primes = sieve(ints); // sieve method from @frhack answer
primes.forEach(p -> System.out.println(p));
Результат: застрял после простого числа = 53327
с огромным распределением кучи и сбором мусора, занимающим более 90%. Потребовалось несколько минут, чтобы перейти с 53323 до 53327, поэтому ждать больше кажется непрактичным.
решение @vidi
Prime.stream().forEach(System.out::println);
Результат: StackOverflowError после простого числа = 134417
.
Мое решение (StreamEx)
sieve(StreamEx.iterate(2, x -> x+1)).forEach(System.out::println);
Результат: StackOverflowError после простого числа = 236167
.
Решение @frhack (rxjava
)
Observable<Integer> primes = Observable.from(()->primesStream.iterator());
primes.forEach((x) -> System.out.println(x.toString()));
Результат: StackOverflowError после простого числа = 367663
.
Решение @Holger
IntStream primes=from(2).filter(i->p.test(i)).peek(i->p=p.and(v->v%i!=0));
primes.forEach(System.out::println);
Результат: StackOverflowError после простого числа = 368089
.
Мое решение (StreamEx с конкатенацией предикатов)
sieve(StreamEx.iterate(2, x -> x+1), i -> true).forEach(System.out::println);
Результат: StackOverflowError после простого числа = 368287
.
Итак, три решения, включающие конкатенацию предикатов, выигрывают, потому что каждое новое условие добавляет только еще 2 кадра кадров. Я думаю, разница между ними маргинальна и не должна считаться определяющей победителем. Однако мне больше нравится мое первое решение StreamEx, так как оно больше похоже на код Scala.
Ответ 3
Вы можете реализовать его так:
static <T> Tuple2<Optional<T>, Seq<T>> splitAtHead(Stream<T> stream) {
Iterator<T> it = stream.iterator();
return tuple(it.hasNext() ? Optional.of(it.next()) : Optional.empty(), seq(it));
}
В приведенном выше примере Tuple2
и Seq
являются типами, заимствованными из jOOλ, библиотеки, которую мы разработали для jOOQ тесты интеграции. Если вы не хотите каких-либо дополнительных зависимостей, вы можете также реализовать их самостоятельно:
class Tuple2<T1, T2> {
final T1 v1;
final T2 v2;
Tuple2(T1 v1, T2 v2) {
this.v1 = v1;
this.v2 = v2;
}
static <T1, T2> Tuple2<T1, T2> tuple(T1 v1, T2 v2) {
return new Tuple<>(v1, v2);
}
}
static <T> Tuple2<Optional<T>, Stream<T>> splitAtHead(Stream<T> stream) {
Iterator<T> it = stream.iterator();
return tuple(
it.hasNext() ? Optional.of(it.next()) : Optional.empty,
StreamSupport.stream(Spliterators.spliteratorUnknownSize(
it, Spliterator.ORDERED
), false)
);
}
Ответ 4
В приведенном ниже решении не выполняются мутации состояния, за исключением деконструирования потока головы/хвоста.
Лязкость получается с помощью IntStream.iterate. Класс Prime используется для сохранения состояния генератора
import java.util.PrimitiveIterator;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class Prime {
private final IntStream candidates;
private final int current;
private Prime(int current, IntStream candidates)
{
this.current = current;
this.candidates = candidates;
}
private Prime next()
{
PrimitiveIterator.OfInt it = candidates.filter(n -> n % current != 0).iterator();
int head = it.next();
IntStream tail = IntStream.generate(it::next);
return new Prime(head, tail);
}
public static Stream<Integer> stream() {
IntStream possiblePrimes = IntStream.iterate(3, i -> i + 1);
return Stream.iterate(new Prime(2, possiblePrimes), Prime::next)
.map(p -> p.current);
}
}
Использование будет следующим:
Stream<Integer> first10Primes = Prime.stream().limit(10)
Ответ 5
Если вы не возражаете против использования сторонних библиотек cyclops-streams, я написал в библиотеке ряд потенциальных решений.
Класс StreamUtils имеет большое количество статических методов для работы непосредственно с java.util.stream.Streams
включая headAndTail
.
HeadAndTail<Integer> headAndTail = StreamUtils.headAndTail(Stream.of(1,2,3,4));
int head = headAndTail.head(); //1
Stream<Integer> tail = headAndTail.tail(); //Stream[2,3,4]
Класс Streamable представляет воспроизводимый Stream
и работает, создавая ленивую промежуточную структуру данных кэширования. Поскольку он кэшируется и возвращается, головка и хвост могут быть реализованы напрямую и отдельно.
Streamable<Integer> replayable= Streamable.fromStream(Stream.of(1,2,3,4));
int head = repayable.head(); //1
Stream<Integer> tail = replayable.tail(); //Stream[2,3,4]
cyclops-streams также предоставляет последовательное расширение Stream
, которое, в свою очередь, расширяет jOOλ и имеет как Tuple
(от jOOλ), так и решения для объекта (HeadAndTail) для извлечения головы и хвоста.
SequenceM.of(1,2,3,4)
.splitAtHead(); //Tuple[1,SequenceM[2,3,4]
SequenceM.of(1,2,3,4)
.headAndTail();
Обновление по запросу Tagir → Java-версия сита Scala с использованием SequenceM
public void sieveTest(){
sieve(SequenceM.range(2, 1_000)).forEach(System.out::println);
}
SequenceM<Integer> sieve(SequenceM<Integer> s){
return s.headAndTailOptional().map(ht ->SequenceM.of(ht.head())
.appendStream(sieve(ht.tail().filter(n -> n % ht.head() != 0))))
.orElse(SequenceM.of());
}
И еще одна версия через Streamable
public void sieveTest2(){
sieve(Streamable.range(2, 1_000)).forEach(System.out::println);
}
Streamable<Integer> sieve(Streamable<Integer> s){
return s.size()==0? Streamable.of() : Streamable.of(s.head())
.appendStreamable(sieve(s.tail()
.filter(n -> n % s.head() != 0)));
}
Примечание. Ни Streamable
of SequenceM
не имеет пустой реализации - следовательно, проверка размера для Streamable
и использование headAndTailOptional
.
Наконец, версия с использованием plain java.util.stream.Stream
import static com.aol.cyclops.streams.StreamUtils.headAndTailOptional;
public void sieveTest(){
sieve(IntStream.range(2, 1_000).boxed()).forEach(System.out::println);
}
Stream<Integer> sieve(Stream<Integer> s){
return headAndTailOptional(s).map(ht ->Stream.concat(Stream.of(ht.head())
,sieve(ht.tail().filter(n -> n % ht.head() != 0))))
.orElse(Stream.of());
}
Другое обновление - ленивая итерация на основе версии @Holger с использованием объектов, а не примитивов (обратите внимание, что возможна и примитивная версия)
final Mutable<Predicate<Integer>> predicate = Mutable.of(x->true);
SequenceM.iterate(2, n->n+1)
.filter(i->predicate.get().test(i))
.peek(i->predicate.mutate(p-> p.and(v -> v%i!=0)))
.limit(100000)
.forEach(System.out::println);
Ответ 6
Чтобы получить голову и хвост, вам нужна реализация Lazy Stream. Java 8 stream или RxJava не подходят.
Вы можете использовать, например, LazySeq следующим образом.
Ленивая последовательность всегда пересекается с самого начала, используя очень дешевую разложение первого/остаточного (head() и tail())
LazySeq реализует интерфейс java.util.List, поэтому его можно использовать в разнообразие мест. Кроме того, он также реализует усовершенствования Java 8 для коллекции, а именно потоки и коллекторы
package com.company;
import com.nurkiewicz.lazyseq.LazySeq;
public class Main {
public static void main(String[] args) {
LazySeq<Integer> ints = integers(2);
LazySeq primes = sieve(ints);
primes.take(10).forEach(p -> System.out.println(p));
}
private static LazySeq<Integer> sieve(LazySeq<Integer> s) {
return LazySeq.cons(s.head(), () -> sieve(s.filter(x -> x % s.head() != 0)));
}
private static LazySeq<Integer> integers(int from) {
return LazySeq.cons(from, () -> integers(from + 1));
}
}
Ответ 7
Вот еще один рецепт, предложенный Хольгером.
Он использует RxJava, чтобы добавить возможность использовать метод take (int) и многие другие.
package com.company;
import rx.Observable;
import java.util.function.IntPredicate;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
final IntPredicate[] p={(x)->true};
IntStream primesStream=IntStream.iterate(2,n->n+1).filter(i -> p[0].test(i)).peek(i->p[0]=p[0].and(v->v%i!=0) );
Observable primes = Observable.from(()->primesStream.iterator());
primes.take(10).forEach((x) -> System.out.println(x.toString()));
}
}
Ответ 8
Здесь представлено много интересных предложений, но если кому-то нужно решение без зависимостей от сторонних библиотек, я придумаю следующее:
import java.util.AbstractMap;
import java.util.Optional;
import java.util.Spliterators;
import java.util.stream.StreamSupport;
/**
* Splits a stream in the head element and a tail stream.
* Parallel streams are not supported.
*
* @param stream Stream to split.
* @param <T> Type of the input stream.
* @return A map entry where {@link Map.Entry#getKey()} contains an
* optional with the first element (head) of the original stream
* and {@link Map.Entry#getValue()} the tail of the original stream.
* @throws IllegalArgumentException for parallel streams.
*/
public static <T> Map.Entry<Optional<T>, Stream<T>> headAndTail(final Stream<T> stream) {
if (stream.isParallel()) {
throw new IllegalArgumentException("parallel streams are not supported");
}
final Iterator<T> iterator = stream.iterator();
return new AbstractMap.SimpleImmutableEntry<>(
iterator.hasNext() ? Optional.of(iterator.next()) : Optional.empty(),
StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false)
);
}
Ответ 9
Если вы хотите получить голову потока, просто:
IntStream.range(1, 5).first();
Если вы хотите получить хвост потока, просто:
IntStream.range(1, 5).skip(1);
Если вы хотите получить как голову, так и хвост, просто:
IntStream s = IntStream.range(1, 5);
int head = s.head();
IntStream tail = s.tail();
Если вы хотите найти простое число, просто:
LongStream.range(2, n)
.filter(i -> LongStream.range(2, (long) Math.sqrt(i) + 1).noneMatch(j -> i % j == 0))
.forEach(N::println);
Если вы хотите узнать больше, перейдите, чтобы получить AbacusUtil
Декларация: я разработчик AbacusUtil.