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.