Aggiunta di due stream Java 8 o di un elemento aggiuntivo a uno stream


168

Posso aggiungere stream o elementi extra, in questo modo:

Stream stream = Stream.concat(stream1, Stream.concat(stream2, Stream.of(element));

E posso aggiungere nuove cose mentre vado, in questo modo:

Stream stream = Stream.concat(
                       Stream.concat(
                              stream1.filter(x -> x!=0), stream2)
                              .filter(x -> x!=1),
                                  Stream.of(element))
                                  .filter(x -> x!=2);

Ma questo è brutto, perché concatè statico. Se concatfosse un metodo di istanza, gli esempi sopra sarebbero molto più facili da leggere:

 Stream stream = stream1.concat(stream2).concat(element);

E

 Stream stream = stream1
                 .filter(x -> x!=0)
                 .concat(stream2)
                 .filter(x -> x!=1)
                 .concat(element)
                 .filter(x -> x!=2);

La mia domanda è:

1) C'è qualche buona ragione per cui concatè statico? O c'è qualche metodo di istanza equivalente che mi manca?

2) In ogni caso, esiste un modo migliore per farlo?


4
Sembra che le cose non siano sempre state così , ma non riesco proprio a trovare il motivo.
Edwin Dalorzo,

Risposte:


126

Se aggiungi importazioni statiche per Stream.concat e Stream.of , il primo esempio potrebbe essere scritto come segue:

Stream<Foo> stream = concat(stream1, concat(stream2, of(element)));

L'importazione di metodi statici con nomi generici può risultare in codice che diventa difficile da leggere e mantenere ( inquinamento dello spazio dei nomi ). Quindi, potrebbe essere meglio creare i tuoi metodi statici con nomi più significativi. Tuttavia, per dimostrazione, rimarrò con questo nome.

public static <T> Stream<T> concat(Stream<? extends T> lhs, Stream<? extends T> rhs) {
    return Stream.concat(lhs, rhs);
}
public static <T> Stream<T> concat(Stream<? extends T> lhs, T rhs) {
    return Stream.concat(lhs, Stream.of(rhs));
}

Con questi due metodi statici (facoltativamente in combinazione con importazioni statiche), i due esempi potrebbero essere scritti come segue:

Stream<Foo> stream = concat(stream1, concat(stream2, element));

Stream<Foo> stream = concat(
                         concat(stream1.filter(x -> x!=0), stream2).filter(x -> x!=1),
                         element)
                     .filter(x -> x!=2);

Il codice ora è significativamente più breve. Tuttavia, sono d'accordo sul fatto che la leggibilità non è migliorata. Quindi ho un'altra soluzione.


In molte situazioni, i Collezionisti possono essere utilizzati per estendere la funzionalità dei flussi. Con i due Collezionisti in fondo, i due esempi potrebbero essere scritti come segue:

Stream<Foo> stream = stream1.collect(concat(stream2)).collect(concat(element));

Stream<Foo> stream = stream1
                     .filter(x -> x!=0)
                     .collect(concat(stream2))
                     .filter(x -> x!=1)
                     .collect(concat(element))
                     .filter(x -> x!=2);

L'unica differenza tra la sintassi desiderata e la sintassi sopra è che devi sostituire concat (...) con collect (concat (...)) . I due metodi statici possono essere implementati come segue (facoltativamente usato in combinazione con importazioni statiche):

private static <T,A,R,S> Collector<T,?,S> combine(Collector<T,A,R> collector, Function<? super R, ? extends S> function) {
    return Collector.of(
        collector.supplier(),
        collector.accumulator(),
        collector.combiner(),
        collector.finisher().andThen(function));
}
public static <T> Collector<T,?,Stream<T>> concat(Stream<? extends T> other) {
    return combine(Collectors.toList(),
        list -> Stream.concat(list.stream(), other));
}
public static <T> Collector<T,?,Stream<T>> concat(T element) {
    return concat(Stream.of(element));
}

Ovviamente c'è un inconveniente con questa soluzione che dovrebbe essere menzionata. raccogliere è un'operazione finale che consuma tutti gli elementi del flusso. Inoltre, il collat ​​concat crea un ArrayList intermedio ogni volta che viene utilizzato nella catena. Entrambe le operazioni possono avere un impatto significativo sul comportamento del programma. Tuttavia, se la leggibilità è più importante delle prestazioni , potrebbe essere comunque un approccio molto utile.


1
Non trovo concatmolto leggibile il collezionista. Sembra strano avere un metodo statico a parametro singolo chiamato così, e anche da usare collectper la concatenazione.
Didier L

@nosid, forse una domanda leggermente ortogonale a questa discussione, ma perché affermi It's a bad idea to import static methods with names? Sono sinceramente interessato - trovo che renda il codice più conciso e leggibile e molte persone a cui avevo chiesto pensavano lo stesso. Vuoi fornire alcuni esempi perché questo è generalmente negativo?
quantum

1
@Quantum: qual è il significato di compare(reverse(getType(42)), of(6 * 9).hashCode())? Si noti che non ho detto che le importazioni statiche sono una cattiva idea, ma le importazioni statiche per nomi generici come ofe lo concatsono.
nosid

1
@nosid: Il passaggio del mouse su ogni affermazione in un IDE moderno rivela rapidamente il significato? Ad ogni modo, penso che ciò possa essere discutibilmente una dichiarazione di preferenza personale nella migliore delle ipotesi, poiché non vedo ancora alcun motivo tecnico per cui le importazioni statiche per i nomi "generici" non siano buone - a meno che non si stia utilizzando Notepad o VI (M) per la programmazione, nel qual caso hai problemi più grandi.
quantum

Non dirò che l'SDK di Scala sia migliore, ma ... oops, l'ho detto.
Eirirlar,

165

Sfortunatamente questa risposta è probabilmente di scarso o nessun aiuto, ma ho fatto un'analisi forense della mailing list di Lambda Java per vedere se potevo trovare la causa di questo disegno. Questo è quello che ho scoperto.

All'inizio c'era un metodo di istanza per Stream.concat (Stream)

Nella mailing list posso vedere chiaramente che il metodo è stato originariamente implementato come metodo di istanza, come puoi leggere in questo thread da Paul Sandoz, sull'operazione concat.

In esso discutono le questioni che potrebbero derivare da quei casi in cui il flusso potrebbe essere infinito e che cosa significherebbe concatenare in quei casi, ma non credo che questo sia stato il motivo della modifica.

Si vede in questo altro thread che alcuni dei primi utenti di JDK 8 si sono interrogati sul comportamento del metodo dell'istanza concat quando utilizzato con argomenti null.

Quest'altro thread rivela, tuttavia, che il progetto del metodo concat era in discussione.

Rifattorizzato su Streams.concat (Stream, Stream)

Ma senza alcuna spiegazione, improvvisamente, i metodi sono stati cambiati in metodi statici, come puoi vedere in questo thread sulla combinazione di flussi . Questo è forse l'unico thread di posta che fa luce su questo cambiamento, ma non era abbastanza chiaro per me determinare il motivo del refactoring. Ma possiamo vedere che hanno fatto un commit in cui hanno suggerito di spostare il concatmetodo da Streame verso la classe helper Streams.

Rifattorizzato su Stream.concat (Stream, Stream)

Successivamente, è stato spostato di nuovo da Streamsa Stream, ma ancora una volta, nessuna spiegazione per questo.

Quindi, in sostanza, il motivo del design non è del tutto chiaro per me e non sono riuscito a trovare una buona spiegazione. Immagino che potresti ancora porre la domanda nella mailing list.

Alcune alternative per la concatenazione di stream

Questo altro thread di Michael Hixson discute / chiede altri modi per combinare / concaticare i flussi

  1. Per combinare due flussi, dovrei fare questo:

    Stream.concat(s1, s2)

    non questo:

    Stream.of(s1, s2).flatMap(x -> x)

    ... giusto?

  2. Per combinare più di due flussi, dovrei fare questo:

    Stream.of(s1, s2, s3, ...).flatMap(x -> x)

    non questo:

    Stream.of(s1, s2, s3, ...).reduce(Stream.empty(), Stream::concat)

    ... giusto?


6
+1 Bella ricerca. E lo userò come Stream.concat prendendo varargs:public static <T> Stream<T> concat(Stream<T>... streams) { return Stream.of(streams).reduce(Stream.empty(), Stream::concat);}
MarcG

1
Oggi ho scritto la mia versione concat, e subito dopo ho finanziato questo argomento. La firma è leggermente diversa ma grazie a ciò è più generica;) ad esempio è possibile unire Stream <Integer> e Stream <Double> in Stream <Number>. @SafeVarargs private static <T> Stream<T> concat(Stream<? extends T>... streams) { return Stream.of(streams).reduce(Stream.empty(),Stream::concat).map(Function.identity());}
kant,

@kant Perché hai bisogno di una Function.identity()mappa? Dopotutto, restituisce lo stesso argomento che riceve. Ciò non dovrebbe avere alcun effetto nel flusso risultante. Mi sto perdendo qualcosa?
Edwin Dalorzo,

1
Hai provato a digitarlo nel tuo IDE? Senza .map (identità ()) si otterrà un errore di compilazione. Voglio restituire Stream <T> ma dichiarazione: return Stream.of(streams).reduce(Stream.empty(),Stream::concat)restituisce Stream <? estende T>. (Qualcosa <T> è il sottotipo di Qualcosa <? estende T>, non viceversa, quindi non può essere lanciato) .map(identity())Cast aggiuntivo <? estende T> a <T>. Succede grazie al mix di 'tipi target' di java 8 di argomenti di metodo e tipi di ritorno e firma del metodo map (). In realtà è Funzione. <T> identità ().
kant,

1
@kant Non vedo molto da fare ? extends T, dato che puoi usare la conversione di acquisizione . In ogni caso, ecco il mio frammento di codice gist Continuiamo la discussione in Gist.
Edwin Dalorzo,

12

La mia libreria StreamEx estende le funzionalità dell'API Stream. In particolare offre metodi come append e prepend che risolvono questo problema (internamente usano concat). Questi metodi possono accettare un altro flusso o raccolta o array varargs. Usando la mia libreria il tuo problema può essere risolto in questo modo (nota che x != 0sembra strano per un flusso non primitivo):

Stream<Integer> stream = StreamEx.of(stream1)
             .filter(x -> !x.equals(0))
             .append(stream2)
             .filter(x -> !x.equals(1))
             .append(element)
             .filter(x -> !x.equals(2));

A proposito c'è anche una scorciatoia per la tua filteroperazione:

Stream<Integer> stream = StreamEx.of(stream1).without(0)
                                 .append(stream2).without(1)
                                 .append(element).without(2);

9

Basta fare:

Stream.of(stream1, stream2, Stream.of(element)).flatMap(identity());

dove identity()è un'importazione statica di Function.identity().

Concatenare più flussi in un flusso è uguale all'appiattimento di un flusso.

Tuttavia, sfortunatamente, per qualche motivo non esiste un flatten()metodo attivo Stream, quindi è necessario utilizzare flatMap()con la funzione di identità.



1

Se non ti dispiace usare Librerie di terze parti cyclops-reag ha un tipo Stream esteso che ti permetterà di farlo proprio tramite gli operatori append / prepend.

Valori individuali, array, iterabili, stream o flussi reattivi Gli editori possono essere aggiunti e anteposti come metodi di istanza.

Stream stream = ReactiveSeq.of(1,2)
                           .filter(x -> x!=0)
                           .append(ReactiveSeq.of(3,4))
                           .filter(x -> x!=1)
                           .append(5)
                           .filter(x -> x!=2);

[Divulgazione Sono lo sviluppatore principale di cyclops-reagire]


1

Alla fine della giornata non mi interessa combinare i flussi, ma ottenere il risultato combinato dell'elaborazione di ciascun elemento di tutti i flussi.

Mentre combinare i flussi potrebbe rivelarsi ingombrante (quindi questo thread), combinare i loro risultati di elaborazione è abbastanza facile.

La chiave da risolvere è creare il proprio raccoglitore e assicurarsi che la funzione fornitore per il nuovo raccoglitore ritorni sempre la stessa raccolta ( non una nuova ), il codice seguente illustra questo approccio.

package scratchpad;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collector;
import java.util.stream.Stream;

public class CombineStreams {
    public CombineStreams() {
        super();
    }

    public static void main(String[] args) {
        List<String> resultList = new ArrayList<>();
        Collector<String, List<String>, List<String>> collector = Collector.of(
                () -> resultList,
                (list, item) -> {
                    list.add(item);
                },
                (llist, rlist) -> {
                    llist.addAll(rlist);
                    return llist;
                }
        );
        String searchString = "Wil";

        System.out.println("After processing first stream\n"
                + createFirstStream().filter(name -> name.contains(searchString)).collect(collector));
        System.out.println();

        System.out.println("After processing second stream\n"
                + createSecondStream().filter(name -> name.contains(searchString)).collect(collector));
        System.out.println();

        System.out.println("After processing third stream\n"
                + createThirdStream().filter(name -> name.contains(searchString)).collect(collector));
        System.out.println();

    }

    private static Stream<String> createFirstStream() {
        return Arrays.asList(
                "William Shakespeare",
                "Emily Dickinson",
                "H. P. Lovecraft",
                "Arthur Conan Doyle",
                "Leo Tolstoy",
                "Edgar Allan Poe",
                "Robert Ervin Howard",
                "Rabindranath Tagore",
                "Rudyard Kipling",
                "Seneca",
                "John Donne",
                "Sarah Williams",
                "Oscar Wilde",
                "Catullus",
                "Alfred Tennyson",
                "William Blake",
                "Charles Dickens",
                "John Keats",
                "Theodor Herzl"
        ).stream();
    }

    private static Stream<String> createSecondStream() {
        return Arrays.asList(
                "Percy Bysshe Shelley",
                "Ernest Hemingway",
                "Barack Obama",
                "Anton Chekhov",
                "Henry Wadsworth Longfellow",
                "Arthur Schopenhauer",
                "Jacob De Haas",
                "George Gordon Byron",
                "Jack London",
                "Robert Frost",
                "Abraham Lincoln",
                "O. Henry",
                "Ovid",
                "Robert Louis Stevenson",
                "John Masefield",
                "James Joyce",
                "Clark Ashton Smith",
                "Aristotle",
                "William Wordsworth",
                "Jane Austen"
        ).stream();
    }

    private static Stream<String> createThirdStream() {
        return Arrays.asList(
                "Niccolò Machiavelli",
                "Lewis Carroll",
                "Robert Burns",
                "Edgar Rice Burroughs",
                "Plato",
                "John Milton",
                "Ralph Waldo Emerson",
                "Margaret Thatcher",
                "Sylvie d'Avigdor",
                "Marcus Tullius Cicero",
                "Banjo Paterson",
                "Woodrow Wilson",
                "Walt Whitman",
                "Theodore Roosevelt",
                "Agatha Christie",
                "Ambrose Bierce",
                "Nikola Tesla",
                "Franz Kafka"
        ).stream();
    }
}

0

Che ne dici di scrivere il tuo metodo concat?

public static Stream<T> concat(Stream<? extends T> a, 
                               Stream<? extends T> b, 
                               Stream<? extends T> args)
{
    Stream<T> concatenated = Stream.concat(a, b);
    for (Stream<T> stream : args)
    {
        concatenated = Stream.concat(concatenated, stream);
    }
    return concatenated;
}

Questo almeno rende il tuo primo esempio molto più leggibile.


1
Prestare attenzione quando si creano flussi da concatenazioni ripetute. L'accesso a un elemento di un flusso profondamente concatenato può provocare catene di chiamate profonde o persino StackOverflowError.
Legna,
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.