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
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
)
;
return
sampleStrings
;
}
public
List
<
String
>
defaultSortUsingStreams
(
)
{
return
sampleStrings
.
stream
(
)
.
sorted
(
)
.
collect
(
Collectors
.
toList
(
)
)
;
}
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
(
)
)
.
collect
(
toList
(
)
)
;
}
public
List
<
String
>
lengthSortUsingComparator
(
)
{
return
sampleStrings
.
stream
(
)
.
sorted
(
Comparator
.
comparingInt
(
String:
:
length
)
)
.
collect
(
toList
(
)
)
;
}
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
)
.
thenComparing
(
naturalOrder
(
)
)
)
.
collect
(
toList
(
)
)
;
}
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
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"
)
.
collect
(
Collectors
.
toSet
(
)
)
;
}
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 HashSet
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
)
;
}
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
)
)
;
actorMap
.
forEach
(
(
key
,
value
)
-
>
System
.
out
.
printf
(
"%s played %s%n"
,
key
,
value
)
)
;
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
Supplier
se 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
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
)
)
;
bookMap
=
books
.
stream
(
)
.
collect
(
Collectors
.
toMap
(
Book:
:
getId
,
Function
.
identity
(
)
)
)
;
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
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.
Método | Descripción |
---|---|
|
Devuelve un comparador que compara |
|
Devuelve un comparador que compara |
|
Devuelve un comparador que compara |
|
Devuelve un comparador que compara |
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
implementaAutoCloseable
, de modo que cuando el bloque try sale, Java llama al métodoclose
enStream
, que a su vez llama al métodoclose
enFile
. -
El filtro restringe el procesamiento posterior sólo a palabras de al menos 20 caracteres de longitud.
-
El método
groupingBy
deCollectors
toma como primer argumento unFunction
, que representa el clasificador. Aquí, el clasificador es la longitud de cada cadena. Si sólo proporcionas un argumento, el resultado es unMap
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 unMap<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 otroCollector
, llamado colector descendente, que postprocesa las listas de palabras. En este caso, el tipo de retorno esMap<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
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
)
)
;
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]
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 Predicate
, 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
)
)
;
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]
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 partitioningBy
.
4.6 Colectores posteriores
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
(
)
)
)
;
numberLengthMap
.
forEach
(
(
k
,
v
)
-
>
System
.
out
.
printf
(
"%5s: %d%n"
,
k
,
v
)
)
;
//
// false: 4
// true: 8
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.
Flujo | Coleccionistas |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
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
(
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
=
new
Employee
(
"A man (or woman) has no name"
,
0
,
"Black and White"
)
;
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
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
public
final
<
T
>
List
<
T
>
createImmutableListJava7
(
T
.
.
.
elements
)
{
return
Collections
.
unmodifiableList
(
Arrays
.
asList
(
elements
)
)
;
}
@SafeVarargs
public
final
<
T
>
Set
<
T
>
createImmutableSetJava7
(
T
.
.
.
elements
)
{
return
Collections
.
unmodifiableSet
(
new
HashSet
<
>
(
Arrays
.
asList
(
elements
)
)
)
;
}
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 unmodifiableSet
.
En Java 8, con la nueva API Stream
, puedes aprovechar el método estático Collectors.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
)
)
;
}
@SafeVarargs
public
final
<
T
>
Set
<
T
>
createImmutableSet
(
T
.
.
.
elements
)
{
return
Arrays
.
stream
(
elements
)
.
collect
(
collectingAndThen
(
toSet
(
)
,
Collections:
:
unmodifiableSet
)
)
;
}
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
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
(
)
)
;
}
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 CONCURRENT
, 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
BiConsumer<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
(
)
;
for
(
T
t
:
data
)
{
collector
.
accumulator
(
)
.
accept
(
container
,
t
)
;
}
return
collector
.
finisher
(
)
.
apply
(
container
)
;
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
,
SortedSet:
:
add
,
(
left
,
right
)
-
>
{
left
.
addAll
(
right
)
;
return
left
;
}
,
Collections:
:
unmodifiableSortedSet
)
;
return
Stream
.
of
(
strings
)
.
filter
(
s
-
>
s
.
length
(
)
%
2
!
=
0
)
.
collect
(
intoSet
)
;
}
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.