Dodanie dwóch strumieni Java 8 lub dodatkowego elementu do strumienia

168

Mogę dodać strumienie lub dodatkowe elementy, na przykład:

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

I mogę dodawać nowe rzeczy na bieżąco, na przykład:

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

Ale to jest brzydkie, ponieważ concatjest statyczne. Gdyby concatbyła to metoda instancji, powyższe przykłady byłyby znacznie łatwiejsze do odczytania:

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

I

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

Moje pytanie brzmi:

1) Czy jest jakiś dobry powód, dla którego concatjest statyczny? A może brakuje mi jakiejś równoważnej metody instancji?

2) W każdym razie czy istnieje lepszy sposób na zrobienie tego?

MarcG
źródło
4
Wygląda na to, że nie zawsze tak było , ale po prostu nie mogę znaleźć powodu.
Edwin Dalorzo

Odpowiedzi:

126

Jeśli dodać importu statycznych dla Stream.concat i Stream.of , pierwszy przykład można zapisać w następujący sposób:

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

Importowanie metod statycznych z nazwami rodzajowymi może spowodować, że kod stanie się trudny do odczytania i utrzymania ( zanieczyszczenie przestrzeni nazw ). Dlatego lepiej byłoby utworzyć własne metody statyczne o bardziej znaczących nazwach. Jednak dla demonstracji pozostanę przy tej nazwie.

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

W przypadku tych dwóch metod statycznych (opcjonalnie w połączeniu ze statycznymi importami) oba przykłady można zapisać w następujący sposób:

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

Kod jest teraz znacznie krótszy. Zgadzam się jednak, że czytelność się nie poprawiła. Więc mam inne rozwiązanie.


W wielu sytuacjach Collectors można wykorzystać do rozszerzenia funkcjonalności strumieni. Mając dwa Kolektory na dole, dwa przykłady można zapisać w następujący sposób:

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

Jedyna różnica między żądaną składni i składni powyższego jest to, że trzeba wymienić concat (...) z honor (concat (...)) . Dwie metody statyczne można zaimplementować w następujący sposób (opcjonalnie używane w połączeniu z importami statycznymi):

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

Oczywiście rozwiązanie to ma wadę, o której należy wspomnieć. zbieranie to operacja końcowa, która zużywa wszystkie elementy strumienia. Ponadto concat kolektorów tworzy pośrednią ArrayList za każdym razem, gdy jest ona używana w łańcuchu. Obie operacje mogą mieć znaczący wpływ na zachowanie programu. Jeśli jednak czytelność jest ważniejsza niż wydajność , może to być bardzo pomocne podejście.

nosid
źródło
1
Wydaje mi się, że concatkolekcjoner nie jest zbyt czytelny. Wydaje się dziwne, że jednoparametrowa metoda statyczna ma taką nazwę, a także ma być używana collectdo konkatenacji.
Didier L
@nosid, być może nieco ortogonalne pytanie do tego wątku, ale dlaczego twierdzisz It's a bad idea to import static methods with names? Jestem szczerze zainteresowany - uważam, że dzięki temu kod jest bardziej zwięzły i czytelny, a wiele osób, o które pytałem, myślało tak samo. Spróbuj podać kilka przykładów, dlaczego jest to ogólnie złe?
kwantowy
1
@Quantum: Jakie jest znaczenie compare(reverse(getType(42)), of(6 * 9).hashCode())? Zauważ, że nie powiedziałem, że statyczne importowanie to zły pomysł, ale statyczne importowanie dla nazw rodzajowych, takich jak ofi concatsą.
nosid
1
@nosid: Czy najechanie kursorem na każdą stwierdzenie w nowoczesnym IDE szybko ujawni znaczenie? W każdym razie myślę, że może to być w najlepszym przypadku oświadczenie o osobistych preferencjach, ponieważ nadal nie widzę żadnego technicznego powodu, dla którego statyczne importowanie nazw „ogólnych” jest złe - chyba że używasz Notatnika lub VI (M) do programowania, w którym to przypadku masz większe problemy.
kwantowy
Nie powiem, że SDK Scala jest lepsze, ale ... Ups, powiedziałem to.
eirirlar
165

Niestety ta odpowiedź jest prawdopodobnie mało pomocna lub żadna, ale przeprowadziłem analizę kryminalistyczną listy mailingowej Java Lambda, aby sprawdzić, czy mogę znaleźć przyczynę tego projektu. Oto, czego się dowiedziałem.

Na początku była metoda instancji dla Stream.concat (Stream)

Na liście mailingowej wyraźnie widzę, że metoda została pierwotnie zaimplementowana jako metoda instancji, o czym możesz przeczytać w tym wątku Paul Sandoz o operacji konkatacji.

Omawiają w nim kwestie, które mogą wyniknąć z tych przypadków, w których strumień mógłby być nieskończony i co oznaczałoby w tych przypadkach konkatenację, ale nie sądzę, aby to było przyczyną modyfikacji.

W tym innym wątku widać, że niektórzy wczesni użytkownicy JDK 8 kwestionowali zachowanie metody instancji concat, gdy jest używana z argumentami zerowymi.

Ten drugi wątek ujawnia jednak, że projekt metody concat był przedmiotem dyskusji.

Refaktoryzacja do Streams.concat (Stream, Stream)

Ale bez żadnego wyjaśnienia nagle metody zostały zmienione na metody statyczne, jak widać w tym wątku o łączeniu strumieni . Jest to prawdopodobnie jedyny wątek wiadomości, który rzuca trochę światła na tę zmianę, ale nie było dla mnie wystarczająco jasne, aby określić przyczynę refaktoryzacji. Ale widzimy, że wykonali zatwierdzenie, w którym zasugerowali przeniesienie concatmetody z Streamklasy pomocniczej do klasy pomocniczej Streams.

Refaktoryzacja do Stream.concat (Stream, Stream)

Później został ponownie przeniesiony z Streamsdo Stream, ale po raz kolejny bez wyjaśnienia.

Podsumowując, powód powstania projektu nie jest dla mnie do końca jasny i nie mogłem znaleźć dobrego wyjaśnienia. Myślę, że nadal możesz zadać pytanie na liście mailingowej.

Niektóre alternatywy dla konkatenacji strumieni

Ten drugi wątek autorstwa Michaela Hixsona omawia / pyta o inne sposoby łączenia / konkatowania strumieni

  1. Aby połączyć dwa strumienie, powinienem zrobić to:

    Stream.concat(s1, s2)

    nie to:

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

    ... dobrze?

  2. Aby połączyć więcej niż dwa strumienie, powinienem zrobić to:

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

    nie to:

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

    ... dobrze?

Edwin Dalorzo
źródło
6
+1 Niezłe badanie. I użyję tego jako mojego Stream.concat biorąc varargs:public static <T> Stream<T> concat(Stream<T>... streams) { return Stream.of(streams).reduce(Stream.empty(), Stream::concat);}
MarcG
1
Dziś napisałem własną wersję concat i zaraz potem sfinansowałem ten temat. Podpis jest nieco inny, ale dzięki temu jest bardziej ogólny;) na przykład można połączyć Stream <Integer> i Stream <Double> z 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 Dlaczego potrzebujesz Function.identity()mapy? W końcu zwraca ten sam argument, który otrzymał. Nie powinno to mieć żadnego wpływu na wynikowy strumień. Czy coś mi brakuje?
Edwin Dalorzo
1
Czy próbowałeś wpisać to w swoim IDE? Bez .map (identity ()) pojawi się błąd kompilacji. Chcę zwrócić Stream <T>, ale instrukcja: return Stream.of(streams).reduce(Stream.empty(),Stream::concat)zwraca Stream <? extends T>. (Coś <T> jest podtypem Coś <? extends T>, a nie odwrotnie, więc nie można tego rzucić) Dodatkowe .map(identity())rzutowanie <? rozszerza T> do <T>. Dzieje się tak dzięki połączeniu argumentów metod i typów zwracanych w języku Java 8, a także sygnatury metody map (). W rzeczywistości jest to tożsamość Function. <T> ().
kant
1
@kant Nie widzę sensu robienia tego ? extends T, ponieważ możesz użyć konwersji przechwytywania . W każdym razie, oto mój fragment kodu streszczenia . Kontynuujmy dyskusję w streszczeniu.
Edwin Dalorzo
12

Moja biblioteka StreamEx rozszerza funkcjonalność Stream API. W szczególności oferuje metody takie jak dołączanie i poprzedzanie, które rozwiązują ten problem (używają wewnętrznie concat). Te metody mogą akceptować inny strumień, kolekcję lub tablicę varargs. Korzystając z mojej biblioteki, twój problem można rozwiązać w ten sposób (zauważ, że x != 0wygląda to dziwnie w przypadku nieprymitywnego strumienia):

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

Przy okazji jest też skrót do Twojej filteroperacji:

Stream<Integer> stream = StreamEx.of(stream1).without(0)
                                 .append(stream2).without(1)
                                 .append(element).without(2);
Tagir Valeev
źródło
9

Po prostu zrób:

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

gdzie identity()jest statyczny import plików Function.identity().

Łączenie wielu strumieni w jeden strumień jest tym samym, co spłaszczanie strumienia.

Jednak niestety z jakiegoś powodu nie ma flatten()włączonej metody Stream, więc musisz użyć flatMap()funkcji tożsamości.

Hermana
źródło
1

Jeśli nie masz nic przeciwko korzystaniu z bibliotek zewnętrznych, cyklop-reakcja ma rozszerzony typ strumienia, który pozwoli ci to zrobić za pomocą operatorów dołączania / uzupełniania.

Poszczególne wartości, tablice, elementy iteracyjne, strumienie lub strumienie reaktywne Wydawcy mogą być dołączani i dodawani na początku jako metody instancji.

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

[Ujawnienie, że jestem głównym twórcą Cyclops-React]

John McClean
źródło
1

Ostatecznie nie interesuje mnie łączenie strumieni, ale uzyskanie połączonego wyniku przetwarzania każdego elementu wszystkich tych strumieni.

Chociaż łączenie strumieni może okazać się uciążliwe (stąd ten wątek), łączenie ich wyników przetwarzania jest dość łatwe.

Kluczem do rozwiązania jest utworzenie własnego kolektora i upewnienie się, że funkcja dostawcy dla nowego kolektora zwraca za każdym razem tę samą kolekcję ( nie nową ), poniższy kod ilustruje to podejście.

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();
    }
}
Legna
źródło
0

Co powiesz na napisanie własnej metody konkatacji?

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

To przynajmniej sprawia, że ​​twój pierwszy przykład jest dużo bardziej czytelny.

Felix S
źródło
1
Zachowaj ostrożność podczas konstruowania strumieni z wielokrotnej konkatenacji. Dostęp do elementu głęboko połączonego strumienia może skutkować głębokimi łańcuchami wywołań, a nawet StackOverflowError.
Legna