Hoy os traigo a este blog una entrada acerca de las mejoras de Java 8. He de confesar que, si bien al principio parecía un auténtico galimatías, con el transcurso de las horas y, sobre todo, con la práctica de las nuevas estructuras, me he ido convenciendo de que realmente el código es muchísimo más legible, cómodo de implementar y lo más importante: eficiente.
Bien es cierto que me queda muchísimo por probar, aprender y afianzar pero hay estructuras y modos de empleo que me han facilitado la vida.
Así que, quizás, también os puedan servir a vosotros.
Stream, ¡contigo empezó todo en Java 8!
A diferencia de su predecesor, la versión Java 8 ha añadido a la interfaz Collection (del paquete java.util) el método stream.
/**
* Returns a sequential {@code Stream} with this collection as its source.
*
*
This method should be overridden when the {@link #spliterator()}
* method cannot return a spliterator that is {@code IMMUTABLE},
* {@code CONCURRENT}, or late-binding. (See {@link #spliterator()}
* for details.)
*
* @implSpec
* The default implementation creates a sequential {@code Stream} from the
* collection's {@code Spliterator}.
*
* @return a sequential {@code Stream} over the elements in this collection
* @since 1.8
*/
default Stream stream() {
return StreamSupport.stream(spliterator(), false);
}
¿Qué es Stream? Pues ni más ni menos que una secuencia de elementos. Con este método podremos transformar una colección de objetos (arrays, lists,…) en una sucesión de objetos.
Dejaremos constancia de cómo se usaría este método para las clases ArrayList o para Arrays:
La interfaz List hereda de Collection por lo que cualquier clase que la implemente también podrá hacer uso de su método stream por lo que es fácil deducir lo siguiente:
List listaCadenas = new ArrayList(); //Notación diamante
List listaCadenas = new ArrayList();
listaCadenas.add("Juan");
listaCadenas.add("Antonio");
listaCadenas.add("Maria");
Stream streamCadenas = listaCadenas.stream();
En el caso de Arrays también podemos usar el método:
public static Stream stream(T[] array) {
return stream(array, 0, array.length);
}
Integer[] enteros = {1,2,3,4,5,6};
Stream enterosStream = Arrays.stream(enteros);
Además del método stream Java 8 incluye otro método muy en la línea: parallelStream. Para más información:
https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html#stream--
Perfecto, pero y con esto ¿qué hacemos? Shhh, tranquilidad…pasito a pasito, suave suave…ejem 😉 Seguimos.

Map, tu aliado
Aunque la interfaz Stream nos proporciona varios métodos, comenzaremos por éste: map. Si bien en las siguientes entradas no dejaremos en el olvido: filter, flatMap y reduce.
¿Cuántas veces hemos recorrido una estructura donde comprobando una condición realizábamos una acción u otra? ¿Cuántas líneas de código? ¿Cuántos for/if/else anidados de los que se quejaba Sónar (y nosotros mismos transcurridas unas semanas en las que olvidábamos qué hacía ese fragmento de código)?
Pues bien en Java 8 se nos permite hacer eso mismo en una línea.
Como antes lo primero es presentaros el método:
/**
* Returns a stream consisting of the results of applying the given
* function to the elements of this stream.
*
*
This is an intermediate
* operation.
*
* @param The element type of the new stream
* @param mapper a non-interfering,
* stateless
* function to apply to each element
* @return the new stream
*/
Stream map(Function mapper);
map aplicará una función que llameremos F sobre cada uno de los elementos de la sucesión y devolverá otra sucesión de elementos ya modificados.
¿Y esa función es…?
Cualquiera que necesitemos aplicar, eso sí cumpliendo unas restricciones.
Por ejemplo, podemos decir que queremos que haga un trim() a cada elemento de la sucesión de tipo String de la sucesión del primer elemento. Para ello podemos escribirlo así en Java 8:
streamCadenas.map(s->s.trim());
Es la forma en la que lo he empezado a usar y con ella sigo aunque no es la única.
En esta opción hacemos uso de una variable s, cuyo ámbito es el del método (fuera de ese map no existirá y no será necesario declararla previamente).
Estamos indicando que a cada elemento de esa sucesión lo vamos a guardar en una variable s, del mismo tipo del elemento, y vamos a aplicarle la función trim().
¿Por qué no hemos puesto String s->s.trim()? Porque Java 8 reconoce perfectamente el tipo de la variable s al conocer el tipo de los elementos de la sucesión (Stream). Si hubiésemos utilizado el Stream enterosStream s sería del tipo Integer y tampoco haría falta declararlo.
Otra forma de hacer lo mismo sería la siguiente:
streamCadenas.map(String :: trim).
La diferencia con la anterior es que nos ahorramos la variable, esta nomenclatura no es válida siempre. Debemos asegurarnos de que el método no tiene parámetros, si los tuviera no podría usarse de este modo aunque sí en su primera versión
Filter o cómo ahorrarme unos cuantos bucles
/**
* Returns a stream consisting of the elements of this stream that match
* the given predicate.
*
*
This is an intermediate
* operation.
*
* @param predicate a non-interfering,
* stateless
* predicate to apply to each element to determine if it
* should be included
* @return the new stream
*/
Stream filter(Predicate predicate);
Como en la propia API se indica, filter recibe una sucesión de elementos y devuelve aquellos que cumplan con el patrón buscado (predicate).
Empezamos mostrando un ejemplo básico.
Tenemos un listado de cadenas en las que guardamos tipos de vehículos y vamos a obtener de ellos los que no sean «motorbike»
List vehicles = Arrays.asList("car", "motorbike", "bus");
Antes haríamos algo de este tipo:
List filteredVehicles = new ArrayList();
for(String vehicle: vehicles){
if(!"motorbike".equals(vehicle)){
filteredVehicles.add(vehicle);
}
}
Con Java 8 sería tal que así:
List filteredVehicles = vehicles.stream()
.filter(v -> !"motorbike".equals(v))
.collect(Collectors.toList());
En nuestro caso, ya para un proyecto, teníamos una lista de tipo Profile llamada profiles donde teníamos instancias de varios tipos: INDIVIDUAL, CORPORATIVO… queríamos obtener tres de ese listado. Uno por cada tipo de perfil de los que constaba la aplicación.
En un principio recurríamos a los for/if en el que en el for iterábamos la lista profiles y en el if especificábamos la condición a cumplir. Como además eran varias listas las que queríamos como salida era necesario usar if/else anidados. Finalmente conseguimos hacerlo mucho más limpio con este método de Java 8. Abajo el ejemplo de una de las listas obtenidas.
En este caso, lo importante es ver otro tipo de estructura donde el predicado del filter es otro método: isType, un método propio.
List individuales = profiles.stream().filter(s -> isType(s, EnumTypeCertificate.INDIVIDUAL.name())).collect(Collectors.toList());
private boolean isType(Profile profile,String typeProfile)
{
return profile.getType().equals(typeProfile);
}
flatMap, magia!
/**
* Returns a stream consisting of the results of replacing each element of
* this stream with the contents of a mapped stream produced by applying
* the provided mapping function to each element. Each mapped stream is
* {@link java.util.stream.BaseStream#close() closed} after its contents
* have been placed into this stream. (If a mapped stream is {@code null}
* an empty stream is used, instead.)
*
*
This is an intermediate
* operation.
*
* @apiNote
* The {@code flatMap()} operation has the effect of applying a one-to-many
* transformation to the elements of the stream, and then flattening the
* resulting elements into a new stream.
*
*
Examples.
*
*
If {@code orders} is a stream of purchase orders, and each purchase
* order contains a collection of line items, then the following produces a
* stream containing all the line items in all the orders:
*
{@code
* orders.flatMap(order -> order.getLineItems().stream())...
* }
*
*
If {@code path} is the path to a file, then the following produces a
* stream of the {@code words} contained in that file:
*
{@code
* Stream lines = Files.lines(path, StandardCharsets.UTF_8);
* Stream words = lines.flatMap(line -> Stream.of(line.split(" +")));
* }
* The {@code mapper} function passed to {@code flatMap} splits a line,
* using a simple regular expression, into an array of words, and then
* creates a stream of words from that array.
*
* @param The element type of the new stream
* @param mapper a non-interfering,
* stateless
* function to apply to each element which produces a stream
* of new values
* @return the new stream
*/
Stream flatMap(Function<? super T, ? extends Stream> mapper);
En el ejemplo que vemos abajo teníamos el siguiente problema. Cada elemento Profile tenía un campo token de tipo cadena que contenía valores separados por coma.
Queríamos obtener cada uno de esos elementos y añadirlos a una lista pero sin elementos repetidos.
Para ello con map tranformamos la lista de objetos Profile en una sucesión de elementos a los que se aplicaba para su variable token el método split.
Imaginemos que tenemos algo así
public class Profile {
String token;
...
//Getters and Setters
...
}
El campo token tiene una valor como éste => token = «BROWSER,PKCS12,JKS»;
Ahora imaginemos que tenemos dos objetos Profile tendríamos algo así :
Lista de Profile
[0] Profile = > token = "BROWSER,PKCS12,JKS,USER_GENERATED,JKS,SOFT"
...
Lo que pretendemos obtener es esto:
Lista de String
[0] "BROWSER"
[1] "PKCS12"
[2] "JKS"
[3] "USER_GENERATED"
[4] "SOFT"
Y más aún, no queríamos en realidad la lista, queríamos devolver falso o cierto dependiendo de si el perfil contenía alguno de los tokens por lo que se debería mostrar en pantalla.
profiles.stream()
.map(s->StringUtils.split(s.getToken(), ","))
.flatMap(Arrays::stream)
.distinct()
.map(StringUtils::trim)
.collect(Collectors.toList())
.stream()
.anyMatch(EnumToken::isShowInMenu);
Para finalizar os dejamos el capítulo 1, de descarga gratuita, de la documentación Java 8.