Streaming di zip usando JDK8 con lambda (java.util.stream.Streams.zip)


149

In JDK 8 con lambda b93 c'era una classe java.util.stream.Streams.zip in b93 che poteva essere usata per comprimere i flussi (questo è illustrato nel tutorial Exploring Java8 Lambdas. Parte 1 di Dhananjay Nene ). Questa funzione:

Crea uno Stream combinato pigro e sequenziale i cui elementi sono il risultato della combinazione degli elementi di due flussi.

Tuttavia nel b98 questo è scomparso. Infatti la Streamsclasse non è nemmeno accessibile in java.util.stream in b98 .

Questa funzionalità è stata spostata e, in caso affermativo, come posso comprimere gli stream in modo conciso utilizzando b98?

L'applicazione che ho in mente è in questa implementazione Java di Shen , dove ho sostituito la funzionalità zip in

  • 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)

funziona con un codice piuttosto dettagliato (che non utilizza la funzionalità di b98).


3
Ah ho appena scoperto che sembra essere stato rimosso completamente: mail.openjdk.java.net/pipermail/lambda-libs-spec-observers/…
artella

"Exploring Java8 Lambdas. Part 1" - il nuovo link per questo articolo è blog.dhananjaynene.com/2013/02/exploring-java8-lambdas-part-1
Aleksei Egorov,

Risposte:


77

Ne avevo anche bisogno, quindi ho preso il codice sorgente da b93 e l'ho messo in una classe "util". Ho dovuto modificarlo leggermente per funzionare con l'API corrente.

Per riferimento ecco il codice di lavoro (prendilo a tuo rischio e pericolo ...):

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
Lo stream risultante non dovrebbe essere SIZEDse uno dei due stream è SIZED, non entrambi?
Didier L

5
Io non la penso così. Entrambi i flussi devono essere SIZEDaffinché questa implementazione funzioni. In realtà dipende da come si definisce zippare. Dovresti essere in grado di comprimere due flussi di dimensioni diverse, ad esempio? Come sarebbe il flusso risultante allora? Credo che questo sia il motivo per cui questa funzione è stata effettivamente omessa dall'API. Ci sono molti modi per farlo ed è compito dell'utente decidere quale comportamento deve essere "corretto". Scarteresti gli elementi dallo stream più lungo o riempiresti l'elenco più corto? In tal caso, con quale valore / i?
Siki,

A meno che non mi manchi qualcosa, non è necessario alcun cast (ad es Spliterator<A>.).
jub0bs

Esiste un sito Web in cui è ospitato il codice sorgente Java 8 b93? Ho problemi a trovarlo.
Starwarswii,

42

zip è una delle funzioni fornite dalla libreria 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"));


34

Se hai Guava nel tuo progetto, puoi usare il metodo Streams.zip (è stato aggiunto in Guava 21):

Restituisce uno stream in cui ciascun elemento è il risultato del passaggio dell'elemento corrispondente di ciascuno streamA e streamB alla funzione. Il flusso risultante sarà solo finché il più breve dei due flussi di input; se uno stream è più lungo, i suoi elementi extra verranno ignorati. Il flusso risultante non è divisibile in modo efficiente. Ciò potrebbe danneggiare le prestazioni parallele.

 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

Comprimere due flussi usando JDK8 con lambda ( 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
Bella soluzione e (relativamente) compatta! Richiede l'inserimento import java.util.function.*;e import java.util.stream.*;nella parte superiore del file.
sffc,

Si noti che questa è un'operazione terminale sullo stream. Ciò significa che per flussi infiniti, questo metodo si interrompe
smac89,

2
Tanto involucri inutili: Qui () -> iteratore qui di nuovo: iterable.spliterator(). Perché non implementare direttamente un Spliteratoranziché un Iterator? Controllare risposta @Doradus stackoverflow.com/a/46230233/1140754
Miguel Gamboa

20

Dal momento che non riesco a concepire alcun uso di zippare su raccolte diverse da quelle indicizzate (Liste) e sono un grande fan della semplicità, questa sarebbe la mia soluzione:

<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
Penso che mapToObjectdovrebbe essere mapToObj.
seanf

se l'elenco non è RandomAccess(ad es. su elenchi collegati) sarà molto lento
avmohan

Decisamente. Ma la maggior parte degli sviluppatori Java sa bene che LinkedList ha scarse prestazioni per le operazioni di accesso all'indice.
Rafael,

11

I metodi della classe che hai citato sono stati spostati Streamnell'interfaccia stessa a favore dei metodi predefiniti. Ma sembra che il zipmetodo sia stato rimosso. Forse perché non è chiaro quale dovrebbe essere il comportamento predefinito per flussi di dimensioni diverse. Ma l'implementazione del comportamento desiderato è semplice:

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);
}

Lo stato che predicatehai passato al filtro non è stato ? Ciò viola il contratto del metodo e soprattutto non funzionerà durante l'elaborazione del flusso in parallelo.
Andreas,

2
@Andreas: nessuna delle soluzioni qui supporta l'elaborazione parallela. Poiché i miei metodi non restituiscono un flusso, si assicurano che i flussi non vengano eseguiti in parallelo. Allo stesso modo, il codice della risposta accettata restituisce un flusso che può essere trasformato in parallelo ma in realtà non farà nulla in parallelo. Detto questo, i predicati statali sono scoraggiati ma non violano il contratto. Potrebbero anche essere utilizzati in un contesto parallelo se si garantisce che l'aggiornamento dello stato sia sicuro per i thread. In alcune situazioni sono inevitabili, ad esempio trasformare un flusso in distinto è un predicato con stato di per sé .
Holger,

2
@Andreas: puoi immaginare perché queste operazioni sono state rimosse dall'API Java ...
Holger

8

Consiglio umilmente questa implementazione. Il flusso risultante viene troncato al più breve dei due flussi di input.

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());
}

Mi piace la tua proposta. Ma non sono totalmente d'accordo con l'ultimo .., leftStream.isParallel() || rightStream.isParallel(). Penso che non abbia alcun effetto perché AbstractSpliteratoroffre un parallelismo limitato per impostazione predefinita. Quindi penso che il risultato finale sarà lo stesso del passaggio false.
Miguel Gamboa,

@MiguelGamboa - grazie per il tuo commento. Non sono sicuro di cosa intendi per "parallelismo limitato per impostazione predefinita" - hai un link ad alcuni documenti?
Doradus,

6

La libreria Lazy-Seq offre funzionalità zip.

https://github.com/nurkiewicz/LazySeq

Questa libreria è fortemente ispirata scala.collection.immutable.Streame mira a fornire un'implementazione della sequenza pigra immutabile, sicura per i thread e facile da usare, forse infinita.


5

Utilizzando l'ultima libreria di Guava (per la Streamsclasse) dovresti essere in grado di farlo

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

2

Funzionerebbe per te? È una funzione breve, che valuta pigramente sui flussi che sta zippando, quindi puoi fornirli con flussi infiniti (non ha bisogno di prendere la dimensione dei flussi che sono compressi).

Se i flussi sono finiti, si interrompe non appena uno dei flussi finisce di elementi.

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);
    }
}

Ecco qualche codice di unit test (molto più lungo del codice stesso!)

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);
    }
}

alla fine ho dovuto abbandonare il takeWhilefatto che non sembra essere in Java8 ma non è un problema in quanto la chiamata può filtrare tutti i null che si verificano quando i flussi zippati non hanno le stesse dimensioni. penso che questa risposta dovrebbe essere la risposta numero 1 in quanto è coerente e comprensibile. ottimo lavoro grazie ancora.
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

La reazione al ciclope di AOL , alla quale contribuisco, fornisce anche funzionalità di zippaggio, sia tramite un'implementazione Stream estesa , che implementa anche l'interfaccia di flussi reattivi ReactiveSeq, sia tramite StreamUtils che offre gran parte delle stesse funzionalità tramite metodi statici ai flussi Java standard.

 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));

Offre anche zip più generalizzate basate su applicazioni. Per esempio

   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;
   }

E anche la possibilità di accoppiare ogni oggetto in uno stream con ogni oggetto in un altro

   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

Se qualcuno ne ha ancora bisogno, c'è una StreamEx.zipWithfunzione nella libreria 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

Questo è fantastico Ho dovuto comprimere due flussi in una mappa con uno stream come chiave e l'altro come valore

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())));

Output: {A = Mela, B = Banana, C = Carota}

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.