Обмежте потік присудком


187

Чи існує потокова операція Java 8, яка обмежує (потенційно нескінченну), Streamпоки перший елемент не зможе відповідати присудку?

У Java 9 ми можемо використовувати, takeWhileяк у наведеному нижче прикладі, для друку всіх цифр менше 10.

IntStream
    .iterate(1, n -> n + 1)
    .takeWhile(n -> n < 10)
    .forEach(System.out::println);

Оскільки в Java 8 такої операції немає, який найкращий спосіб її здійснення в цілому?


1
Можливо , корисна інформація по адресою: stackoverflow.com/q/19803058/248082
nobeh


Мені цікаво, як архітектори могли коли-небудь пройти через "для чого ми насправді можемо це використати ", не наштовхуючись на цю корисну скриньку. Що стосується потоків Java 8, фактично корисні лише існуючі структури даних: - /
Thorbjørn Ravn Andersen


З Java 9 писати було б простішеIntStream.iterate(1, n->n<10, n->n+1).forEach(System.out::print);
Марк Дзабель

Відповіді:


81

Така операція повинна бути можливою з Java 8 Stream, але це не обов'язково може бути виконано ефективно - наприклад, не можна обов'язково паралелізовувати таку операцію, оскільки потрібно дивитися на елементи по порядку.

API не забезпечує простий спосіб зробити це, але, мабуть, найпростіший спосіб - це взяти Stream.iterator(), обернути, Iteratorщоб мати реалізацію "взяти під час", а потім повернутися до a, Spliteratorа потім a Stream. Або - можливо - заверніть Spliterator, хоча це реально більше не може бути розділене в цій реалізації.

Ось неперевірена реалізація takeWhileщодо Spliterator:

static <T> Spliterator<T> takeWhile(
    Spliterator<T> splitr, Predicate<? super T> predicate) {
  return new Spliterators.AbstractSpliterator<T>(splitr.estimateSize(), 0) {
    boolean stillGoing = true;
    @Override public boolean tryAdvance(Consumer<? super T> consumer) {
      if (stillGoing) {
        boolean hadNext = splitr.tryAdvance(elem -> {
          if (predicate.test(elem)) {
            consumer.accept(elem);
          } else {
            stillGoing = false;
          }
        });
        return hadNext && stillGoing;
      }
      return false;
    }
  };
}

static <T> Stream<T> takeWhile(Stream<T> stream, Predicate<? super T> predicate) {
   return StreamSupport.stream(takeWhile(stream.spliterator(), predicate), false);
}

8
Теоретично, паралелізувати takeWhile з предикатом без громадянства легко. Оцініть умову паралельними партіями (припустимо, що присудок не кидає або не має побічного ефекту, якщо виконується кілька додаткових разів). Проблема полягає в тому, що це відбувається в умовах рекурсивної декомпозиції (fork / join Framework), яку використовують Streams. Дійсно, це Потоки, які жахливо неефективні.
Олександр Дубінський

91
Потоки були б набагато кращими, якби вони не були настільки зайняті автоматичним паралелізмом. Паралелізм потрібен лише у крихітній частині місць, де можна використовувати потоки. Крім того, якби Oracle так піклувався про перфоманс, вони могли б зробити JVM JIT автовекторизацією і отримати набагато більшу прискореність виконання, не турбуючи розробників. Тепер автоматичний паралелізм зроблено правильно.
Олександр Дубінський

Цю відповідь слід оновити зараз, коли виходить Java 9.
Radiodef

4
Ні, @Radiodef. Питання задається спеціально для рішення Java 8.
Ренато Назад

145

Операції takeWhileта dropWhileдодані до JDK 9. Ваш приклад коду

IntStream
    .iterate(1, n -> n + 1)
    .takeWhile(n -> n < 10)
    .forEach(System.out::println);

буде поводитись так, як ви цього очікували при компіляції та запуску під JDK 9.

JDK 9 було випущено. Доступний для завантаження тут: http://jdk.java.net/9/


3
Пряме посилання на документи для попереднього перегляду для потоку JDK9, з takeWhile/ dropWhile: download.java.net/jdk9/docs/api/java/util/stream/Stream.html
милі

1
Чи є причина , чому вони називаються takeWhileі , dropWhileа не limitWhileта skipWhile, для забезпечення узгодженості з існуючими API?
Лукас Едер

10
@LukasEder takeWhileі dropWhileдосить поширені, зустрічаються в Scala, Python, Groovy, Ruby, Haskell і Clojure. Асиметрія з skipі limitприкрою. Може бути , skipі limitварто було б назвати dropі take, але це не так інтуїтивно , якщо ви вже знайомі з Haskell.
Стюарт відзначає

3
@StuartMarks: Я розумію, що це dropXXXі takeXXXпопулярніші терміни, але я особисто можу жити з більш SQL-esque limitXXXі skipXXX. Я вважаю цю нову асиметрію набагато заплутанішою, ніж індивідуальний вибір термінів ... :) (btw: Scala також є drop(int)і take(int))
Лукас Едер

1
так, дозвольте мені просто перейти на Jdk 9 у виробництві. Багато розробників досі перебувають на Jdk8, така функція повинна була бути включена до потоків із самого початку.
wilmol

50

allMatch()є функцією короткого замикання, тому ви можете використовувати її для припинення обробки. Основним недоліком є ​​те, що ви повинні зробити свій тест двічі: раз, щоб побачити, чи варто ви його обробляти, і знову, щоб побачити, чи продовжувати працювати.

IntStream
    .iterate(1, n -> n + 1)
    .peek(n->{if (n<10) System.out.println(n);})
    .allMatch(n->n < 10);

5
Спочатку це здалося мені неінтуїтивним (даючи назву методу), але документи підтверджують, що Stream.allMatch()це операція короткого замикання . Так це завершиться навіть на нескінченному потоці, як IntStream.iterate(). Звичайно, в ретроспективі, це розумна оптимізація.
Бейлі Паркер

3
Це акуратно, але я не думаю, що він дуже добре повідомляє, що його намір є органом peek. Якщо я зіткнувся з цим у наступному місяці, я би замислився хвилину, щоб замислитися, чому програміст перед мною перевіряв, якщо allMatchі тоді ігнорував відповідь.
Джошуа Голдберг

10
Недоліком цього рішення є те, що він повертає булеву форму, тому ви не можете збирати результати потоку, як зазвичай.
neXus

35

Як відповідь на відповідь @StuartMarks . Моя бібліотека StreamEx має takeWhileоперацію, сумісну з поточною реалізацією JDK-9. Під час роботи під JDK-9 він просто делегує реалізацію JDK (завдяки MethodHandle.invokeExactякій це дуже швидко). Під час роботи під JDK-8 буде застосовано реалізацію "polyfill". Тож за допомогою моєї бібліотеки проблему можна вирішити так:

IntStreamEx.iterate(1, n -> n + 1)
           .takeWhile(n -> n < 10)
           .forEach(System.out::println);

Чому ви не реалізували його для класу StreamEx?
Someguy

@Someguy я це здійснив.
Тагір Валєєв


11

Оновлення: Java 9 Streamтепер поставляється із методом takeWhile .

Немає потреби в хаках чи інших рішеннях. Просто використовуйте це!


Я впевнений, що це може бути значно покращено: (хтось може зробити це безпечним для потоків)

Stream<Integer> stream = Stream.iterate(0, n -> n + 1);

TakeWhile.stream(stream, n -> n < 10000)
         .forEach(n -> System.out.print((n == 0 ? "" + n : "," + n)));

Хак точно ... Не елегантно - але це працює ~: D

class TakeWhile<T> implements Iterator<T> {

    private final Iterator<T> iterator;
    private final Predicate<T> predicate;
    private volatile T next;
    private volatile boolean keepGoing = true;

    public TakeWhile(Stream<T> s, Predicate<T> p) {
        this.iterator = s.iterator();
        this.predicate = p;
    }

    @Override
    public boolean hasNext() {
        if (!keepGoing) {
            return false;
        }
        if (next != null) {
            return true;
        }
        if (iterator.hasNext()) {
            next = iterator.next();
            keepGoing = predicate.test(next);
            if (!keepGoing) {
                next = null;
            }
        }
        return next != null;
    }

    @Override
    public T next() {
        if (next == null) {
            if (!hasNext()) {
                throw new NoSuchElementException("Sorry. Nothing for you.");
            }
        }
        T temp = next;
        next = null;
        return temp;
    }

    public static <T> Stream<T> stream(Stream<T> s, Predicate<T> p) {
        TakeWhile tw = new TakeWhile(s, p);
        Spliterator split = Spliterators.spliterator(tw, Integer.MAX_VALUE, Spliterator.ORDERED);
        return StreamSupport.stream(split, false);
    }

}

8

Ви можете використовувати java8 + rxjava .

import java.util.stream.IntStream;
import rx.Observable;


// Example 1)
IntStream intStream  = IntStream.iterate(1, n -> n + 1);
Observable.from(() -> intStream.iterator())
    .takeWhile(n ->
          {
                System.out.println(n);
                return n < 10;
          }
    ).subscribe() ;


// Example 2
IntStream intStream  = IntStream.iterate(1, n -> n + 1);
Observable.from(() -> intStream.iterator())
    .takeWhile(n -> n < 10)
    .forEach( n -> System.out.println(n));

6

Насправді є два способи зробити це в Java 8 без зайвих бібліотек або використання Java 9.

Якщо ви хочете роздрукувати номери від 2 до 20 на консолі, ви можете зробити це:

IntStream.iterate(2, (i) -> i + 2).peek(System.out::println).allMatch(i -> i < 20);

або

IntStream.iterate(2, (i) -> i + 2).peek(System.out::println).anyMatch(i -> i >= 20);

Вихід є в обох випадках:

2
4
6
8
10
12
14
16
18
20

Ніхто ще не згадував жодногоМача . Це причина цієї посади.


5

Це джерело, скопійоване з JDK 9 java.util.stream.Stream.takeWhile (предикат). Невелика різниця для роботи з JDK 8.

static <T> Stream<T> takeWhile(Stream<T> stream, Predicate<? super T> p) {
    class Taking extends Spliterators.AbstractSpliterator<T> implements Consumer<T> {
        private static final int CANCEL_CHECK_COUNT = 63;
        private final Spliterator<T> s;
        private int count;
        private T t;
        private final AtomicBoolean cancel = new AtomicBoolean();
        private boolean takeOrDrop = true;

        Taking(Spliterator<T> s) {
            super(s.estimateSize(), s.characteristics() & ~(Spliterator.SIZED | Spliterator.SUBSIZED));
            this.s = s;
        }

        @Override
        public boolean tryAdvance(Consumer<? super T> action) {
            boolean test = true;
            if (takeOrDrop &&               // If can take
                    (count != 0 || !cancel.get()) && // and if not cancelled
                    s.tryAdvance(this) &&   // and if advanced one element
                    (test = p.test(t))) {   // and test on element passes
                action.accept(t);           // then accept element
                return true;
            } else {
                // Taking is finished
                takeOrDrop = false;
                // Cancel all further traversal and splitting operations
                // only if test of element failed (short-circuited)
                if (!test)
                    cancel.set(true);
                return false;
            }
        }

        @Override
        public Comparator<? super T> getComparator() {
            return s.getComparator();
        }

        @Override
        public void accept(T t) {
            count = (count + 1) & CANCEL_CHECK_COUNT;
            this.t = t;
        }

        @Override
        public Spliterator<T> trySplit() {
            return null;
        }
    }
    return StreamSupport.stream(new Taking(stream.spliterator()), stream.isParallel()).onClose(stream::close);
}

4

Ось версія, зроблена на ints - як задано в питанні.

Використання:

StreamUtil.takeWhile(IntStream.iterate(1, n -> n + 1), n -> n < 10);

Ось код для StreamUtil:

import java.util.PrimitiveIterator;
import java.util.Spliterators;
import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;

public class StreamUtil
{
    public static IntStream takeWhile(IntStream stream, IntPredicate predicate)
    {
        return StreamSupport.intStream(new PredicateIntSpliterator(stream, predicate), false);
    }

    private static class PredicateIntSpliterator extends Spliterators.AbstractIntSpliterator
    {
        private final PrimitiveIterator.OfInt iterator;
        private final IntPredicate predicate;

        public PredicateIntSpliterator(IntStream stream, IntPredicate predicate)
        {
            super(Long.MAX_VALUE, IMMUTABLE);
            this.iterator = stream.iterator();
            this.predicate = predicate;
        }

        @Override
        public boolean tryAdvance(IntConsumer action)
        {
            if (iterator.hasNext()) {
                int value = iterator.nextInt();
                if (predicate.test(value)) {
                    action.accept(value);
                    return true;
                }
            }

            return false;
        }
    }
}

2

Перейдіть до бібліотеки AbacusUtil . Він надає точний API, який ви хочете, і багато іншого:

IntStream.iterate(1, n -> n + 1).takeWhile(n -> n < 10).forEach(System.out::println);

Декларація: я розробник AbacusUtil.


0

Ви не можете перервати потік, за винятком операції терміналу короткого замикання, яка залишить деякі значення потоку необробленими, незалежно від їх значення. Але якщо ви просто хочете уникати операцій над потоком, ви можете додати трансформацію та фільтр до потоку:

import java.util.Objects;

class ThingProcessor
{
    static Thing returnNullOnCondition(Thing thing)
    {    return( (*** is condition met ***)? null : thing);    }

    void processThings(Collection<Thing> thingsCollection)
    {
        thingsCollection.stream()
        *** regular stream processing ***
        .map(ThingProcessor::returnNullOnCondition)
        .filter(Objects::nonNull)
        *** continue stream processing ***
    }
} // class ThingProcessor

Це перетворює потік речей в нульові, коли речі відповідають певній умові, а потім фільтрує нулі. Якщо ви бажаєте потурати побічним ефектам, ви можете встановити значення умови істинним, коли трапляється щось, тож усі наступні речі відфільтруються незалежно від їх вартості. Але навіть якщо ні, ви можете зберегти багато (якщо не зовсім) обробки, фільтруючи значення з потоку, який ви не хочете обробляти.


Прикро, що якийсь анонімний рейтинг знизив мою відповідь, не сказав чому. Тому ні я, ні будь-який інший читач не знаємо, що не так у моїй відповіді. За відсутності їх обґрунтування я вважатиму їх критику недійсною, а мою відповідь правильною.
Матвій

Ви відповідаєте, що не вирішує проблему ОП, яка має справу з нескінченними потоками. Це також, здається, непотрібно ускладнювати речі, оскільки ви можете записати умову у самому виклику filter (), не потребуючи map (). У запитанні вже є приклад коду, просто спробуйте застосувати свою відповідь до цього коду, і ви побачите, що програма буде циклічно назавжди.
SenoCtar

0

Навіть у мене була схожа вимога - зверніться до веб-сервісу, якщо вона не працює, повторіть її 3 рази. Якщо це не вдалося навіть після цих багатьох випробувань, надішліть сповіщення електронною поштою. Багато гуглившись, anyMatch()прийшов як рятівник. Мій зразок коду наступним чином. У наступному прикладі, якщо метод webServiceCall повертає істину в першій самій ітерації, потік не повторює далі, як ми викликали anyMatch(). Я вірю, це те, що ви шукаєте.

import java.util.stream.IntStream;

import io.netty.util.internal.ThreadLocalRandom;

class TrialStreamMatch {

public static void main(String[] args) {        
    if(!IntStream.range(1,3).anyMatch(integ -> webServiceCall(integ))){
         //Code for sending email notifications
    }
}

public static boolean webServiceCall(int i){
    //For time being, I have written a code for generating boolean randomly
    //This whole piece needs to be replaced by actual web-service client code
    boolean bool = ThreadLocalRandom.current().nextBoolean();
    System.out.println("Iteration index :: "+i+" bool :: "+bool);

    //Return success status -- true or false
    return bool;
}

0

Якщо ви знаєте точну кількість повторень, які будуть виконані, ви можете зробити

IntStream
          .iterate(1, n -> n + 1)
          .limit(10)
          .forEach(System.out::println);

1
Хоча це може відповісти на запитання авторів, у ньому відсутні деякі пояснювальні слова та посилання на документацію. Фрагменти сирого коду не дуже корисні без певних фраз. Ви також можете знайти, як написати гарну відповідь дуже корисно. Відредагуйте свою відповідь.
привіт

0
    IntStream.iterate(1, n -> n + 1)
    .peek(System.out::println) //it will be executed 9 times
    .filter(n->n>=9)
    .findAny();

замість піку ви можете використовувати mapToObj для повернення кінцевого об'єкта чи повідомлення

    IntStream.iterate(1, n -> n + 1)
    .mapToObj(n->{   //it will be executed 9 times
            if(n<9)
                return "";
            return "Loop repeats " + n + " times";});
    .filter(message->!message.isEmpty())
    .findAny()
    .ifPresent(System.out::println);

-2

Якщо у вас є інша проблема, може знадобитися інше рішення, але для вашої поточної проблеми я б просто перейшов до:

IntStream
    .iterate(1, n -> n + 1)
    .limit(10)
    .forEach(System.out::println);

-2

Це може бути трохи поза темою, але це те, що ми маємо, List<T>а не для чогоStream<T> .

Спочатку потрібно мати takeметод утиліти. Цей метод займає перші nелементи:

static <T> List<T> take(List<T> l, int n) {
    if (n <= 0) {
        return newArrayList();
    } else {
        int takeTo = Math.min(Math.max(n, 0), l.size());
        return l.subList(0, takeTo);
    }
}

це просто працює так scala.List.take

    assertEquals(newArrayList(1, 2, 3), take(newArrayList(1, 2, 3, 4, 5), 3));
    assertEquals(newArrayList(1, 2, 3), take(newArrayList(1, 2, 3), 5));

    assertEquals(newArrayList(), take(newArrayList(1, 2, 3), -1));
    assertEquals(newArrayList(), take(newArrayList(1, 2, 3), 0));

тепер буде досить просто написати takeWhileметод, заснований наtake

static <T> List<T> takeWhile(List<T> l, Predicate<T> p) {
    return l.stream().
            filter(p.negate()).findFirst(). // find first element when p is false
            map(l::indexOf).        // find the index of that element
            map(i -> take(l, i)).   // take up to the index
            orElse(l);  // return full list if p is true for all elements
}

це працює так:

    assertEquals(newArrayList(1, 2, 3), takeWhile(newArrayList(1, 2, 3, 4, 3, 2, 1), i -> i < 4));

ця реалізація повторює список частково на кілька разів, але це не додасть O(n^2)операцій додавання . Сподіваюся, це прийнятно.


-3

У мене є ще одне швидке рішення, реалізуючи це (що насправді є нечистим, але ви розумієте):

public static void main(String[] args) {
    System.out.println(StreamUtil.iterate(1, o -> o + 1).terminateOn(15)
            .map(o -> o.toString()).collect(Collectors.joining(", ")));
}

static interface TerminatedStream<T> {
    Stream<T> terminateOn(T e);
}

static class StreamUtil {
    static <T> TerminatedStream<T> iterate(T seed, UnaryOperator<T> op) {
        return new TerminatedStream<T>() {
            public Stream<T> terminateOn(T e) {
                Builder<T> builder = Stream.<T> builder().add(seed);
                T current = seed;
                while (!current.equals(e)) {
                    current = op.apply(current);
                    builder.add(current);
                }
                return builder.build();
            }
        };
    }
}

2
Ви заздалегідь оцінюєте весь потік! І якщо currentніколи .equals(e), ви отримаєте нескінченну петлю. Обидва, навіть якщо згодом ви подаєте заявку, наприклад .limit(1). Це набагато гірше, ніж "нечисте" .
Чарлі

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.