Guía de coleccionistas de Java 8: Guía definitiva de toList()

En esta guía, entraremos en detalles sobre cómo funciona la recopilación de secuencias en listas y cómo puede usar recopiladores predefinidos y definir los suyos propios para convertir una secuencia en una lista en Java 8.

Introducción

Las transmisiones no contienen ningún dato por sí mismas, simplemente la transmiten desde una fuente. Sin embargo, las rutinas de código comunes esperan algún tipo de estructura para mantener los resultados después de procesar los datos. Es por eso que, después de las operaciones intermedias (opcionales), Stream API proporciona formas de convertir los elementos sobre los que puede haber actuado en colecciones, como listas, que puede usar más en su código.

Estas formas incluyen aplicar:

  • Colectores predefinidos o personalizados:
1
<R,A> R collect(Collector<? super T,A,R> collector);

Este es el enfoque más común, limpio y simple que puede utilizar, y lo cubriremos primero.

  • Proveedores, acumuladores y combinadores (separando un ‘Collector’ en sus partes constituyentes):
1
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);

O bien, puede terminar una transmisión convirtiéndola en una matriz. Luego, convierta esa matriz en una lista. Esto se debe a que la API ya tiene dos métodos para producir arreglos. Incluyen:

1
Object[] toArray();

Que devuelve una matriz que contiene los elementos de un flujo.

1
<A> A[] toArray(IntFunction<A[]> generator);

Donde, el generador es una función que produce una nueva matriz del tipo deseado y la longitud proporcionada

Estos métodos de producción de matrices están obligados a hacer que el código sea más detallado. Y eso puede hacer que su código sea menos legible. Sin embargo, al final, aún lo ayudarán a convertir una secuencia en una lista.

If you'd like to read more about array to list conversion, read up on Cómo convertir una matriz Java a ArrayList.

De lo contrario, esta guía analizará en detalle cómo funcionan todos estos enfoques. También agregará algunos trucos sucios que también lo ayudarán a convertir. Sin embargo, tenga cuidado con ellos: tales retoques dañarán el rendimiento de su código.

Cómo convertir una secuencia en una lista mediante recopiladores

La documentación oficial define un recopilador como una implementación que es:

  1. mutable;
  2. Una operación de reducción;

Y:

[3] que acumula elementos de entrada en un contenedor de resultados mutable, [4] opcionalmente transformando el resultado acumulado en una representación final después de que se hayan procesado todos los elementos de entrada.

Tenga en cuenta cómo estas 4 condiciones parecen un bocado. Pero, como veremos a continuación, no son tan difíciles de cumplir.

Recopiladores predefinidos

La API de flujo de Java 8 funciona en conjunto con la API de recopiladores. La clase Collectors ofrece recopiladores listos para usar que aplican el proveedor-acumulador-combinador en sus implementaciones.

Por lo tanto, el uso de las instalaciones de la clase de utilidad ‘Collectors’ limpiará significativamente su código.

El método que podemos usar de la clase Collectors es Collectors.toList().

Para convertir un flujo en una lista usando Collectors preconstruidos, simplemente lo recolectamos() en una lista:

1
2
List list = Stream.of("David", "Scott", "Hiram").collect(Collectors.toList());
System.out.println(String.format("Class: %s\nList: %s", list.getClass(), list));

Este ejemplo es bastante simple y solo trata con cadenas:

1
2
Class: class java.util.ArrayList
List: [David, Scott, Hiram]

Sin embargo, si no está trabajando con cadenas o tipos más simples, es probable que tenga que mapear () sus objetos antes de recopilarlos, lo que suele ser el caso. Definamos un objeto ‘Donante’ simple y un ‘BloodBank’ que realice un seguimiento de ellos, y convierta un Flujo de Donantes en una Lista.

Convertir secuencia en lista con map() y recopilar()

Comencemos declarando una clase ‘Donante’ para modelar un donante de sangre:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Donor implements Comparable<Donor>{

    private final String name;
    //O-, O+, A-, A+, B-, B+, AB-, AB+
    private final String bloodGroup;
    //The amount of blood donated in mls
    //(An adult can donate about 450 ml of blood)
    private final int amountDonated;

    public Donor(String name, String bloodGroup, int amountDonated) {
        //Validation of the name and the bloodtype should occur here
        this.name = name;
        this.bloodGroup = bloodGroup;
        this.amountDonated = amountDonated;
    }
    
    @Override
    public int compareTo(Donor otherDonor) {
        return Comparator.comparing(Donor::getName)
                .thenComparing(Donor::getBloodGroup)
                .thenComparingInt(Donor::getAmountDonated)
                .compare(this, otherDonor);
    }
}

Es recomendable implementar aquí la interfaz Comparable ya que facilita el ordenamiento y clasificación de los objetos Donante en las colecciones. Siempre puede proporcionar ‘Comparadores’ personalizados en su lugar, sin embargo, una entidad ‘Comparable’ es simplemente más fácil y limpia para trabajar.

Luego, definimos una interfaz BloodBank, que especifica que los bancos de sangre pueden recibir una donación de un Donor, ​​así como devolver todos los tipos disponibles:

1
2
3
4
public interface BloodBank {
    void receiveDonationFrom(Donor donor);
    List<String> getAvailableTypes();    
}

El siguiente paso es crear una implementación concreta de un BloodBank. Dado que todas las implementaciones concretas aceptarán donantes, y solo el enfoque para obtener los tipos disponibles dependerá de la implementación, creemos una clase abstracta como intermediario:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public abstract class AbstractBloodBank implements BloodBank {
    // Protected so as to expose the donors' records to all other blood banks that will extend this AbstractBloodBank
    protected final List<Donor> donors;

    public AbstractBloodBank() {
        this.donors = new ArrayList<>();
    }

    @Override
    public void receiveDonationFrom(Donor donor) {
        donors.add(donor);
    }

    // Classes that extend AbstractBloodBank should offer their unique implementations
    // of extracting the blood group types from the donors' records 
    @Override
    public abstract List<String> getAvailableTypes();
}

Finalmente, podemos seguir adelante y crear una implementación concreta y mapear() la lista de Donantes a su tipo de sangre, dentro de un Stream y recolectar() de nuevo en una lista, devolviendo los tipos de sangre disponibles:

1
2
3
4
5
6
7
public class CollectorsBloodBank extends AbstractBloodBank {

    @Override
    public List<String> getAvailableTypes() {
        return donors.stream().map(Donor::getBloodGroup).collect(Collectors.toList());
    }
}

Puede ‘asignar ()’ a los donantes a cualquiera de los campos del objeto y devolver una lista de esos campos, como ‘cantidad donada’ o ’nombre’ también. Tener un campo comparable también hace posible clasificarlos a través de sorted().

If you'd like to read more about the sorted() method, read our Cómo ordenar una lista con Stream.sorted().

En su lugar, podría devolver todas las instancias de Donor simplemente llamando a collect() en su Stream:

1
2
3
4
@Override
public List<Donor> getAvailableDonors() {
    return donors.stream().collect(Collectors.toList());
}

Sin embargo, no está limitado a recopilar un flujo en una lista; aquí es donde entra en juego el método collectingAndThen().

Convertir flujo en lista con Collectors.collectingAndThen()

Anteriormente consultamos la documentación oficial y afirma que los recolectores tienen la capacidad de:

transformar opcionalmente el resultado acumulado en una representación final después de que se hayan procesado todos los elementos de entrada.

El resultado acumulado en CollectorsBloodBank, por ejemplo, está representado por Collectors.toList(). Podemos transformar aún más este resultado utilizando el método Collectors.collectorsAndThen().

La buena práctica requiere que uno devuelva objetos de colección inmutables. Entonces, si tuviéramos que apegarnos a esta práctica, se puede agregar un paso final a la conversión de flujo a lista:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class UnmodifiableBloodBank extends AbstractBloodBank {

    @Override
    public List<String> getAvailableTypes() {
        return donors.stream()
                .map(Donor::getBloodGroup)
                .collect(
                        Collectors.collectingAndThen(
                                //Result list
                                Collectors.toList(),
                                //Transforming the mutable list into an unmodifiable one
                                Collections::unmodifiableList
                        )
                );
    }
}

Alternativamente, también puede poner cualquier Función<R, RR> como finalizador aquí.

Si quieres leer más, también puedes leer nuestra detallada guía sobre el método Collectors.collectingAndThen() (¡próximamente!)

Convertir flujo en lista con proveedores, acumuladores y combinadores

En lugar de usar recopiladores predefinidos, puede usar Proveedores, Acumuladores y Combinadores separados. Estos se implementan como Proveedor<R>, BiConsumidor<R, ? super T> y BiConsumer<R,R>, que encajan cómodamente en un collect() en lugar de un Collector predefinido.

Echemos un vistazo a cómo puede utilizar esta flexibilidad para devolver todos los tipos disponibles:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class LambdaBloodBank extends AbstractBloodBank {

    @Override
    public List<String> getAvailableTypes() {
        return donors.stream() //(1)
                .map(donor -> donor.getBloodGroup()) //(2)
                .collect(
                        () -> new ArrayList<String>(), //(3)
                        (bloodGroups, bloodGroup) -> bloodGroups.add(bloodGroup), //(4)
                        (resultList, bloodGroups) -> resultList.addAll(bloodGroups) //(5)
                );
    }
}

La implementación anterior aplica el patrón requerido proveedor-acumulador-combinador en unos pocos pasos:

En primer lugar, convierte el campo de la lista donantes en un flujo de elementos Donantes.

Recuerde, LambdaBloodBank puede acceder al campo donantes porque amplía AbstractBloodBank. Y, el campo donantes tiene acceso protegido en la clase AbstractBloodBank.

Luego, se realiza una operación de mapa intermedia en el flujo de ‘Donantes’. La operación crea una nueva secuencia que contiene los valores String que representan los tipos de grupos sanguíneos de los donantes. Después. un contenedor de resultados que es mutable, es decir, se crea el proveedor del recopilador. Este contenedor de proveedores se denominará de ahora en adelante grupos sanguíneos.

Agregamos cada tipo de grupo sanguíneo (llamado “grupo sanguíneo” en este paso) de la transmisión al contenedor mutable: “grupos sanguíneos”. En otras palabras, la acumulación ocurre en este paso.

El contenedor de proveedor mutable grupos de sangre se agrega al contenedor de resultados conocido como resultList en este paso. Este es, por lo tanto, el paso combinador.

Podemos mejorar aún más el método getAvailableTypes() de LambdaBloodBank utilizando método de referencias en lugar de lambdas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class MembersBloodBank extends AbstractBloodBank {

    @Override
    public List<String> getAvailableTypes() {
        return donors.stream()
                .map(Donor::getBloodGroup)
                .collect(
                        ArrayList::new,
                        ArrayList::add,
                        ArrayList::addAll
                );
    }
}

Creación de recopiladores personalizados para flujos de Java 8

Cuando pasas:

1
Collectors.collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);

Está proporcionando los argumentos que la clase de utilidad Collectors usará para crear un recopilador personalizado para usted, implícitamente. De lo contrario, el punto de partida para crear un recopilador personalizado es la implementación de la interfaz Collector.

En nuestro caso, un colector que acumula los tipos de grupos sanguíneos se parecería a esta clase CustomCollector:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class CustomCollector implements Collector<String, List<String>, List<String>> {

    // Defines the mutable container that will hold the results
    @Override
    public Supplier<List<String>> supplier() {
        return ArrayList::new;
    }

    // Defines how the mutable container
    // should accumulate the elements passed to it from the stream
    @Override
    public BiConsumer<List<String>, String> accumulator() {
        return List::add;
    }

    // The combiner method will only be called when you are running the stream in parallel
    // If you stick to sequential stream processing 
    // Only the supplier and accumulator will be called and, optionally the finisher method
    @Override
    public BinaryOperator<List<String>> combiner() {
        return (bloodGroups, otherBloodGroups) -> {
            bloodGroups.addAll(otherBloodGroups);
            return bloodGroups;
        };
    }

    //Defines any other transformations that should be carried out on the mutable container before
    //it is finally returned at when the stream terminates
    @Override
    public Function<List<String>, List<String>> finisher() {
        return Collections::unmodifiableList;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

La clase CustomCollector puede ayudarlo a convertir un flujo en una lista como en esta clase CustomCollectorBloodBank:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class CustomCollectorBloodBank extends AbstractBloodBank {

    @Override
    public List<String> getAvailableTypes() {
        return donors.stream()
                .map(Donor::getBloodGroup)
                // Plug in the custom collector
                .collect(new CustomCollector());
    }
}

{.icon aria-hidden=“true”}

Nota: Si tuviera que hacer todo lo posible con esto, puede tener varios métodos, como toList(), toMap(), etc. que devuelven diferentes colecciones, usando esta misma clase.

Cómo convertir una secuencia en una lista usando matrices

La API de transmisión ofrece una forma de recopilar elementos de una canalización de transmisión en matrices. Y debido a que la clase de utilidad Arrays tiene métodos que transforman matrices en listas, esta es una ruta por la que puede optar. No obstante, este enfoque es detallado, en cuanto al código, y se recomienda utilizar recopiladores prediseñados o definir los suyos propios si los estándar no se ajustan a su caso de uso.

Matrices de objetos

Usando el método Stream.toArray(), transforma una secuencia en una matriz de objetos. (Es decir, elementos de la clase base Objeto). Esto puede volverse demasiado detallado, dependiendo de su caso de uso y corre el riesgo de reducir la legibilidad de su código en una medida considerable.

Tome esta clase ArrayOfObjectsBloodBank, por ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class ArrayOfObjectsBloodBank extends AbstractBloodBank {

    @Override
    public List<String> getAvailableTypes() {
        // Transform the stream into an array of objects
        Object[] bloodGroupObjects = donors.stream()
                .map(Donor::getBloodGroup)
                .toArray();
        // Initialize another array with the same length as that of the array of objects from the stream
        String[] bloodGroups = new String[bloodGroupObjects.length];
        // Iterate over the array of objects to read each object sequentially
        for (int i = 0; i < bloodGroupObjects.length; i++) {
            Object bloodGroupObject = bloodGroupObjects[i];
            //Cast each object into an equivalent string representation
            bloodGroups[i] = String.class.cast(bloodGroupObject);
        }
        // Transform the array of blood group string representations into a list
        return Arrays.asList(bloodGroups);
    }
}

Este enfoque es inconstante, requiere iteración y bucles for clásicos, conversión manual y es considerablemente menos legible que los enfoques anteriores, pero funciona.

Matrices que requieren un generador de funciones internas {#matrices que requieren un generador de funciones internas}

Otra forma que ofrece Stream API para convertir un flujo de elementos en una matriz es el método Stream.toArray(IntFunction<A[]> generator). Mientras que la táctica anterior de derivar una matriz de objetos requería el uso de muchas líneas de código, el enfoque del generador es bastante sucinto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class ArrayBloodBank extends AbstractBloodBank {

    @Override
    public List<String> getAvailableTypes() {
        // Transform the stream into an array holding elements of the same class type
        // like those in the stream pipeline
        String[] bloodGroupArr = donors.stream()
                .map(Donor::getBloodGroup)
                .toArray(String[]::new);
        //Transform the array into a list
        return Arrays.asList(bloodGroupArr);
    }
}

Esto es mucho mejor que el enfoque anterior, y en realidad no es tan malo; sin embargo, todavía hay una conversión simplemente redundante entre una matriz y una lista aquí.

Otras tácticas (desaconsejadas) de convertir secuencias en listas {#otras tácticas desaconsejadas para convertir secuencias en listas}

La API de Stream desaconseja la introducción de efectos secundarios en la canalización de corriente. Debido a que los flujos pueden estar expuestos a subprocesos paralelos, es peligroso intentar modificar un contenedor de fuente declarado externamente.

Por lo tanto, los dos ejemplos siguientes del uso de Stream.forEach() y Stream.reduce() cuando desea convertir un flujo en una lista son malos trucos.

Aprovechando Stream.forEach()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class ForEachBloodBank extends AbstractBloodBank {

    @Override
    public List<String> getAvailableTypes() {
        List<String> bloodGroups  = new ArrayList<>();
        
        donors.stream()
                .map(Donor::getBloodGroup)
                //Side effects are introduced here - this is bad for parallelism
                .forEach(bloodGroups::add);
        return bloodGroups;
    }
}

Sin paralelismo, esto funciona bien y el código producirá los resultados que desea pero no está preparado para el futuro y es mejor evitarlo.

Convertir una transmisión en lista usando Stream.reduce()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StreamReduceBloodBank extends AbstractBloodBank {

    @Override
    public List<String> getAvailableTypes() {
        return donors.stream()
                .map(Donor::getBloodGroup)
                .reduce(
                        // Identity
                        new ArrayList<>(),
                        // Accumulator function
                        (bloodGroups, bloodGroup) -> {
                            bloodGroups.add(bloodGroup);
                            return bloodGroups;
                        },
                        // Combiner function
                        (bloodGroups, otherBloodGroups) -> {
                            bloodGroups.addAll(otherBloodGroups);
                            return bloodGroups;
                        }
                );
    }
}

Conclusión

La API Stream introdujo múltiples formas de hacer que Java fuera más funcional por naturaleza. Debido a que los flujos ayudan a que las operaciones se ejecuten en paralelo, es importante que las operaciones intermedias y terminales opcionales respeten los principios de:

  • No interferencia
  • Minimizar los efectos secundarios
  • Mantener los comportamientos de operación sin estado

Entre las tácticas que ha explorado este artículo, el uso de recopiladores es el que promete ayudarlo a lograr los tres principios. Por lo tanto, es importante que, a medida que continúe trabajando con secuencias, mejore sus habilidades para manejar recopiladores predefinidos y personalizados.

El código fuente de esta guía está disponible en GitHub.

Licensed under CC BY-NC-SA 4.0