Cómo Fusionar Streams en Java para Programadores

Merging Streams in Java: Una Guía Completa

En este artículo, explicaremos diferentes maneras de fusionar Streams en Java, una operación que no resulta muy intuitiva. Los Streams son una poderosa herramienta en Java que permite trabajar con secuencias de datos de manera fluida y mantenible. Sin embargo, fusionar múltiples Streams puede presentar ciertos retos. A lo largo de esta entrada, exploraremos varias maneras de hacerlo, desde métodos simples ofrecidos por la biblioteca estándar de Java hasta soluciones más complejas y potentes proporcionadas por bibliotecas adicionales como StreamEx y jOOλ.

1. Overview

La clase Stream de Java, introducida en Java 8, tiene métodos estáticos útiles para trabajar con secuencias de datos. A continuación, nos centraremos en cómo podemos fusionar diferentes Streams utilizando principalmente el método concat() de la clase Stream.

2. Using Plain Java

2.1. Merging Two Streams

La forma más sencilla de combinar dos Streams es usando el método estático Stream.concat(). Este método toma dos Streams como parámetros y devuelve un nuevo Stream que contiene todos los elementos de ambos.

@Test
public void whenMergingStreams_thenResultStreamContainsElementsFromBoth() {
    Stream stream1 = Stream.of(1, 3, 5);
    Stream stream2 = Stream.of(2, 4, 6);

    Stream resultingStream = Stream.concat(stream1, stream2);

    assertEquals(
      Arrays.asList(1, 3, 5, 2, 4, 6),
      resultingStream.collect(Collectors.toList()));
}

En este código, creamos dos Streams de números enteros y, usando Stream.concat, obtenemos un Stream resultante que contiene todos los elementos de ambos. El uso de assertEquals verifica que el resultado es el esperado.

2.2. Merging Multiple Streams

Cuando necesitamos fusionar más de dos Streams, la operación se vuelve un poco más complicada. Una posibilidad es concatenar el primer par de Streams y luego fusionar el resultado con el siguiente, y así sucesivamente. Veamos cómo se realiza esto:

@Test
public void given3Streams_whenMerged_thenResultStreamContainsAllElements() {
    Stream stream1 = Stream.of(1, 3, 5);
    Stream stream2 = Stream.of(2, 4, 6);
    Stream stream3 = Stream.of(18, 15, 36);

    Stream resultingStream = Stream.concat(Stream.concat(stream1, stream2), stream3);

    assertEquals(
      Arrays.asList(1, 3, 5, 2, 4, 6, 18, 15, 36),
      resultingStream.collect(Collectors.toList()));
}

Sin embargo, este enfoque se vuelve poco manejable con más Streams. Una alternativa más eficiente y legible es utilizar flatMap en una lista de Streams.

@Test
public void given4Streams_whenMerged_thenResultStreamContainsAllElements() {
    Stream stream1 = Stream.of(1, 3, 5);
    Stream stream2 = Stream.of(2, 4, 6);
    Stream stream3 = Stream.of(18, 15, 36);
    Stream stream4 = Stream.of(99);

    Stream resultingStream = Stream.of(stream1, stream2, stream3, stream4)
                                             .flatMap(i -> i);

    assertEquals(
      Arrays.asList(1, 3, 5, 2, 4, 6, 18, 15, 36, 99),
      resultingStream.collect(Collectors.toList()));
}

En este ejemplo, primero creamos un nuevo Stream que contiene los cuatro Streams (lo que resulta en un Stream<Stream>) y luego aplicamos flatMap para aplanar los Streams en un único Stream.

3. Using StreamEx

StreamEx es una biblioteca de Java de código abierto que extiende las posibilidades de los Streams de Java 8. Utiliza la clase StreamEx como mejora de la interfaz Stream del JDK.

3.1. Merging Streams

La biblioteca StreamEx permite fusionar Streams usando el método de instancia append():

@Test
public void given4Streams_whenMerged_thenResultStreamContainsAllElements() {
    Stream stream1 = Stream.of(1, 3, 5);
    Stream stream2 = Stream.of(2, 4, 6);
    Stream stream3 = Stream.of(18, 15, 36);
    Stream stream4 = Stream.of(99);

    Stream resultingStream = StreamEx.of(stream1)
                                              .append(stream2)
                                              .append(stream3)
                                              .append(stream4);

    assertEquals(
      Arrays.asList(1, 3, 5, 2, 4, 6, 18, 15, 36, 99),
      resultingStream.collect(Collectors.toList()));
}

Dado que se trata de un método de instancia, podemos encadenar fácilmente múltiples Streams. También se puede crear una List a partir del Stream usando toList() si tipamos la variable resultante al tipo StreamEx.

3.2. Merging Streams Using prepend

La biblioteca StreamEx contiene un método que agrega elementos antes que otros, llamado prepend():

@Test
public void given3Streams_whenPrepended_thenResultStreamContainsAllElements() {
    Stream stream1 = Stream.of("foo", "bar");
    Stream openingBracketStream = Stream.of("[");
    Stream closingBracketStream = Stream.of("]");

    Stream resultingStream = StreamEx.of(stream1)
                                              .append(closingBracketStream)
                                              .prepend(openingBracketStream);

    assertEquals(
      Arrays.asList("[", "foo", "bar", "]"),
      resultingStream.collect(Collectors.toList()));
}

Este método permite una manipulación más rica de los Streams, haciendo que la lógica sea más clara al agrupar adecuadamente elementos antes y después.

4. Using Jooλ

jOOλ es una biblioteca compatible con JDK 8 que proporciona extensiones útiles a la biblioteca estándar. La abstracción principal de Stream en jOOλ se llama Seq. Es importante destacar que este Stream es secuencial y ordenado, por lo que llamar a parallel() no tendrá efecto.

4.1. Merging Streams

Al igual que la biblioteca StreamEx, jOOλ ofrece un método append():

@Test
public void given2Streams_whenMerged_thenResultStreamContainsAllElements() {
    Stream seq1 = Stream.of(1, 3, 5);
    Stream seq2 = Stream.of(2, 4, 6);

    Stream resultingSeq = Seq.ofType(seq1, Integer.class)
                                       .append(seq2);

    assertEquals(
      Arrays.asList(1, 3, 5, 2, 4, 6),
      resultingSeq.collect(Collectors.toList()));
}

También existe un método de conveniencia toList() si tipamos la variable resultante al tipo Seq de jOOλ.

4.2. Merging Streams With prepend

Como era de esperarse, dado que existe un método append(), también hay un método prepend() en jOOλ:

@Test
public void given3Streams_whenPrepending_thenResultStreamContainsAllElements() {
    Stream seq = Stream.of("foo", "bar");
    Stream openingBracketSeq = Stream.of("[");
    Stream closingBracketSeq = Stream.of("]");

    Stream resultingStream = Seq.ofType(seq, String.class)
                                         .append(closingBracketSeq)
                                         .prepend(openingBracketSeq);

    Assert.assertEquals(
      Arrays.asList("[", "foo", "bar", "]"),
      resultingStream.collect(Collectors.toList()));
}

5. Conclusion

Hemos visto que fusionar streams es relativamente sencillo utilizando JDK 8. Sin embargo, cuando necesitamos realizar múltiples fusiones, puede ser beneficioso emplear bibliotecas como StreamEx o jOOλ. Estas bibliotecas no solo mejoran la legibilidad del código, sino que también ofrecen funcionalidades adicionales que resultan útiles en desarrollos más complejos.

Para los programadores que están buscando optimizar su trabajo con Streams en Java, considerar estas herramientas puede ser una elección acertada. Además, experimentar con distintos enfoques puede proporcionar nuevas perspectivas y mejores prácticas en el desarrollo de soluciones en Java.