Agregar dos secuencias Java 8 o un elemento adicional a una secuencia

168

Puedo agregar secuencias o elementos adicionales, como este:

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

Y puedo agregar cosas nuevas a medida que avanzo, así:

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

Pero esto es feo, porque concates estático. Si concatfuera un método de instancia, los ejemplos anteriores serían mucho más fáciles de leer:

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

Y

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

Mi pregunta es:

1) ¿Hay alguna buena razón por la cual concates estática? ¿O hay algún método de instancia equivalente que me falta?

2) En cualquier caso, ¿hay una mejor manera de hacerlo?

MarcG
fuente
44
Parece que las cosas no siempre fueron así , pero no puedo encontrar la razón.
Edwin Dalorzo

Respuestas:

126

Si agrega importaciones estáticas para Stream.concat y Stream.of , el primer ejemplo podría escribirse de la siguiente manera:

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

La importación de métodos estáticos con nombres genéricos puede generar un código que se vuelve difícil de leer y mantener ( contaminación del espacio de nombres ). Por lo tanto, podría ser mejor crear sus propios métodos estáticos con nombres más significativos. Sin embargo, para la demostración me quedaré con este nombre.

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 estos dos métodos estáticos (opcionalmente en combinación con importaciones estáticas), los dos ejemplos podrían escribirse de la siguiente manera:

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

El código ahora es significativamente más corto. Sin embargo, estoy de acuerdo en que la legibilidad no ha mejorado. Entonces tengo otra solución.


En muchas situaciones, los recopiladores se pueden usar para ampliar la funcionalidad de las transmisiones. Con los dos recopiladores en la parte inferior, los dos ejemplos podrían escribirse de la siguiente manera:

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

La única diferencia entre la sintaxis deseada y la sintaxis anterior es que debe reemplazar concat (...) con collect (concat (...)) . Los dos métodos estáticos se pueden implementar de la siguiente manera (opcionalmente en combinación con importaciones estáticas):

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

Por supuesto, hay una desventaja con esta solución que debería mencionarse. recoger es una operación final que consume todos los elementos de la secuencia. Además de eso, el recopilador concat crea una ArrayList intermedia cada vez que se usa en la cadena. Ambas operaciones pueden tener un impacto significativo en el comportamiento de su programa. Sin embargo, si la legibilidad es más importante que el rendimiento , aún podría ser un enfoque muy útil.

nosid
fuente
1
No encuentro que el concatcolector sea muy legible. Parece extraño tener un método estático de un solo parámetro llamado así, y también usarlo collectpara la concatenación.
Didier L
@nosid, tal vez una pregunta un poco ortogonal a este hilo, pero ¿por qué reclamas It's a bad idea to import static methods with names? Estoy realmente interesado: creo que hace que el código sea más conciso y legible, y muchas personas a las que les pregunté pensaron lo mismo. ¿Quieres dar algunos ejemplos de por qué eso es generalmente malo?
Quantum
1
@Quantum: ¿Cuál es el significado de compare(reverse(getType(42)), of(6 * 9).hashCode())? Tenga en cuenta que no dije que las importaciones estáticas son una mala idea, pero las importaciones estáticas para nombres genéricos como ofy concatson.
nosid
1
@nosid: ¿No se desplazará sobre cada declaración en un IDE moderno para revelar rápidamente el significado? De todos modos, creo que esto podría ser una declaración de preferencia personal en el mejor de los casos, ya que todavía no veo ninguna razón técnica por la cual las importaciones estáticas para nombres "genéricos" sean malas, a menos que esté usando Notepad o VI (M) para la programación, en cuyo caso Tienes mayores problemas.
Quantum
No voy a decir que el SDK de Scala es mejor, pero ... Vaya, lo dije.
Eirirlar
165

Lamentablemente, esta respuesta probablemente sea de poca o ninguna ayuda, pero hice un análisis forense de la lista de correo Java Lambda para ver si podía encontrar la causa de este diseño. Esto es lo que descubrí.

Al principio había un método de instancia para Stream.concat (Stream)

En la lista de correo puedo ver claramente que el método se implementó originalmente como un método de instancia, como puede leer en este hilo de Paul Sandoz, sobre la operación concat.

En él discuten los problemas que podrían surgir de aquellos casos en los que la secuencia podría ser infinita y qué significaría la concatenación en esos casos, pero no creo que esa sea la razón de la modificación.

Ves en este otro hilo que algunos de los primeros usuarios del JDK 8 cuestionaron el comportamiento del método de instancia concat cuando se usaban con argumentos nulos.

Sin embargo, este otro hilo revela que el diseño del método concat estaba en discusión.

Refactorizado a Streams.concat (Stream, Stream)

Pero sin ninguna explicación, de repente, los métodos se cambiaron a métodos estáticos, como puede ver en este hilo sobre la combinación de secuencias . Este es quizás el único hilo de correo que arroja un poco de luz sobre este cambio, pero no fue lo suficientemente claro para determinar el motivo de la refactorización. Pero podemos ver que hicieron un commit en el que sugirieron mover el concatmétodo fuera de Streamla clase auxiliar Streams.

Refactorizado a Stream.concat (Stream, Stream)

Más tarde, se movió nuevamente de Streamsa Stream, pero una vez más, no hay explicación para eso.

Entonces, en resumen, la razón del diseño no está del todo clara para mí y no pude encontrar una buena explicación. Supongo que aún podrías hacer la pregunta en la lista de correo.

Algunas alternativas para la concatenación de corrientes

Este otro hilo de Michael Hixson discute / pregunta sobre otras formas de combinar / concat streams

  1. Para combinar dos corrientes, debería hacer esto:

    Stream.concat(s1, s2)

    no esta:

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

    ... ¿derecho?

  2. Para combinar más de dos transmisiones, debería hacer esto:

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

    no esta:

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

    ... ¿derecho?

Edwin Dalorzo
fuente
66
+1 Buena investigación. Y usaré esto como mi Stream.concat tomando varargs:public static <T> Stream<T> concat(Stream<T>... streams) { return Stream.of(streams).reduce(Stream.empty(), Stream::concat);}
MarcG
1
Hoy escribí mi propia versión de concat, y justo después de eso financio este tema. La firma es ligeramente diferente, pero gracias a eso es más genérica;) por ejemplo, podría fusionar Stream <Integer> y Stream <Double> a 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 ¿Por qué necesitas un Function.identity()mapa? Después de todo, devuelve el mismo argumento que recibe. Esto no debería tener ningún efecto en la secuencia resultante. ¿Me estoy perdiendo de algo?
Edwin Dalorzo
1
¿Has intentado escribirlo en tu IDE? Sin .map (identity ()) obtendrá un error de compilación. Quiero devolver Stream <T> pero declaración: return Stream.of(streams).reduce(Stream.empty(),Stream::concat)devuelve Stream <? extiende T>. (Algo <T> es subtipo de Algo <? extiende T>, no al revés, por lo que no se puede lanzar) ¿Lanzamiento adicional .map(identity())<? extiende T> a <T>. Ocurre gracias a la combinación de java 8 'tipos de destino' de argumentos de métodos y tipos de retorno y firma del método map (). En realidad es Function. <T> identity ().
kant
1
@kant No veo mucho sentido en hacerlo ? extends T, ya que puedes usar la conversión de captura . En cualquier caso, aquí está mi fragmento de código de Gist. Continuemos la discusión en Gist.
Edwin Dalorzo
12

Mi biblioteca StreamEx amplía la funcionalidad de Stream API. En particular, ofrece métodos como agregar y anteponer que resuelven este problema (internamente lo usan concat). Estos métodos pueden aceptar otra secuencia o colección o matriz de varargs. Usando mi biblioteca, su problema se puede resolver de esta manera (tenga en cuenta que x != 0parece extraño para la secuencia no primitiva):

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

Por cierto, también hay un atajo para su filteroperación:

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

Solo haz:

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

donde identity()es una importación estática de Function.identity().

Concatenar múltiples flujos en un flujo es lo mismo que aplanar un flujo.

Sin embargo, desafortunadamente, por alguna razón no hay ningún flatten()método activado Stream, por lo que debe usarlo flatMap()con la función de identidad.

Germán
fuente
1

Si no le importa usar Bibliotecas de terceros, cyclops-react tiene un tipo de Stream extendido que le permitirá hacer eso a través de los operadores anexar / anteponer.

Los valores individuales, las matrices, los iterables, las secuencias o las secuencias reactivas Los editores se pueden agregar y agregar como métodos de instancia.

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

[Divulgación Soy el desarrollador principal de cyclops-react]

John McClean
fuente
1

Al final del día, no estoy interesado en combinar secuencias, sino en obtener el resultado combinado de procesar cada elemento de todas esas secuencias.

Si bien la combinación de secuencias puede resultar engorrosa (por lo tanto, este hilo), combinar sus resultados de procesamiento es bastante fácil.

La clave para resolver es crear su propio recopilador y asegurarse de que la función de proveedor para el nuevo recopilador devuelva la misma colección cada vez ( no una nueva ), el siguiente código ilustra este enfoque.

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
fuente
0

¿Qué tal escribir tu propio método 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;
}

Esto al menos hace que tu primer ejemplo sea mucho más legible.

Felix S
fuente
1
Tenga precaución cuando construya flujos de concatenación repetida. Acceder a un elemento de una secuencia profundamente concatenada puede dar como resultado cadenas de llamadas profundas, o incluso StackOverflowError.
Legna