Introducción a las secuencias de Java 8

I

Introducción

El tema principal de este artículo son los temas de procesamiento de datos avanzados que utilizan una nueva funcionalidad agregada a Java 8: la API Stream y la API Collector.

Para aprovechar al máximo este artículo, ya debe estar familiarizado con las principales API de Java, la Object y String clases y la API de colección.

API de transmisión

los java.util.stream El paquete consta de clases, interfaces y muchos tipos para permitir operaciones de estilo funcional sobre elementos. Java 8 introduce un concepto de Stream que permite al programador procesar datos de forma descriptiva y confiar en una arquitectura de múltiples núcleos sin la necesidad de escribir ningún código especial.

¿Qué es un Stream?

UN Stream representa una secuencia de objetos derivados de una fuente, sobre la cual se pueden realizar operaciones agregadas.

Desde un punto de vista puramente técnico, una secuencia es una interfaz escrita, una secuencia de T. Esto significa que una secuencia se puede definir para cualquier tipo de objeto, una secuencia de números, una secuencia de caracteres, una secuencia de personas o incluso un arroyo de una ciudad.

Desde el punto de vista del desarrollador, es un concepto nuevo que puede parecer una colección, pero de hecho es muy diferente de una colección.

Hay algunas definiciones clave que debemos analizar para comprender esta noción de Stream y por qué se diferencia de una colección:

Una secuencia no contiene datos

El concepto erróneo más común que me gustaría abordar primero: una corriente no contener cualquier dato. Es muy importante tenerlo en cuenta y comprenderlo.

No hay datos en una secuencia, sin embargo, hay datos almacenados en una colección.

UN Collection es una estructura que contiene sus datos. Un Stream está ahí para procesar los datos y extraerlos de la fuente dada, o moverlos a un destino. La fuente puede ser una colección, aunque también puede ser una matriz o un recurso de E / S. La transmisión se conectará a la fuente, consumirá los datos y procesará los elementos que contiene de alguna manera.

Una transmisión no debería modificar la fuente

Una secuencia no debe modificar la fuente de los datos que procesa. En realidad, esto no lo hace cumplir el compilador de la JVM en sí, por lo que es simplemente un contrato. Si voy a construir mi propia implementación de una secuencia, no debo modificar la fuente de los datos que estoy procesando. Aunque está perfectamente bien modificar los datos en la transmisión.

¿Por qué es así? Porque si queremos procesar estos datos en paralelo, los vamos a distribuir entre todos los núcleos de nuestros procesadores y no queremos tener ningún tipo de problemas de visibilidad o sincronización que puedan dar lugar a malos rendimientos o errores. Evitar este tipo de interferencia significa que no debemos modificar la fuente de los datos mientras los procesamos.

Una fuente puede ser ilimitada

Probablemente el punto más poderoso de estos tres. Significa que el flujo en sí mismo puede procesar tantos datos como queramos. Ilimitado no significa que una fuente deba ser infinita. De hecho, una fuente puede ser finita, pero es posible que no tengamos acceso a los elementos contenidos en esa fuente.

Suponga que la fuente es un archivo de texto simple. Un archivo de texto tiene un tamaño conocido incluso si es muy grande. Suponga también que los elementos de esa fuente son, de hecho, las líneas de este archivo de texto.

Ahora, es posible que sepamos el tamaño exacto de este archivo de texto, pero si no lo abrimos y revisamos manualmente el contenido, nunca sabremos cuántas líneas tiene. Esto es lo que significa ilimitado: es posible que no siempre sepamos de antemano la cantidad de elementos que una secuencia procesará desde la fuente.

Esas son las tres definiciones de una corriente. De modo que podemos ver en esas tres definiciones que un flujo realmente no tiene nada que ver con una colección. Una colección contiene sus datos. Una colección puede modificar los datos que contiene. Y, por supuesto, una colección contiene una cantidad conocida y finita de datos.

Características de la corriente

  • Secuencia de elementos: las secuencias proporcionan un conjunto de elementos de un tipo particular de manera secuencial. La secuencia obtiene un elemento a pedido y nunca almacena un elemento.
  • Fuente: las secuencias toman una colección, una matriz o recursos de E / S como fuente para sus datos.
  • Operaciones agregadas: las transmisiones admiten operaciones agregadas como forEach, filtro, mapa, ordenado, coincidencia y otras.
  • Anulación: la mayoría de las operaciones sobre una secuencia devuelven una secuencia, lo que significa que sus resultados se pueden encadenar. La función de estas operaciones es tomar datos de entrada, procesarlos y devolver la salida de destino. los collect() El método es una operación de terminal que generalmente está presente al final de las operaciones para indicar el final del procesamiento de la secuencia.
  • Iteraciones automatizadas: las operaciones de transmisión llevan a cabo iteraciones internamente sobre la fuente de los elementos, a diferencia de las colecciones donde se requiere una iteración explícita.

Crear una secuencia

Podemos generar un flujo con la ayuda de algunos métodos:

corriente()

los stream() El método devuelve la secuencia secuencial con una colección como fuente. Puede utilizar cualquier colección de objetos como fuente:

private List<String> list = new Arrays.asList("Scott", "David", "Josh");
list.stream();
paraleloStream ()

los parallelStream() El método devuelve una secuencia paralela con una colección como fuente:

private List<String> list = new Arrays.asList("Scott", "David", "Josh");
list.parallelStream().forEach(element -> method(element));

Lo que pasa con los flujos paralelos es que al ejecutar una operación de este tipo, el tiempo de ejecución de Java segrega el flujo en múltiples subflujos. Ejecuta las operaciones agregadas y combina el resultado. En nuestro caso, llama al method con cada elemento de la corriente en paralelo.

Sin embargo, esto puede ser un arma de doble filo, ya que ejecutar operaciones pesadas de esta manera podría bloquear otros flujos paralelos ya que bloquea los subprocesos en el grupo.

Corriente de()

La estática of() El método se puede utilizar para crear una secuencia a partir de una matriz de objetos u objetos individuales:

Stream.of(new Employee("David"), new Employee("Scott"), new Employee("Josh"));
Stream.builder ()

Y por último, puede utilizar la estática .builder() método para crear una secuencia de objetos:

Stream.builder<String> streamBuilder = Stream.builder();

streamBuilder.accept("David");
streamBuilder.accept("Scott");
streamBuilder.accept("Josh");

Stream<String> stream = streamBuilder.build();

Llamando al .build() , empaquetamos los objetos aceptados en un Stream normal.

Filtrar con una corriente

public class FilterExample {
    public static void main(String[] args) {
    List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Orange");

    // Traditional approach
    for (String fruit : fruits) {
        if (!fruit.equals("Orange")) {
            System.out.println(fruit + " ");
        }
    }

    // Stream approach
    fruits.stream() 
            .filter(fruit -> !fruit.equals("Orange"))
            .forEach(fruit -> System.out.println(fruit));
    }
}

Un enfoque tradicional para filtrar una sola fruta sería con un bucle clásico para cada uno.

El segundo enfoque utiliza una secuencia para filtrar los elementos de la secuencia que coinciden con el predicado dado, en una nueva secuencia que es devuelta por el método.

Además, este enfoque utiliza una forEach() método, que realiza una acción para cada elemento del flujo devuelto. Puede reemplazar esto con algo llamado referencia de método. En Java 8, una referencia de método es la sintaxis abreviada de una expresión lambda que ejecuta solo un método.

La sintaxis de referencia del método es simple e incluso puede reemplazar la expresión lambda anterior .filter(fruit -> !fruit.equals("Orange")) con eso:

Object::method;

Actualicemos el ejemplo y usemos referencias de métodos y veamos cómo se ve:

public class FilterExample {
    public static void main(String[] args) {
    List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Orange");

    fruits.stream()
            .filter(FilterExample::isNotOrange)
            .forEach(System.out::println);
    }
    
    private static boolean isNotOrange(String fruit) {
        return !fruit.equals("Orange");
    }
}

Las transmisiones son más fáciles y mejores de usar con expresiones Lambda y este ejemplo destaca lo simple y limpia que se ve la sintaxis en comparación con el enfoque tradicional.

Mapeo con una corriente

Un enfoque tradicional sería iterar a través de una lista con un bucle for mejorado:

List<String> models = Arrays.asList("BMW", "Audi", "Peugeot", "Fiat");

System.out.print("Imperative style: " + "n");

for (String car : models) {
    if (!car.equals("Fiat")) {
        Car model = new Car(car);
        System.out.println(model);
    }
}

Por otro lado, un enfoque más moderno es utilizar un Stream para mapear:

List<String> models = Arrays.asList("BMW", "Audi", "Peugeot", "Fiat");
        
System.out.print("Functional style: " + "n");

models.stream()
        .filter(model -> !model.equals("Fiat"))
//      .map(Car::new)                 // Method reference approach
//      .map(model -> new Car(model))  // Lambda approach
        .forEach(System.out::println);

Para ilustrar el mapeo, considere esta clase:

private String name;
    
public Car(String model) {
    this.name = model;
}

// getters and setters

@Override
public String toString() {
    return "name="" + name + """;
}

Es importante señalar que el models lista es una lista de cadenas, no una lista de Car. los .map() El método espera un objeto de tipo T y devuelve un objeto de tipo R.

Estamos convirtiendo String en un tipo de automóvil, esencialmente.

Si ejecuta este código, el estilo imperativo y el estilo funcional deberían devolver lo mismo.

Recolectar con una corriente

A veces, querrá convertir una secuencia en una colección o mapa. Utilizando la clase de utilidad Collectors y las funcionalidades que ofrece:

List<String> models = Arrays.asList("BMW", "Audi", "Peugeot", "Fiat");

List<Car> carList = models.stream()
        .filter(model -> !model.equals("Fiat"))
        .map(Car::new)
        .collect(Collectors.toList());

Coincidencia con una corriente

Una tarea clásica es categorizar objetos de acuerdo con ciertos criterios. Podemos hacer esto al hacer coincidir la información necesaria con la información del objeto y verificar si eso es lo que necesitamos:

List<Car> models = Arrays.asList(new Car("BMW", 2011), new Car("Audi", 2018), new Car("Peugeot", 2015));

boolean all = models.stream().allMatch(model -> model.getYear() > 2010);
System.out.println("Are all of the models newer than 2010: " + all);

boolean any = models.stream().anyMatch(model -> model.getYear() > 2016);
System.out.println("Are there any models newer than 2016: " + any);

boolean none = models.stream().noneMatch(model -> model.getYear() < 2010);
System.out.println("Is there a car older than 2010: " + none);
  • allMatch() – Devoluciones true si todos los elementos de esta secuencia coinciden con el predicado proporcionado.
  • anyMatch() – Devoluciones true si algún elemento de esta secuencia coincide con el predicado proporcionado.
  • noneMatch() – Devoluciones true si ningún elemento de esta secuencia coincide con el predicado proporcionado.

En el ejemplo de código anterior, todos los predicados dados se satisfacen y todos devolverán true.

Conclusión

La mayoría de las personas utilizan Java 8. Aunque no todo el mundo utiliza Streams. El hecho de que representen un enfoque más nuevo de la programación y representen un toque con la programación de estilo funcional junto con las expresiones lambda para Java, no significa necesariamente que sea un enfoque mejor. Simplemente ofrecen una nueva forma de hacer las cosas. Depende de los propios desarrolladores decidir si confiar en la programación de estilo funcional o imperativo. Con un nivel suficiente de ejercicio, la combinación de ambos principios puede ayudarlo a mejorar su software.

Como siempre, le recomendamos que consulte el documentación oficial para informacion adicional.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad