Поєднання потоків за допомогою JDK8 з лямбда (java.util.stream.Streams.zip)


149

У JDK 8 з лямбда-b93 був клас java.util.stream.Streams.zip в b93, який можна було використовувати для поштових потоків (це проілюстровано у підручнику « Дослідження Java8 Lambdas». Частина 1 від Dhananjay Nene ). Ця функція:

Створюється лінивий і послідовний поєднаний Потік, елементи якого є результатом поєднання елементів двох потоків.

Однак у b98 це зникло. StreamsКлас Infact навіть недоступний у java.util.stream у b98 .

Чи переміщена ця функціональність, і якщо так, то як я можу стискати потоки з допомогою b98?

Я маю на увазі додаток, яке полягає в цій Java-програмі Shen , де я замінив функцію zip в

  • static <T> boolean every(Collection<T> c1, Collection<T> c2, BiPredicate<T, T> pred)
  • static <T> T find(Collection<T> c1, Collection<T> c2, BiPredicate<T, T> pred)

функції з досить багатослівним кодом (який не використовує функціональність від b98).


3
Ну щойно з’ясували, що його, здається, видалили повністю: mail.openjdk.java.net/pipermail/lambda-libs-spec-observers/…
artella

"Дослідження лямбдасів Java8. Частина 1" - нове посилання на цю статтю - blog.dhananjaynene.com/2013/02/exploring-java8-lambdas-part-1
Олексій Єгоров

Відповіді:


77

Мені це також знадобилося, тому я просто взяв вихідний код з b93 і помістив його у клас "util". Мені довелося трохи змінити його для роботи з поточним API.

Для довідки ось робочий код (прийміть його на свій страх і ризик ...):

public static<A, B, C> Stream<C> zip(Stream<? extends A> a,
                                     Stream<? extends B> b,
                                     BiFunction<? super A, ? super B, ? extends C> zipper) {
    Objects.requireNonNull(zipper);
    Spliterator<? extends A> aSpliterator = Objects.requireNonNull(a).spliterator();
    Spliterator<? extends B> bSpliterator = Objects.requireNonNull(b).spliterator();

    // Zipping looses DISTINCT and SORTED characteristics
    int characteristics = aSpliterator.characteristics() & bSpliterator.characteristics() &
            ~(Spliterator.DISTINCT | Spliterator.SORTED);

    long zipSize = ((characteristics & Spliterator.SIZED) != 0)
            ? Math.min(aSpliterator.getExactSizeIfKnown(), bSpliterator.getExactSizeIfKnown())
            : -1;

    Iterator<A> aIterator = Spliterators.iterator(aSpliterator);
    Iterator<B> bIterator = Spliterators.iterator(bSpliterator);
    Iterator<C> cIterator = new Iterator<C>() {
        @Override
        public boolean hasNext() {
            return aIterator.hasNext() && bIterator.hasNext();
        }

        @Override
        public C next() {
            return zipper.apply(aIterator.next(), bIterator.next());
        }
    };

    Spliterator<C> split = Spliterators.spliterator(cIterator, zipSize, characteristics);
    return (a.isParallel() || b.isParallel())
           ? StreamSupport.stream(split, true)
           : StreamSupport.stream(split, false);
}

1
Чи не повинен бути отриманий потік, SIZEDякщо будь-який потік є SIZED, не обидва?
Didier L

5
Я не думаю, що так. Обидва потоки повинні бути, SIZEDщоб ця реалізація працювала. Це насправді залежить від того, як ви визначаєте блискавку. Чи маєте ви, наприклад, мати можливість застебнути два потоки різної величини? Як би виглядав отриманий потік тоді? Я вважаю, що саме тому цю функцію було фактично опущено з API. Існує багато способів зробити це, і користувач повинен вирішити, яка поведінка повинна бути "правильною". Чи б ви відкидали елементи з більш тривалого потоку або додаєте коротший список? Якщо так, то з якими значеннями?
siki

Якщо я чогось не пропускаю, немає необхідності в жодній ролі (наприклад, до Spliterator<A>).
jub0bs

Чи є веб-сайт, на якому розміщений вихідний код Java 8 b93? У мене виникають проблеми з її пошуку
Starwarswii

42

zip - одна з функцій, що надаються бібліотекою protonpack .

Stream<String> streamA = Stream.of("A", "B", "C");
Stream<String> streamB  = Stream.of("Apple", "Banana", "Carrot", "Doughnut");

List<String> zipped = StreamUtils.zip(streamA,
                                      streamB,
                                      (a, b) -> a + " is for " + b)
                                 .collect(Collectors.toList());

assertThat(zipped,
           contains("A is for Apple", "B is for Banana", "C is for Carrot"));

1
також знайдено в StreamEx: amaembo.github.io/streamex/javadoc/one/util/streamex/…
tokland

34

Якщо у вашому проекті є Guava, ви можете використовувати метод Streams.zip (доданий у Guava 21):

Повертає потік, у якому кожен елемент є результатом передачі відповідного елемента кожного потокуA та streamB у функцію. Отриманий потік буде лише до тих пір, поки коротший з двох вхідних потоків; якщо один потік довший, його зайві елементи будуть ігноровані. Отриманий потік не ефективно розподіляється. Це може зашкодити паралельній роботі.

 public class Streams {
     ...

     public static <A, B, R> Stream<R> zip(Stream<A> streamA,
             Stream<B> streamB, BiFunction<? super A, ? super B, R> function) {
         ...
     }
 }

26

З’єднання двох потоків за допомогою JDK8 з лямбда ( gist ).

public static <A, B, C> Stream<C> zip(Stream<A> streamA, Stream<B> streamB, BiFunction<A, B, C> zipper) {
    final Iterator<A> iteratorA = streamA.iterator();
    final Iterator<B> iteratorB = streamB.iterator();
    final Iterator<C> iteratorC = new Iterator<C>() {
        @Override
        public boolean hasNext() {
            return iteratorA.hasNext() && iteratorB.hasNext();
        }

        @Override
        public C next() {
            return zipper.apply(iteratorA.next(), iteratorB.next());
        }
    };
    final boolean parallel = streamA.isParallel() || streamB.isParallel();
    return iteratorToFiniteStream(iteratorC, parallel);
}

public static <T> Stream<T> iteratorToFiniteStream(Iterator<T> iterator, boolean parallel) {
    final Iterable<T> iterable = () -> iterator;
    return StreamSupport.stream(iterable.spliterator(), parallel);
}

2
Приємне рішення та (відносно) компактне! Потрібно поставити import java.util.function.*;і import java.util.stream.*;вгорі файлу.
sffc

Зауважте, що це термінальна операція в потоці. Це означає, що для нескінченних потоків цей метод руйнується
smac89

2
Стільки марних фантиків: Тут () -> iteratorі тут знову : iterable.spliterator(). Чому б не реалізувати безпосередньо, Spliteratorа не реально Iterator? Перевірте @Doradus відповідь stackoverflow.com/a/46230233/1140754
Мігель Гамбоа

20

Оскільки я не можу уявити будь-яке використання zipping для колекцій, окрім індексованих (Списки), і я великий шанувальник простоти, це було б моїм рішенням:

<A,B,C>  Stream<C> zipped(List<A> lista, List<B> listb, BiFunction<A,B,C> zipper){
     int shortestLength = Math.min(lista.size(),listb.size());
     return IntStream.range(0,shortestLength).mapToObj( i -> {
          return zipper.apply(lista.get(i), listb.get(i));
     });        
}

1
Я думаю, що mapToObjectмає бути mapToObj.
seanf

якщо списку немає RandomAccess(наприклад, у пов'язаних списках), це буде дуже повільно
avmohan

Безумовно. Але більшість розробників Java добре розуміють, що LinkedList має низьку продуктивність для операцій доступу до індексу.
Рафаель

11

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

static <T> boolean every(
  Collection<T> c1, Collection<T> c2, BiPredicate<T, T> pred) {
    Iterator<T> it=c2.iterator();
    return c1.stream().allMatch(x->!it.hasNext()||pred.test(x, it.next()));
}
static <T> T find(Collection<T> c1, Collection<T> c2, BiPredicate<T, T> pred) {
    Iterator<T> it=c2.iterator();
    return c1.stream().filter(x->it.hasNext()&&pred.test(x, it.next()))
      .findFirst().orElse(null);
}

Хіба predicateви не передали фільтр станом ? Це порушує контракт методу і особливо не працюватиме при паралельній обробці потоку.
Андреас

2
@Andreas: жодне рішення тут не підтримує паралельну обробку. Оскільки мої методи не повертають потік, вони гарантують, що потоки не працюють паралельно. Аналогічно, код прийнятої відповіді повертає потік, який можна перетворити на паралельний, але насправді нічого не робитиме паралельно. Однак, державні предикати відмовляються, але не порушують договір. Вони можуть навіть використовуватися в паралельному контексті, якщо ви гарантуєте, що оновлення стану є безпечним для потоків. У деяких випадках вони неминучі, наприклад , перетворення потоку на відміну є statefull предикат сам по собі .
Холгер

2
@Andreas: ви можете здогадатися, чому ці операції були видалені з Java API…
Holger

8

Я смиренно пропоную цю реалізацію. Отриманий потік усічений на коротший з двох вхідних потоків.

public static <L, R, T> Stream<T> zip(Stream<L> leftStream, Stream<R> rightStream, BiFunction<L, R, T> combiner) {
    Spliterator<L> lefts = leftStream.spliterator();
    Spliterator<R> rights = rightStream.spliterator();
    return StreamSupport.stream(new AbstractSpliterator<T>(Long.min(lefts.estimateSize(), rights.estimateSize()), lefts.characteristics() & rights.characteristics()) {
        @Override
        public boolean tryAdvance(Consumer<? super T> action) {
            return lefts.tryAdvance(left->rights.tryAdvance(right->action.accept(combiner.apply(left, right))));
        }
    }, leftStream.isParallel() || rightStream.isParallel());
}

Мені подобається ваша пропозиція. Але я остаточно не згоден з останнім .., leftStream.isParallel() || rightStream.isParallel(). Я думаю, що це не має ефекту, оскільки AbstractSpliteratorза умовчанням пропонується обмежений паралелізм. Тому я думаю, що кінцевий результат буде таким самим, як проходження false.
Мігель Гамбоа

@MiguelGamboa - дякую за ваш коментар Я не впевнений, що ви маєте на увазі під обмеженим паралелізмом за замовчуванням - чи є у вас посилання на деякі документи?
Doradus

6

Бібліотека Lazy-Seq забезпечує функціональність zip.

https://github.com/nurkiewicz/LazySeq

Ця бібліотека дуже натхненна scala.collection.immutable.Streamі спрямована на те, щоб забезпечити незмінну, безпечну для потоків та просту у використанні ледачу реалізацію послідовностей, можливо, нескінченну.


5

Використовуючи останню бібліотеку Guava (для Streamsкласу), ви повинні мати можливість

final Map<String, String> result = 
    Streams.zip(
        collection1.stream(), 
        collection2.stream(), 
        AbstractMap.SimpleEntry::new)
    .collect(Collectors.toMap(e -> e.getKey(), e  -> e.getValue()));

2

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

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

import java.util.Objects;
import java.util.function.BiFunction;
import java.util.stream.Stream;

class StreamUtils {
    static <ARG1, ARG2, RESULT> Stream<RESULT> zip(
            Stream<ARG1> s1,
            Stream<ARG2> s2,
            BiFunction<ARG1, ARG2, RESULT> combiner) {
        final var i2 = s2.iterator();
        return s1.map(x1 -> i2.hasNext() ? combiner.apply(x1, i2.next()) : null)
                .takeWhile(Objects::nonNull);
    }
}

Ось одиничний тестовий код (набагато довший, ніж сам код!)

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

class StreamUtilsTest {
    @ParameterizedTest
    @MethodSource("shouldZipTestCases")
    <ARG1, ARG2, RESULT>
    void shouldZip(
            String testName,
            Stream<ARG1> s1,
            Stream<ARG2> s2,
            BiFunction<ARG1, ARG2, RESULT> combiner,
            Stream<RESULT> expected) {
        var actual = StreamUtils.zip(s1, s2, combiner);

        assertEquals(
                expected.collect(Collectors.toList()),
                actual.collect(Collectors.toList()),
                testName);
    }

    private static Stream<Arguments> shouldZipTestCases() {
        return Stream.of(
                Arguments.of(
                        "Two empty streams",
                        Stream.empty(),
                        Stream.empty(),
                        (BiFunction<Object, Object, Object>) StreamUtilsTest::combine,
                        Stream.empty()),
                Arguments.of(
                        "One singleton and one empty stream",
                        Stream.of(1),
                        Stream.empty(),
                        (BiFunction<Object, Object, Object>) StreamUtilsTest::combine,
                        Stream.empty()),
                Arguments.of(
                        "One empty and one singleton stream",
                        Stream.empty(),
                        Stream.of(1),
                        (BiFunction<Object, Object, Object>) StreamUtilsTest::combine,
                        Stream.empty()),
                Arguments.of(
                        "Two singleton streams",
                        Stream.of("blah"),
                        Stream.of(1),
                        (BiFunction<Object, Object, Object>) StreamUtilsTest::combine,
                        Stream.of(pair("blah", 1))),
                Arguments.of(
                        "One singleton, one multiple stream",
                        Stream.of("blob"),
                        Stream.of(2, 3),
                        (BiFunction<Object, Object, Object>) StreamUtilsTest::combine,
                        Stream.of(pair("blob", 2))),
                Arguments.of(
                        "One multiple, one singleton stream",
                        Stream.of("foo", "bar"),
                        Stream.of(4),
                        (BiFunction<Object, Object, Object>) StreamUtilsTest::combine,
                        Stream.of(pair("foo", 4))),
                Arguments.of(
                        "Two multiple streams",
                        Stream.of("nine", "eleven"),
                        Stream.of(10, 12),
                        (BiFunction<Object, Object, Object>) StreamUtilsTest::combine,
                        Stream.of(pair("nine", 10), pair("eleven", 12)))
        );
    }

    private static List<Object> pair(Object o1, Object o2) {
        return List.of(o1, o2);
    }

    static private <T1, T2> List<Object> combine(T1 o1, T2 o2) {
        return List.of(o1, o2);
    }

    @Test
    void shouldLazilyEvaluateInZip() {
        final var a = new AtomicInteger();
        final var b = new AtomicInteger();
        final var zipped = StreamUtils.zip(
                Stream.generate(a::incrementAndGet),
                Stream.generate(b::decrementAndGet),
                (xa, xb) -> xb + 3 * xa);

        assertEquals(0, a.get(), "Should not have evaluated a at start");
        assertEquals(0, b.get(), "Should not have evaluated b at start");

        final var takeTwo = zipped.limit(2);

        assertEquals(0, a.get(), "Should not have evaluated a at take");
        assertEquals(0, b.get(), "Should not have evaluated b at take");

        final var list = takeTwo.collect(Collectors.toList());

        assertEquals(2, a.get(), "Should have evaluated a after collect");
        assertEquals(-2, b.get(), "Should have evaluated b after collect");
        assertEquals(List.of(2, 4), list);
    }
}

Мені довелося скинути це takeWhileв кінці - це, здається, не в java8, але це не проблема, оскільки каллі може відфільтрувати будь-які нулі, які виникають, коли потоки в блискавці не мають однакового розміру. Я думаю, що ця відповідь повинна відповідати на номер 1, оскільки вона складається і зрозуміла. чудова робота ще раз дякую.
simbo1905

1
public class Tuple<S,T> {
    private final S object1;
    private final T object2;

    public Tuple(S object1, T object2) {
        this.object1 = object1;
        this.object2 = object2;
    }

    public S getObject1() {
        return object1;
    }

    public T getObject2() {
        return object2;
    }
}


public class StreamUtils {

    private StreamUtils() {
    }

    public static <T> Stream<Tuple<Integer,T>> zipWithIndex(Stream<T> stream) {
        Stream<Integer> integerStream = IntStream.range(0, Integer.MAX_VALUE).boxed();
        Iterator<Integer> integerIterator = integerStream.iterator();
        return stream.map(x -> new Tuple<>(integerIterator.next(), x));
    }
}

1

Циклоп-реагування AOL , до якого я сприяю, також забезпечує функцію зшивання, як через розширену реалізацію потоку , яка також реалізує інтерфейс реактивних потоків ReactiveSeq, так і через StreamUtils, що пропонує багато однакових функцій за допомогою статичних методів для стандартних Java Streams.

 List<Tuple2<Integer,Integer>> list =  ReactiveSeq.of(1,2,3,4,5,6)
                                                  .zip(Stream.of(100,200,300,400));


  List<Tuple2<Integer,Integer>> list = StreamUtils.zip(Stream.of(1,2,3,4,5,6),
                                                  Stream.of(100,200,300,400));

Він також пропонує більш узагальнені додатки на основі блискавок. Напр

   ReactiveSeq.of("a","b","c")
              .ap3(this::concat)
              .ap(of("1","2","3"))
              .ap(of(".","?","!"))
              .toList();

   //List("a1.","b2?","c3!");

   private String concat(String a, String b, String c){
    return a+b+c;
   }

І навіть можливість пари кожного елемента в одному потоці з кожним елементом в іншому

   ReactiveSeq.of("a","b","c")
              .forEach2(str->Stream.of(str+"!","2"), a->b->a+"_"+b);

   //ReactiveSeq("a_a!","a_2","b_b!","b_2","c_c!","c2")

0

Якщо комусь це ще потрібно, StreamEx.zipWithу бібліотеці streamex є функція :

StreamEx<String> givenNames = StreamEx.of("Leo", "Fyodor")
StreamEx<String> familyNames = StreamEx.of("Tolstoy", "Dostoevsky")
StreamEx<String> fullNames = givenNames.zipWith(familyNames, (gn, fn) -> gn + " " + fn);

fullNames.forEach(System.out::println);  // prints: "Leo Tolstoy\nFyodor Dostoevsky\n"

-1

Це чудово. Мені довелося зафіксувати два потоки на карті, один ключ - ключовий, а інший - значення

Stream<String> streamA = Stream.of("A", "B", "C");
Stream<String> streamB  = Stream.of("Apple", "Banana", "Carrot", "Doughnut");    
final Stream<Map.Entry<String, String>> s = StreamUtils.zip(streamA,
                    streamB,
                    (a, b) -> {
                        final Map.Entry<String, String> entry = new AbstractMap.SimpleEntry<String, String>(a, b);
                        return entry;
                    });

System.out.println(s.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())));

Вихід: {A = Apple, B = банан, C = морква}

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