Skip to content

2 Operaciones intermedias

Filtrado

Otra de las operaciones intermedias que se pueden realizar sobre un stream es el filtrado de sus elementos, es decir, la generación de un nuevo stream que sólo contenga algunos de los elementos del stream original. Java nos proporciona distintos métodos:

  • Stream<T> distinct(): retorna un nuevo stream con los elementos del stream original, excepto aquellos que estuvieran repetidos. Para determinar que dos elementos son iguales se usará al método equals() del elemento. Ejemplo:

    import java.util.stream.Stream;
    
    public class Distinct{
    
        public void show(){
            Stream.of(1, 3, 2, 3, 1)
                .distinct()
                .forEach(System.out::println);
        }
    
        public static void main(String[] args){
            new Distinct().show();
        }
    }
    
    1
    3
    2
    
    • Stream<T> limit(long maxSize): retorna un nuevo stream con tan sólo maxSize elementos del stream original, atendiendo al orden intrínseco del mismo. Tiene un mal rendimiento en streams paralelos ordenados. Ejemplo:
    public class Limit{
    
        public void show(){
            Stream.of("Ricardo", "Luis", "Paco")
                .limit(2)
                .forEach(System.out::println);
        }
    
        public static void main(String[] args){
            new Limit().show();
        }
    }
    
    Ricardo
    Luis
    
    • Stream<T> filter(Predicate<? super T> predicate): retorna un nuevo stream que sólo incorpora los elementos del stream original que cumplan el predicado recibido. Ejemplo:
    public class Filter{
    
        public void show(){
            Stream.of(9, 12, 15, 24, 37, 6)
                .filter(n -> n % 2 == 0)
                .forEach(System.out::println);
        }
    
        public static void main(String[] args){
            new Filter().show();
        }
    }
    
    12
    24
    6
    
    • Stream<T> skip(long n): retorna un nuevo stream en el que no se incluyen los primeros n elementos del stream original pero sí se incluye el resto. No proporciona un buen rendimiento en streams paralelos ordenados. Ejemplo:
    public class Skip{
    
        public void show(){
            Stream.of(9, 12, 15, 24, 27, 6)
                .skip(3)
                .forEach(System.out::println);
        }
    
        public static void main(String[] args){
            new Skip().show();
        }
    }
    
    24
    37
    6
    
    • default Stream<T> dropWhile(Predicate <? super T> predicate): retorna un nuevo stream con el primer elemento que no cumpla el predicado y el resto de elementos, independientemente de si cumplen el predicado o no. Proporciona un mal rendimiento con streams paralelos ordenados:
    public class DropWhile{
    
        public void show(){
            Stream.of(9, 13, 15, 24, 37, 6)
                .dropWhile(n -> n % 2 != 0)
                .forEach(System.out::println);
        }
    
        public static void main(String[] args){
            new DropWhile().show();
        }
    }
    
    24
    37
    6
    
    • default Stream<T> takeWhile(Predicate<? super T> predicate): mientras los elementos cumplan el predicado se van incluyendo en el stream, pero en cuanto se encuentra un elemento que no cumple el predicado se deja de incluir el resto de elementos, incluso aunque cumplan el predicado. Ejemplo:
    public class TakeWhile{
    
        public void show(){
            Stream.of(9, 13, 15, 24, 37, 6)
                .takeWhile(n -> n % 2 != 0)
                .forEach(System.out::println);
        }
    
        public static void main(String[] args){
            new TakeWhile().show();
        }
    }
    
    9
    13
    15
    

Ordenación

Algunos streams son ordenados, es decir, que sus elementos poseen un determinado orden intrínseco significativo, conocido como encounter order. Por ejemplo, un stream cuya fuente de datos corresponda a una lista creará un stream ordenado, cuyo encounter order será el orden en el que los elementos están situados en la lista. Sin embargo, otros streams no son ordenados, en el sentido de que sus elementos no tienen un orden intrínseco significativo. Por ejemplo, un stream cuya fuente de datos sea un conjunto (Set) será un stream sin encounter order, ya que un conjunto los elementos no tienen un orden preestablecido.

El hecho de que un stream sea ordenado o no dependerá del tipo de fuente de datos asociada y de las operaciones intermedias anteriores que hayamos realizado mediante las que se ha obtenido el stream.

Algunas operaciones trabajan por defecto en base a este encounter order, imponiendo una restricción acerca del orden en el que los elementos deben ser procesados, como por ejemplo las operaciones intermedias limit o skip.

Sin embargo, existen otras operaciones que no tienen en cuenta el encounter order, como por ejemplo forEach. Si se ejecuta sobre un stream paralelo, no hay ninguna garantía sobre en que orden se aplica la acción a los elementos. Si queremos que sí se tenga en cuenta el orden, entonces tendríamos que usar el método void forEachOrdered(Consumer<? super T> action). Normalmente se usa encadenado después de llamar a un método de ordenación que habrá ordenado el stream. La ventaja de este método es que se garantiza que la acción se aplica a los elementos en el orden intrínseco del stream, incluso aunque éste se trate de un stream paralelo, aunque conlleve un peor rendimiento.

Al trabajar con streams secuenciales, el encounter order no afecta al rendimiento de la aplicación, pero si trabajamos con streams paralelos, el empleo del encounter order por parte de algunos operadores pueden afectar en gran medida al rendimiento general de la aplicación. Dependiendo de la operación de la que se trate, será necesario procesar a la vez más de un elemento del stream a partir del anterior, en el que no se tenga en cuenta el encounter order. Al ejecutar el método unordered(), tan solo se está creando un nuevo stream en el que se ha borrado el indicador de que el encounter order debe tenerse en cuenta. Normalmente, esta operación de desactivación del encounter order se realiza con el objetivo de mejorar el rendimiento en streams paralelos.

Por otra parte, si queremos obtener un stream ordenado a partir de otro desordenado o a partir de otro stream ordenado pero por un orden distinto, podemos usar el método sorted(), en cuyo caso los elementos del stream deben implementar la interfaz Comparable para determinar el orden en el que deben ser ordenados. Otra posibilidad es usar una versión sobrecargada de dicho método que recibe un objeto Comparator como argumento. Ejemplo:

public class Sorted{

    public void show(){
        Stream.of("Ricardo", "Luis", "Paco")
            .sorted()
            .limit(2)
            .forEach(System.out::println);
    }

    public static void main(String[] args){
        new Sorted().show();
    }
}
Luis
Paco

Ejemplo utilizando Comparator:

public class SortedReverseOrder{

    public void show(){
        Stream.of("Ricardo", "Luis", "Paco")
            .sorted(Comparator.reverseOrder())
            .limit(2)
            .forEach(System.out::println);
    }

    public static void main(String[] args){
        new SortedReverseOrder().show();
    }
}
Ricardo
Paco

Transformación

Java nos proporciona distintos métodos:

  • <R> Stream<R> map(Function<? super T, ? extends R> mapper): retorna un nuevo stream obtenido a partir de aplicar la función de transformación indicada a cada uno de los elementos del stream original. El tipo del stream resultante corresponderá al tipo de retorno de la función de transformación, que puede ser distinto al tipo del stream original, pero contendrá tantos elementos como éste.

    public class Map{
    
        public void show(){
            Stream.of(20, 27, 31)
                .map(n -> "Número " + n)
                .forEach(System.out::println);
        }
    
        public static void main(String[] args){
            new Map().show();
        }
    }
    
    Número 20
    Número 27
    Número 31
    
  • Métodos que permiten obtener un stream de un tipo primitivo a partir de uno que no lo sea:

    • DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper): retorna un DoubleStream correspondiente de aplicar a cada elemento del stream original la función a double recibida:

      public class MapToDouble{
      
          public void show(){
              Stream.of(20, 27, 31)
                  .mapToDouble(n -> n * 0.5)
                  .forEach(System.out::println);
          }
      
          public static void main(String[] args){
              new MapToDouble().show();
          }
      }
      
      10.0
      13.5
      15.5
      
    • IntStream mapToInt(ToIntFunction<? super T> mapper): retorna un IntStream correspondiente de aplicar a cada elemento del stream original la función de conversión a int recibida. Ejemplo:

      public class MapToInt{
      
          public void show(){
              Stream.of("Ricardo", "Luis Miguel", "Paco")
                  .mapToInt(n -> n.length)
                  .forEach(System.out::println);
          }
      
          public static void main(String[] args){
              new MapToInt().show();
          }
      }
      
      7
      11
      4
      
    • LongStream mapToLong(ToLongFunction<? super T> mapper): retorna un LongStream correspondiente de aplicar a cada elemento del stream original la función de conversión a long recibida.

      public class MapToLong{
      
          public void show(){
              Stream.of(55000, 60000, 72500)
                  .mapToLong(n -> (long)n * n)
                  .forEach(System.out::println);
          }
      
          public static void main(String[] args){
              new MapToLong().show();
          }
      }
      
      3025000000
      3600000000
      5256250000
      
  • <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper): cuando una función de transformación retorna un stream y se aplica esta función con el método map, el stream resultante es un Stream<Stream<Tipo>>. En estos casos, es más óptimo obtener un único Stream<Tipo> que contuviera concatenados todos los elementos de todos los substreams. A este proceso se le conoce como aplanado (flat) de substreams.

    public class FlatMap{
    
        public void show(){
            Stream.of(1, 2, 3)
                .flatMap(n -> IntStream.rangeClosed(1, n).boxed())
                .forEach(System.out::println);
        }
    
        public static void main(String[] args){
            new FlatMap().show();
        }
    }
    
    1
    1
    2
    1
    2
    3
    

    En el ejemplo, muestra los valores 1 (proveniente del primer substream), 1, 2 (provenientes del segundo substream) y 1, 2 y 3, provenientes del tercer substream, en este orden.

    Si nos interesa que el tipo del stream resultante fuera primitivo, podemos usar los métodos flatMapToDouble, flatMapToInt o flatMapToLong.

    public class FlatMapToInt{
    
        public void show(){
            Stream.of(1, 2, 3)
                .flatMapToInt(n -> IntStream.rangeClosed(1, n))
                .forEach(System.out::println);
        }
    
        public static void main(String[] args){
            new FlatMapToInt().show();
        }
    }
    
    1
    1
    2
    1
    2
    3