Capítulo 4. Comparadores y colectores

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Java 8 mejora la interfaz Comparator con varios métodos estáticos y predeterminados que simplifican mucho las operaciones de ordenación. Ahora es posible ordenar una colección de POJOs por una propiedad, luego igualar las primeras propiedades por una segunda, luego por una tercera, y así sucesivamente, sólo con una serie de llamadas a la biblioteca.

Java 8 también añade una nueva clase de utilidad llamada java.util.stream.Collectors, que proporciona métodos estáticos para convertir de nuevo flujos en varios tipos de colecciones. Los colectores también pueden aplicarse "aguas abajo", lo que significa que pueden postprocesar una operación de agrupación o partición.

Las recetas de este capítulo ilustran todos estos conceptos.

4.1 Ordenar con un comparador

Problema

Quieres ordenar objetos.

Solución

Utiliza el método sorted de Stream con un Comparator, implementado con una expresión lambda o generado por uno de los métodos estáticos compare de la interfaz Compa⁠rator.

Debate

El método sorted de Stream produce un nuevo flujo ordenado utilizando el orden natural de la clase. El orden natural se especifica implementando la interfaz java.util.Comparable.

Por ejemplo, considera la posibilidad de ordenar una colección de cadenas , como se muestra en el Ejemplo 4-1.

Ejemplo 4-1. Ordenar cadenas lexicográficamente
private List<String> sampleStrings =
    Arrays.asList("this", "is", "a", "list", "of", "strings");

public List<String> defaultSort() {
    Collections.sort(sampleStrings);  1
    return sampleStrings;
}

public List<String> defaultSortUsingStreams() {
    return sampleStrings.stream()
        .sorted()                     2
        .collect(Collectors.toList());
}
1

Ordenación por defecto a partir de Java 7 e inferiores

2

Ordenación por defecto a partir de Java 8

Java ha tenido una clase de utilidad llamada Collections desde que se añadió el marco de colecciones en la versión 1.2. El método estático sort de Collections toma un List como argumento, pero devuelve void. La ordenación es destructiva, modificando la colección suministrada. Este enfoque no sigue los principios funcionales admitidos por Java 8, que hacen hincapié en la inmutabilidad.

Java 8 utiliza el método sorted en los flujos para realizar la misma ordenación, pero produce un nuevo flujo en lugar de modificar la colección original. En este ejemplo, después de ordenar la colección, la lista devuelta se ordena según el orden natural de la clase. Para las cadenas, el orden natural es lexicográfico, que se reduce a alfabético cuando todas las cadenas están en minúsculas, como en este ejemplo.

Si quieres ordenar las cadenas de una forma diferente, existe un método sobrecargado sorted que toma como argumento un Comparator.

El Ejemplo 4-2 muestra una ordenación por longitud de cadenas de dos formas distintas.

Ejemplo 4-2. Ordenar cadenas por longitud
public List<String> lengthSortUsingSorted() {
    return sampleStrings.stream()
        .sorted((s1, s2) -> s1.length() - s2.length()) 1
        .collect(toList());
}

public List<String> lengthSortUsingComparator() {
    return sampleStrings.stream()
        .sorted(Comparator.comparingInt(String::length)) 2
        .collect(toList());
}
1

Utilizando una lambda para que Comparator ordene por longitud

2

Utiliza un Comparator utilizando el método comparingInt

El argumento del método sorted es un java.util.Comparator, que es una interfaz funcional. En lengthSortUsingSorted, se proporciona una expresión lambda para implementar el método compare en Comparator. En Java 7 y versiones anteriores, la implementación la proporcionaría normalmente una clase interna anónima, pero aquí basta con una expresión lambda.

Nota

Java 8 añadió sort(Comparator) como método de instancia default en List, equivalente al método static void sort(List, Comparator) en Collections. Ambos son ordenaciones destructivas que devuelven void, por lo que se sigue prefiriendo el método sorted(Comparator) sobre flujos que se comenta aquí (que devuelve un nuevo flujo ordenado).

El segundo método, lengthSortUsingComparator, aprovecha uno de los métodos estáticos añadidos a la interfaz Comparator. El método comparingInt toma un argumento de tipo ToIntFunction que transforma la cadena en un int, llamado keyExtractor en los documentos, y genera un Comparator que ordena la colección utilizando esa clave.

Los métodos por defecto añadidos en Comparator son extremadamente útiles. Aunque puedes escribir un Comparator que ordene por longitud con bastante facilidad, cuando quieres ordenar por más de un campo la cosa se puede complicar. Considera la posibilidad de ordenar las cadenas por longitud, y luego las cadenas de igual longitud alfabéticamente. Utilizando los métodos por defecto y estático de Comparator, eso se convierte en algo casi trivial, como se muestra en el Ejemplo 4-3.

Ejemplo 4-3. Ordenar por longitud y luego por longitudes iguales lexicográficamente
public List<String> lengthSortThenAlphaSort() {
    return sampleStrings.stream()
        .sorted(comparing(String::length)            1
                    .thenComparing(naturalOrder()))
        .collect(toList());
}
1

Ordena por longitud, luego por orden alfabético las cadenas de igual longitud

Comparator proporciona un método default llamado thenComparing. Al igual que comparing, también toma como argumento un Function, también conocido como keyExtractor. Encadenándolo con el método comparing, se obtiene un Comparator que compara por la primera cantidad, luego iguala la primera por la segunda, y así sucesivamente.

Las importaciones estáticas suelen facilitar la lectura del código. Una vez que te acostumbras a los métodos estáticos de Comparator y Collectors, se convierte en una forma fácil de simplificar el código. En este caso, se han importado estáticamente los métodos comparing y naturalOrder.

Este método funciona con cualquier clase, aunque no implemente Comparable. Considera la clase Golfer mostrada en el Ejemplo 4-4.

Ejemplo 4-4. Una clase para golfistas
public class Golfer {
    private String first;
    private String last;
    private int score;

    // ... other methods ...
}

Para crear una tabla de clasificación en un torneo, tiene sentido ordenar por puntuación, luego por apellido y después por nombre. El Ejemplo 4-5 muestra cómo hacerlo.

Ejemplo 4-5. Clasificar golfistas
private List<Golfer> golfers = Arrays.asList(
    new Golfer("Jack", "Nicklaus", 68),
    new Golfer("Tiger", "Woods", 70),
    new Golfer("Tom", "Watson", 70),
    new Golfer("Ty", "Webb", 68),
    new Golfer("Bubba", "Watson", 70)
);

public List<Golfer> sortByScoreThenLastThenFirst() {
    return golfers.stream()
        .sorted(comparingInt(Golfer::getScore)
                    .thenComparing(Golfer::getLast)
                    .thenComparing(Golfer::getFirst))
        .collect(toList());
}

El resultado de llamar a sortByScoreThenLastThenFirst se muestra en el Ejemplo 4-6.

Ejemplo 4-6. Golfistas clasificados
Golfer{first='Jack', last='Nicklaus', score=68}
Golfer{first='Ty', last='Webb', score=68}
Golfer{first='Bubba', last='Watson', score=70}
Golfer{first='Tom', last='Watson', score=70}
Golfer{first='Tiger', last='Woods', score=70}

Los golfistas se ordenan por puntuación, de modo que Nicklaus y Webb quedan antes que Woods y ambos Watson.1 A continuación, las puntuaciones iguales se ordenan por apellidos, poniendo a Nicklaus antes que a Webb y a Watson antes que a Woods. Por último, las puntuaciones y apellidos iguales se ordenan por nombre, poniendo a Bubba Watson antes que a Tom Watson.

Los métodos por defecto y estáticos de Comparator, junto con el nuevo método sorted en Stream, facilitan la generación de ordenaciones complejas.

4.2 Convertir un flujo en una colección

Problema

Después de procesar el flujo, quieres convertirlo a List, Set, u otra colección lineal.

Solución

Utiliza los métodos toList, toSet, o toCollection de la clase de utilidad Collectors.

Debate

El lenguaje Java 8 a menudo implica pasar elementos de un flujo a través de una cadena de operaciones intermedias, terminando con una operación terminal. Una operación terminal es el método collect, que se utiliza para convertir un Stream en una colección.

El método collect de Stream tiene dos versiones sobrecargadas, como se muestra en el Ejemplo 4-7.

Ejemplo 4-7. El método collect en Stream<T>
<R,A> R collect(Collector<? super T,A,R> collector)
<R>   R collect(Supplier<R> supplier,
                BiConsumer<R,? super T> accumulator,
                BiConsumer<R,R> combiner)

Esta receta se ocupa de la primera versión, que toma como argumento un Collector. Los recolectores realizan una "operación de reducción mutable" que acumula elementos en un contenedor de resultados. Aquí el resultado será una colección.

Collector es una interfaz, por lo que no puede instanciarse. La interfaz contiene un método estático of para producirlos, pero a menudo hay una forma mejor, o al menos más fácil.

Consejo

La API de Java 8 utiliza con frecuencia un método estático llamado of como método de fábrica.

Aquí, los métodos estáticos de la clase Collectors se utilizarán para producir instancias de Collector, que se utilizarán como argumento de Stream.collect para rellenar una colección.

En el Ejemplo 4-8 se muestra un ejemplo sencillo que crea un List.2

Ejemplo 4-8. Crear una lista
List<String> superHeroes =
    Stream.of("Mr. Furious", "The Blue Raja", "The Shoveler",
              "The Bowler", "Invisible Boy", "The Spleen", "The Sphinx")
          .collect(Collectors.toList());

Este método crea y rellena un ArrayList con los elementos de flujo dados. Crear un Set es igual de fácil, como en el Ejemplo 4-9.

Ejemplo 4-9. Crear un Conjunto
Set<String> villains =
    Stream.of("Casanova Frankenstein", "The Disco Boys",
              "The Not-So-Goodie Mob", "The Suits", "The Suzies",
              "The Furriers", "The Furriers")  1
          .collect(Collectors.toSet());
}
1

Nombre duplicado, eliminado al convertir a Set

Este método crea una instancia de HashSet y la rellena, eliminando los duplicados.

Ambos ejemplos utilizan las estructuras de datos predeterminadas:ArrayList para List, y Hash⁠Set para Set. Si deseas especificar una estructura de datos concreta, debes utilizar el método Collectors⁠.toCollection, que toma como argumento un Supplier. El Ejemplo 4-10 muestra el código de ejemplo.

Ejemplo 4-10. Crear una lista enlazada
List<String> actors =
    Stream.of("Hank Azaria", "Janeane Garofalo", "William H. Macy",
              "Paul Reubens", "Ben Stiller", "Kel Mitchell", "Wes Studi")
          .collect(Collectors.toCollection(LinkedList::new));
}

El argumento del método toCollection es una colección Supplier, por lo que aquí se proporciona la referencia del constructor a LinkedList. El método collect crea un LinkedList y lo rellena con los nombres dados.

La clase Collectors también contiene un método para crear una matriz de objetos. Hay dos sobrecargas del método toArray:

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

La primera devuelve una matriz que contiene los elementos de este flujo, pero sin especificar el tipo. La segunda toma una función que produce una nueva matriz del tipo deseado con una longitud igual al tamaño de la secuencia, y es más fácil de utilizar con un constructor de matriz de referencia, como se muestra en el Ejemplo 4-11.

Ejemplo 4-11. Crear una matriz
String[] wannabes =
    Stream.of("The Waffler", "Reverse Psychologist", "PMS Avenger")
          .toArray(String[]::new); 1
}
1

Referencia del constructor de matriz como Supplier

La matriz devuelta es del tipo especificado, cuya longitud coincide con el número de elementos del flujo.

Para transformarse en un Map, el método Collectors.toMap requiere dos instancias de Function: una para las claves y otra para los valores.

Considera un POJO Actor, que envuelve un name y un role. Si tienes un Set de Actor instancias de una película determinada, el código del Ejemplo 4-12 crea un Map a partir de ellas.

Ejemplo 4-12. Crear un mapa
Set<Actor> actors = mysteryMen.getActors();

Map<String, String> actorMap = actors.stream()
    .collect(Collectors.toMap(Actor::getName, Actor::getRole)); 1

actorMap.forEach((key,value) ->
    System.out.printf("%s played %s%n", key, value));
1

Funciones para producir claves y valores

El resultado es

Janeane Garofalo played The Bowler
Greg Kinnear played Captain Amazing
William H. Macy played The Shoveler
Paul Reubens played The Spleen
Wes Studi played The Sphinx
Kel Mitchell played Invisible Boy
Geoffrey Rush played Casanova Frankenstein
Ben Stiller played Mr. Furious
Hank Azaria played The Blue Raja

Un código similar funciona para ConcurrentMap utilizando el método toConcurrentMap.

Ver también

Supplierse tratan en la Receta 2.2. Las referencias a constructores están en la Receta 1.3. El método toMap también se muestra en la Receta 4.3.

4.3 Añadir una colección lineal a un mapa

Problema

Quieres añadir una colección de objetos a un Map, donde la clave es una de las propiedades del objeto y el valor es el propio objeto.

Solución

Utiliza el método toMap de Collectors, junto con Function.identity.

Debate

Éste es un caso de uso breve y muy concreto, pero cuando se plantea en la práctica, la solución aquí puede ser muy conveniente.

Digamos que tienes un List de instancias de Book, donde Book es un POJO simple que tiene un ID, un nombre y un precio. En el Ejemplo 4-13 se muestra una forma abreviada de la clase Book.

Ejemplo 4-13. Un POJO sencillo que representa un libro
public class Book {
    private int id;
    private String name;
    private double price;

    // ... other methods ...
}

Supón ahora que tienes una colección de instancias de Book, como se muestra en el Ejemplo 4-14.

Ejemplo 4-14. Una colección de libros
List<Book> books = Arrays.asList(
    new Book(1, "Modern Java Recipes", 49.99),
    new Book(2, "Java 8 in Action", 49.99),
    new Book(3, "Java SE8 for the Really Impatient", 39.99),
    new Book(4, "Functional Programming in Java", 27.64),
    new Book(5, "Making Java Groovy", 45.99)
    new Book(6, "Gradle Recipes for Android", 23.76)
);

En muchas situaciones, en lugar de un List podrías querer un Map, donde las claves son los ID de los libros y los valores son los propios libros. Esto es muy fácil de conseguir utilizando el método toMap en Collectors, como se muestra de dos formas distintas en el Ejemplo 4-15.

Ejemplo 4-15. Añadir los libros a un mapa
Map<Integer, Book> bookMap = books.stream()
    .collect(Collectors.toMap(Book::getId, b -> b));              1

bookMap = books.stream()
    .collect(Collectors.toMap(Book::getId, Function.identity())); 2
1

Identidad lambda: dado un elemento, devuélvelo

2

El método estático identity en Function hace lo mismo

El método toMap de Collectors toma como argumentos dos instancias de Function, la primera de las cuales genera una clave y la segunda genera el valor a partir del objeto proporcionado. En este caso, la clave es asignada por el método getId en Book, y el valor es el propio libro.

El primer toMap del Ejemplo 4-15 utiliza el método getId para mapear a la clave y una expresión lambda explícita que simplemente devuelve su parámetro. El segundo ejemplo utiliza el método estático identity en Function para hacer lo mismo.

Ver también

Las funciones se tratan en la Receta 2.4, que también trata de los operadores unarios y binarios.

4.4 Ordenar mapas

Problema

Quieres ordenar un Map por clave o por valor.

Solución

Utiliza los nuevos métodos estáticos de la interfaz Map.Entry.

Debate

La interfaz Map siempre ha contenido una interfaz pública, estática e interna llamada Map.Entry, que representa un par clave-valor. El método Map.entrySet devuelve un Set de elementos Map.Entry. Antes de Java 8, los principales métodos utilizados en esta interfaz eran getKey y getValue, que hacen lo que cabría esperar.

En Java 8, se han añadido los métodos estáticos de la Tabla 4-1.

Tabla 4-1. Métodos estáticos en Map.Entry (de la documentación de Java 8)
Método Descripción

comparingByKey()

Devuelve un comparador que compara Map.Entryen orden natural en clave

comparingByKey(Comparator<? super K> cmp)

Devuelve un comparador que compara Map.Entrypor clave utilizando la clave dada Comparator

comparingByValue()

Devuelve un comparador que compara Map.Entryen orden natural sobre el valor

comparingByValue(Comparator<? super V> cmp)

Devuelve un comparador que compara Map.Entrypor valor utilizando el dado Comparator

Para demostrar cómo utilizarlos, el Ejemplo 4-18 genera un Map de longitudes de palabra a número de palabras en un diccionario. Todos los sistemas Unix contienen un archivo en el directorio usr/share/dict/words con el contenido del diccionario Webster's 2ª edición, con una palabra por línea. El método Files.lines puede utilizarse para leer un archivo y producir un flujo de cadenas que contengan esas líneas. En este caso, el flujo contendrá cada palabra del diccionario.

Ejemplo 4-18. Lectura del archivo del diccionario en un Mapa
System.out.println("\nNumber of words of each length:");
try (Stream<String> lines = Files.lines(dictionary)) {
    lines.filter(s -> s.length() > 20)
        .collect(Collectors.groupingBy(
            String::length, Collectors.counting()))
        .forEach((len, num) -> System.out.printf("%d: %d%n", len, num));
} catch (IOException e) {
    e.printStackTrace();
}

Este ejemplo se trata en la Receta 7.1, pero para resumir:

  • El archivo se lee dentro de un bloque try-with-resources. Stream implementa AutoCloseable, de modo que cuando el bloque try sale, Java llama al método close en Stream, que a su vez llama al método close en File.

  • El filtro restringe el procesamiento posterior sólo a palabras de al menos 20 caracteres de longitud.

  • El método groupingBy de Collectors toma como primer argumento un Function, que representa el clasificador. Aquí, el clasificador es la longitud de cada cadena. Si sólo proporcionas un argumento, el resultado es un Map en el que las claves son los valores del clasificador y los valores son listas de elementos que coinciden con el clasificador. En el caso que estamos examinando, groupingBy(String::length) habría producido un Map<Integer,List<String>> donde las claves son las longitudes de las palabras y los valores son listas de palabras de esa longitud.

  • En este caso, la versión de dos argumentos de groupingBy te permite suministrar otro Collector, llamado colector descendente, que postprocesa las listas de palabras. En este caso, el tipo de retorno es Map<Integer,Long>, donde las claves son las longitudes de las palabras y los valores son el número de palabras de esa longitud en el diccionario.

El resultado es:

Number of words of each length:
21: 82
22: 41
23: 17
24: 5

En otras palabras, hay 82 palabras de longitud 21, 41 palabras de longitud 22, 17 palabras de longitud 23 y 5 palabras de longitud 24.3

Los resultados muestran que el mapa se imprime en orden ascendente de longitud de palabra. Para verlo en orden descendente, utiliza Map.Entry.comparingByKey como en el Ejemplo 4-19.

Ejemplo 4-19. Ordenar el mapa por clave
System.out.println("\nNumber of words of each length (desc order):");
try (Stream<String> lines = Files.lines(dictionary)) {
    Map<Integer, Long> map = lines.filter(s -> s.length() > 20)
        .collect(Collectors.groupingBy(
            String::length, Collectors.counting()));

    map.entrySet().stream()
        .sorted(Map.Entry.comparingByKey(Comparator.reverseOrder()))
        .forEach(e -> System.out.printf("Length %d: %2d words%n",
            e.getKey(), e.getValue()));
} catch (IOException e) {
    e.printStackTrace();
}

Tras calcular el Map<Integer,Long>, esta operación extrae el entrySet y produce un flujo. El método sorted en Stream se utiliza para producir un flujo ordenado utilizando el comparador proporcionado.

En este caso, Map.Entry.comparingByKey genera un comparador que ordena por las claves, y utilizar la sobrecarga que toma un comparador permite que el código especifique que lo queremos en orden inverso.

Nota

El método sorted en Stream produce un nuevo flujo ordenado que no modifica el origen. El original Map no se ve afectado.

El resultado es:

Number of words of each length (desc order):
Length 24:  5 words
Length 23: 17 words
Length 22: 41 words
Length 21: 82 words

Los demás métodos de clasificación enumerados en la Tabla 4-1 se utilizan de forma similar.

Ver también

En el Apéndice A se muestra un ejemplo adicional de ordenación de un Map por claves o valores. Los colectores descendentes se tratan en la Receta 4.6. Las operaciones de archivo en el diccionario forman parte de la Receta 7.1.

4.5 Particionar y Agrupar

Problema

Quieres dividir una colección de elementos en categorías.

Solución

El método Collectors.partitioningBy divide los elementos en los que satisfacen un Predicate y los que no. El método Collectors.groupingBy produce un Map de categorías, donde los valores son los elementos de cada categoría.

Debate

Supongamos que tienes una colección de cadenas. Si quieres dividirlas en las que tienen longitud par y las que tienen longitud impar, puedes utilizar Collectors.partitioningBy, como en el Ejemplo 4-20.

Ejemplo 4-20. Particionar cadenas por longitudes pares o impares
List<String> strings = Arrays.asList("this", "is", "a", "long", "list", "of",
        "strings", "to", "use", "as", "a", "demo");

Map<Boolean, List<String>> lengthMap = strings.stream()
    .collect(Collectors.partitioningBy(s -> s.length() % 2 == 0)); 1

lengthMap.forEach((key,value) -> System.out.printf("%5s: %s%n", key, value));
//
// false: [a, strings, use, a]
//  true: [this, is, long, list, of, to, as, demo]
1

Partición por longitud par o impar

La firma de los dos métodos partitioningBy son:

static <T> Collector<T,?,Map<Boolean,List<T>>> partitioningBy(
    Predicate<? super T> predicate)
static <T,D,A> Collector<T,?,Map<Boolean,D>> partitioningBy(
    Predicate<? super T> predicate, Collector<? super T,A,D> downstream)

Los tipos de retorno tienen un aspecto bastante desagradable debido a los genéricos, pero rara vez tendrás que tratar con ellos en la práctica. En su lugar, el resultado de cualquiera de las dos operaciones se convierte en el argumento del método collect, que utiliza el recolector generado para crear el mapa de salida definido por el tercer argumento genérico.

El primer método partitioningBy toma como argumento un único Predicate. Divide los elementos en los que satisfacen el Predicate y los que no. Siempre obtendrás como resultado un Map que tiene exactamente dos entradas: una lista de valores que satisfacen el Predic⁠ate, y una lista de valores que no lo satisfacen.

La versión sobrecargada del método toma un segundo argumento de tipo Collector, llamado colector posterior. Esto te permite postprocesar las listas devueltas por la partición, y se trata en la Receta 4.6.

El método groupingBy realiza una operación similar a una sentencia "agrupar por" en SQL. Devuelve un Map donde las claves son los grupos y los valores son listas de elementos de cada grupo.

Nota

Si obtienes los datos de una base de datos, realiza allí las operaciones de agrupación. Los nuevos métodos de la API son métodos de conveniencia para los datos en memoria.

La firma del método groupingBy es

static <T,K> Collector<T,?,Map<K,List<T>>>	groupingBy(
    Function<? super T,? extends K> classifier)

El argumento Function toma cada elemento de la cadena y extrae una propiedad por la que agrupar. Esta vez, en lugar de dividir simplemente las cadenas en dos categorías, considera separarlas por longitud, como en el Ejemplo 4-21.

Ejemplo 4-21. Agrupar cadenas por longitud
List<String> strings = Arrays.asList("this", "is", "a", "long", "list", "of",
        "strings", "to", "use", "as", "a", "demo");

Map<Integer, List<String>> lengthMap = strings.stream()
    .collect(Collectors.groupingBy(String::length)); 1

lengthMap.forEach((k,v) -> System.out.printf("%d: %s%n", k, v));
//
// 1: [a, a]
// 2: [is, of, to, as]
// 3: [use]
// 4: [this, long, list, demo]
// 7: [strings]
1

Agrupar cadenas por longitud

Las claves del mapa resultante son las longitudes de las cadenas (1, 2, 3, 4 y 7) y los valores son listas de cadenas de cada longitud.

Ver también

Una extensión de la receta que acabamos de ver, la Receta 4.6 muestra cómo postprocesar las listas devueltas por una operación groupingBy o partitioning​By.

4.6 Colectores posteriores

Problema

Quieres postprocesar las colecciones devueltas por una operación groupingBy o partitioningBy.

Solución

Utiliza uno de los métodos de utilidad estáticos de la clase java.util.stream.Collectors.

Debate

En la Receta 4.5, vimos cómo separar elementos en varias categorías. Los métodos partitioningBy y groupingBy devuelven un Map en el que las claves eran las categorías (booleanos true y false para partitioningBy, objetos para groupingBy) y los valores eran listas de elementos que satisfacían cada categoría. Recuerda el ejemplo de partición de cadenas por longitudes pares e impares, mostrado en el Ejemplo 4-20 pero repetido en el Ejemplo 4-22 por comodidad.

Ejemplo 4-22. Particionar cadenas por longitudes pares o impares
List<String> strings = Arrays.asList("this", "is", "a", "long", "list", "of",
        "strings", "to", "use", "as", "a", "demo");

Map<Boolean, List<String>> lengthMap = strings.stream()
    .collect(Collectors.partitioningBy(s -> s.length() % 2 == 0));

lengthMap.forEach((key,value) -> System.out.printf("%5s: %s%n", key, value));
//
// false: [a, strings, use, a]
//  true: [this, is, long, list, of, to, as, demo]

En lugar de las listas en sí, puede que te interese saber cuántos elementos entran en cada categoría. En otras palabras, en lugar de producir un Map cuyos valores sean List<String>, puede que sólo quieras el número de elementos de cada una de las listas. El método partitioningBy tiene una versión sobrecargada cuyo segundo argumento es del tipo Collector:

static <T,D,A> Collector<T,?,Map<Boolean,D>>	partitioningBy(
    Predicate<? super T> predicate, Collector<? super T,A,D> downstream)

Aquí es donde resulta útil el método estático Collectors.counting . El Ejemplo 4-23 muestra cómo funciona.

Ejemplo 4-23. Contar las cadenas particionadas
Map<Boolean, Long> numberLengthMap = strings.stream()
    .collect(Collectors.partitioningBy(s -> s.length() % 2 == 0,
                 Collectors.counting()));  1

numberLengthMap.forEach((k,v) -> System.out.printf("%5s: %d%n", k, v));
//
// false: 4
//  true: 8
1

Colector aguas abajo

Se denomina colector descendente, porque postprocesa las listas resultantes en sentido descendente (es decir, una vez finalizada la operación de partición).

El método groupingBy también tiene una sobrecarga que toma un colector descendente:

/**
* @param <T> the type of the input elements
* @param <K> the type of the keys
* @param <A> the intermediate accumulation type of the downstream collector
* @param <D> the result type of the downstream reduction
* @param classifier a classifier function mapping input elements to keys
* @param downstream a {@code Collector} implementing the downstream reduction
* @return a {@code Collector} implementing the cascaded group-by operation
*/
static <T,K,A,D> Collector<T,?,Map<K,D>>	groupingBy(
    Function<? super T,? extends K> classifier,
    Collector<? super T,A,D> downstream)

En la firma se incluye una parte del comentario Javadoc del código fuente, que muestra que T es el tipo del elemento de la colección, K es el tipo de clave del mapa resultante, A es un acumulador y D es el tipo del colector descendente. El ? representa "desconocido". Consulta el Apéndice A para más detalles sobre los genéricos en Java 8.

Varios métodos de Stream tienen análogos en la clase Collectors. La Tabla 4-2 muestra cómo se alinean.

Tabla 4-2. Métodos de los colectores similares a los métodos de los flujos
Flujo Coleccionistas

count

counting

map

mapping

min

minBy

max

maxBy

IntStream.sum

summingInt

DoubleStream.sum

summingDouble

LongStream.sum

summingLong

IntStream.summarizing

summarizingInt

DoubleStream.summarizing

summarizingDouble

LongStream.summarizing

summarizingLong

De nuevo, el propósito de un recopilador descendente es posprocesar la colección de objetos producida por una operación ascendente, como la partición o la agrupación.

Ver también

Lareceta 7.1 muestra un ejemplo de colector descendente al determinar las palabras más largas de un diccionario. La receta 4.5 trata con más detalle los métodos partitionBy y groupingBy. Todo el tema de los genéricos se trata en el Apéndice A.

4.7 Encontrar los valores máximo y mínimo

Problema

Quieres determinar el valor máximo o mínimo de un flujo.

Solución

Tienes varias opciones: los métodos maxBy y minBy en BinaryOperator, los métodos max y min en Stream, o los métodos de utilidad maxBy y minBy en Collectors.

Debate

Un BinaryOperator es una de las interfaces funcionales del paquete java.util.function. Amplía BiFunction y se aplica cuando tanto los argumentos de la función como el valor de retorno pertenecen a la misma clase.

La interfaz BinaryOperator añade dos métodos estáticos:

static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator)
static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator)

Cada uno de devuelve un BinaryOperator que utiliza el Comparator suministrado.

Para demostrar las distintas formas de obtener el valor máximo de un flujo, considera un POJO llamado Employee que contiene tres atributos: name, salary, y department, como en el Ejemplo 4-24.

Ejemplo 4-24. POJO de empleado
public class Employee {
    private String name;
    private Integer salary;
    private String department;

    // ... other methods ...
}

List<Employee> employees = Arrays.asList(                  1
        new Employee("Cersei",     250_000, "Lannister"),
        new Employee("Jamie",      150_000, "Lannister"),
        new Employee("Tyrion",       1_000, "Lannister"),
        new Employee("Tywin",    1_000_000, "Lannister"),
        new Employee("Jon Snow",    75_000, "Stark"),
        new Employee("Robb",       120_000, "Stark"),
        new Employee("Eddard",     125_000, "Stark"),
        new Employee("Sansa",            0, "Stark"),
        new Employee("Arya",         1_000, "Stark"));

Employee defaultEmployee =                                2
    new Employee("A man (or woman) has no name", 0, "Black and White");
1

Recogida de empleados

2

Por defecto para cuando el flujo está vacío

Dada una colección de empleados, puedes utilizar el método reduce en Stream, que toma un BinaryOperator como argumento. El fragmento del Ejemplo 4-25 muestra cómo obtener el empleado con el salario más alto.

Ejemplo 4-25. Uso de BinaryOperator.maxBy
Optional<Employee> optionalEmp = employees.stream()
    .reduce(BinaryOperator.maxBy(Comparator.comparingInt(Employee::getSalary)));

System.out.println("Emp with max salary: " +
    optionalEmp.orElse(defaultEmployee));

El método reduce requiere un BinaryOperator. El método estático maxBy produce ese BinaryOperator basándose en el Comparator suministrado, que en este caso compara empleados por salario.

Esto funciona, pero en realidad existe un método práctico llamado max que puede aplicarse directamente a el flujo:

Optional<T> max(Comparator<? super T> comparator)

En el Ejemplo 4-26 se muestra cómo utilizar directamente ese método.

Ejemplo 4-26. Uso de Stream.max
optionalEmp = employees.stream()
        .max(Comparator.comparingInt(Employee::getSalary));

El resultado es el mismo.

Ten en cuenta que también hay un método llamado max en los flujos primitivos (IntStream, LongStream, y DoubleStream) que no toma argumentos. El Ejemplo 4-27 muestra ese método en acción.

Ejemplo 4-27. Encontrar el salario más alto
OptionalInt maxSalary = employees.stream()
        .mapToInt(Employee::getSalary)
        .max();
System.out.println("The max salary is " + maxSalary);

En este caso, el método mapToInt se utiliza para convertir el flujo de empleados en un flujo de enteros invocando al método getSalary, y el flujo devuelto es un IntStream. El método max devuelve entonces un OptionalInt.

También hay un método estático llamado maxBy en la clase de utilidad Collectors. Puedes utilizarlo directamente aquí, como en el Ejemplo 4-28.

Ejemplo 4-28. Uso de Recopiladores.maxBy
optionalEmp = employees.stream()
    .collect(Collectors.maxBy(Comparator.comparingInt(Employee::getSalary)));

Sin embargo, esto es incómodo y puede sustituirse por el método max en Stream, como se muestra en el ejemplo anterior. El método maxBy en Collectors es útil cuando se utiliza como recopilador posterior (es decir, cuando se postprocesa una operación de agrupación o partición). El código del Ejemplo 4-29 utiliza groupingBy en Stream para crear un Map de departamentos a listas de empleados, pero luego determina el empleado con el mayor salario en cada departamento.

Ejemplo 4-29. Utilizar Colectores.maxBy como colector descendente
Map<String, Optional<Employee>> map = employees.stream()
    .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.maxBy(
                    Comparator.comparingInt(Employee::getSalary))));

map.forEach((house, emp) ->
        System.out.println(house + ": " + emp.orElse(defaultEmployee)));

El método minBy de cada una de estas clases funciona de la misma manera.

Ver también

Las funciones se tratan en la Receta 2.4. Los colectores descendentes están en la Receta 4.6.

4.8 Crear colecciones inmutables

Problema

Quieres crear una lista, conjunto o mapa inmutable utilizando la API Stream.

Solución

Utiliza el nuevo método estático collectingAndThen en la clase Collectors.

Debate

Al centrarse en la paralelización y la claridad, la programación funcional favorece el uso de objetos inmutables siempre que sea posible. El marco de trabajo Colecciones, añadido en Java 1.2, siempre ha tenido métodos para crear colecciones inmutables a partir de otras existentes, aunque de forma un tanto torpe.

La clase de utilidad Collections tiene los métodos unmodifiableList, unmodifiableSet y unmodifiableMap (junto con algunos otros métodos con el mismo prefijo unmodifiable ), como se muestra en el Ejemplo 4-30.

Ejemplo 4-30. Métodos no modificables de la clase Colecciones
static <T> List<T>    unmodifiableList(List<? extends T> list)
static <T> Set<T>     unmodifiableSet(Set<? extends T> s)
static <K,V> Map<K,V> unmodifiableMap(Map<? extends K,? extends V> m)

En cada caso, el argumento del método es una lista, conjunto o mapa existente, y la lista, conjunto o mapa resultante tiene los mismos elementos que el argumento, pero con una diferencia importante: todos los métodos que podían modificar la colección, como add o remove, ahora lanzan un UnsupportedOperationException.

Antes de Java 8, si recibías los valores individuales como argumento, utilizando una lista de argumentos variable, producías una lista o conjunto no modificable, como se muestra en el Ejemplo 4-31.

Ejemplo 4-31. Crear listas o conjuntos no modificables antes de Java 8
@SafeVarargs  1
public final <T> List<T> createImmutableListJava7(T... elements) {
    return Collections.unmodifiableList(Arrays.asList(elements));
}

@SafeVarargs  1
public final <T> Set<T> createImmutableSetJava7(T... elements) {
    return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(elements)));
}
1

Te comprometes a no corromper el tipo de matriz de entrada. Consulta el Apéndice A para más detalles.

La idea en cada caso es empezar por tomar los valores entrantes y convertirlos en un List. Puedes envolver la lista resultante utilizando unmodifiableList, o, en el caso de un Set, utilizar la lista como argumento de un constructor de conjuntos antes de utilizar unmodi⁠fiable​Set.

En Java 8, con la nueva API Stream, puedes aprovechar el método estático Col⁠lectors.collectingAndThen, como en el Ejemplo 4-32.

Ejemplo 4-32. Crear listas o conjuntos no modificables en Java 8
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

// ... define a class with the following methods ...

@SafeVarargs
public final <T> List<T> createImmutableList(T... elements) {
    return Arrays.stream(elements)
        .collect(collectingAndThen(toList(),
                    Collections::unmodifiableList));  1
}

@SafeVarargs
public final <T> Set<T> createImmutableSet(T... elements) {
    return Arrays.stream(elements)
        .collect(collectingAndThen(toSet(),
                    Collections::unmodifiableSet));   1
}
1

"Finalizador" envuelve las colecciones generadas

El método Collectors.collectingAndThen toma dos argumentos: un Collector de bajada y un Function llamado finalizador. La idea es transmitir los elementos de entrada y luego recogerlos en un List o Set, y después la función no modificable envuelve la colección resultante.

Convertir una serie de elementos de entrada en un Map no modificable no está tan claro, en parte porque no es obvio cuáles de los elementos de entrada se asumirían como claves y cuáles como valores. El código que se muestra en el Ejemplo 4-334 crea un Map inmutable de una forma muy torpe, utilizando un inicializador de instancia.

Ejemplo 4-33. Crear un Mapa inmutable
Map<String, Integer> map = Collections.unmodifiableMap(
  new HashMap<String, Integer>() {{
    put("have", 1);
    put("the", 2);
    put("high", 3);
    put("ground", 4);
}});

Sin embargo, los lectores familiarizados con Java 9 ya saben que toda esta receta puede sustituirse por un conjunto muy sencillo de métodos de fábrica: List.of, Set.of, y Map.of.

Ver también

La receta 10.3 muestra los nuevos métodos de fábrica de Java 9 que crean automáticamente colecciones inmutables.

4.9 Implementar la interfaz del colector

Problema

Tienes que implementar java.util.stream.Collector manualmente, porque ninguno de los métodos de fábrica de la clase java.util.stream.Collectors te da exactamente lo que necesitas.

Solución

Proporciona expresiones lambda o referencias a métodos para las funciones Supplier, acumulador, combinador y finalizador utilizadas por los métodos de la fábrica Collector.of, junto con las características que desees.

Debate

La clase de utilidad java.util.stream.Collectors tiene varios métodos estáticos muy prácticos cuyo tipo de retorno es Collector. Algunos ejemplos son toList, toSet, toMap, e incluso toCollection, cada uno de los cuales se ilustra en otra parte de este libro. Las instancias de las clases que implementan Collector se envían como argumentos al método collect de Stream. Por ejemplo, en el Ejemplo 4-34, el método acepta argumentos de cadena y devuelve un List que contiene sólo aquellas cuya longitud es par.

Ejemplo 4-34. Utilizar collect para devolver una Lista
public List<String> evenLengthStrings(String... strings) {
    return Stream.of(strings)
        .filter(s -> s.length() % 2 == 0)
        .collect(Collectors.toList());  1
}
1

Recoge las cadenas de longitud par en una List

Sin embargo, si necesitas escribir tus propios recopiladores, el procedimiento es un poco más complicado. Los recopiladores utilizan cinco funciones que trabajan juntas para acumular entradas en un contenedor mutable y, opcionalmente, transformar el resultado. Las cinco funciones se denominan supplier, accumulator, combiner, finisher y characteristics.

Tomando primero la función characteristics, representa un Set inmutable de elementos de un enum tipo Collector.Characteristics. Los tres valores posibles son CON⁠CURRENT, IDENTITY_FINISH, y UNORDERED. CONCURRENT significa que el contenedor de resultados puede soportar que la función acumulador sea llamada concurrentemente sobre el contenedor de resultados desde múltiples hilos. UNORDERED dice que la operación de recolección no necesita preservar el orden de encuentro de los elementos. IDENTITY_FINISH significa que la función de finalización devuelve su argumento sin ningún cambio.

Ten en cuenta que no tienes que proporcionar ninguna característica si los valores por defecto son los que quieres.

La finalidad de cada uno de los métodos requeridos es:

supplier()

Crea el contenedor acumulador utilizando un Supplier<A>

accumulator()

Añade un único elemento de datos nuevo al contenedor acumulador utilizando un Bi​Consumer<A,T>

combiner()

Fusiona dos contenedores acumuladores utilizando un BinaryOperator<A>

finisher()

Transforma el contenedor acumulador en el contenedor resultado utilizando un Function​<A,R>

characteristics()

Un Set<Collector.Characteristics> elegido entre los valores del enum

Como de costumbre, la comprensión de las interfaces funcionales definidas en el paquete java.util.function lo aclara todo. Un Supplier se utiliza para crear el contenedor donde se acumulan los resultados temporales. Un BiConsumer añade un único elemento al acumulador. Un BinaryOperator significa que tanto el tipo de entrada como el tipo de salida son iguales, por lo que aquí se trata de combinar dos acumuladores en uno. Un Function transforma finalmente el acumulador en el contenedor de resultados deseado.

Cada uno de estos métodos se invoca durante el proceso de recogida, que se activa mediante (por ejemplo) el método collect en Stream. Conceptualmente, el proceso de recogida es equivalente al código (genérico) que se muestra en el Ejemplo 4-35, tomado de los Javadocs.

Ejemplo 4-35. Cómo se utilizan los métodos del Colector
R container = collector.supplier.get();           1
for (T t : data) {
    collector.accumulator().accept(container, t); 2
}
return collector.finisher().apply(container);     3
1

Crea el contenedor acumulador

2

Añade cada elemento al contenedor acumulador

3

Convierte el contenedor acumulador en contenedor de resultados utilizando la acabadora

Brilla por su ausencia cualquier mención a la función combiner. Si tu flujo es secuencial, no la necesitas: el algoritmo procede como se ha descrito. Si, por el contrario, operas sobre un flujo paralelo, el trabajo se divide en varias regiones, cada una de las cuales produce su propio contenedor acumulador. El combinador se utiliza durante el proceso de unión para fusionar los contenedores acumuladores en uno solo antes de aplicar la función finalizador.

En el Ejemplo 4-36 se muestra un código similar al del Ejemplo 4-34.

Ejemplo 4-36. Utilizar collect para devolver un SortedSet no modificable
public SortedSet<String> oddLengthStringSet(String... strings) {
        Collector<String, ?, SortedSet<String>> intoSet =
                Collector.of(TreeSet<String>::new,           1
                        SortedSet::add,                      2
                        (left, right) -> {                   3
                              left.addAll(right);
                              return left;
                        },
                        Collections::unmodifiableSortedSet); 4
        return Stream.of(strings)
                .filter(s -> s.length() % 2 != 0)
                .collect(intoSet);
    }
1

Supplier para crear un nuevo TreeSet

2

BiConsumer para añadir cada cadena a TreeSet

3

BinaryOperator para combinar dos instancias de SortedSet en una

4

finisher para crear un conjunto no modificable

El resultado será un conjunto ordenado e inmodificable de cadenas, ordenadas lexicográficamente.

En este ejemplo se ha utilizado una de las dos versiones sobrecargadas del método static of para producir colectores, cuyas firmas son:

static <T,A,R> Collector<T,A,R>	of(Supplier<A> supplier,
    BiConsumer<A,T> accumulator,
    BinaryOperator<A> combiner,
    Function<A,R> finisher,
    Collector.Characteristics... characteristics)
static <T,R> Collector<T,R,R>	of(Supplier<R> supplier,
    BiConsumer<R,T> accumulator,
    BinaryOperator<R> combiner,
    Collector.Characteristics... characteristics)

Dados los prácticos métodos de la clase Collectors que producen colectores por ti, rara vez necesitarás crear uno propio de esta forma. Aún así, es una habilidad útil, y una vez más ilustra cómo las interfaces funcionales del paquete java.util.function se unen para crear objetos interesantes.

Ver también

La función finisher es un ejemplo de colector descendente, del que se habla más en la Receta 4.6. Las interfaces funcionales Supplier, Function y BinaryOperator se tratan en varias recetas del Capítulo 2. Los métodos de utilidad estáticos de Collectors se tratan en la Receta 4.2.

1 Ty Webb, por supuesto, es de la película Caddyshack. Juez Smails: "Ty, ¿qué has tirado hoy?" Ty Webb: "Oh, juez, no llevo la cuenta". Smails: "¿Entonces cómo te mides con otros golfistas?" Webb: "Por altura". Añadir una clasificación por altura se deja al lector como un ejercicio fácil.

2 Los nombres de esta receta proceden de Mystery Men, una de las grandes películas olvidadas de los 90. (Mr. Furioso: "Lance Hunt es el Capitán Asombroso". El Palafrenero: "Lance Hunt lleva gafas. El Capitán Asombroso no lleva gafas". Mr. Furioso: "Se las quita cuando se transforma". El Palafrenero: "¡Eso no tiene ningún sentido! No podría ver!")

3 Para que conste, esas cinco palabras más largas son formaldehído-sulfoxilato, patológico-psicológico, científico-filosófico, tetrayodofenolftaleína y tiroparatiroidectomizar. Buena suerte con eso, corrector ortográfico.

4 De la entrada del blog de Carl Martensen "Las colecciones inmutables de Java 9 son más fáciles de crear, pero utilízalas con precaución".

Get Recetas Java modernas now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.