mobile menu icon

Refactorizando con Streams

Publicado por José Déniz el 08/10/2015

Java, Functional Programming, Refactoring, Learning


Hoy traigo una serie de ejemplos de refactorización a modo de introducción a las nuevas características funcionales de Java 8. Así que, si tenías curiosidad y no te habías atrevido a experimentar con ellas, ¡te animo a que pruebes!

Empezamos definiendo una clase Person básica con un nombre y una edad:

public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
@Override
public String toString() {
return name + " - " + age;
}
}
view raw Person.java hosted with ❤ by GitHub

Una vez definida nuestra clase base, vamos a hacer un ejemplo sencillo: tenemos una lista de personas y queremos tener otra lista pero sólo con las personas que sean mayores de 22 años.

public class Main {
public static void main(String[] args) {
List<Person> people = Arrays.asList(new Person("Ronny", 20), new Person("Nick", 21),
new Person("Angelo", 21), new Person("Miguel", 22),
new Person("Juan", 22), new Person("Dani", 23),
new Person("Jose", 23), new Person("Ruben", 23));
List<Person> peopleOlderThanTwentyTwo = getPeopleOlderThanTwentyTwo(people);
System.out.println(peopleOlderThanTwentyTwo);
}
private static List<Person> getPeopleOlderThanTwentyTwo(List<Person> people) {
List<Person> peopleOlderThanTwentyTwo = new ArrayList<>();
for (Person person : people) {
if (person.age() > 22) {
peopleOlderThanTwentyTwo.add(person);
}
}
return peopleOlderThanTwentyTwo;
}
}
view raw Main.java hosted with ❤ by GitHub

Y ahora usando Java 8:

public class Main {
public static void main(String[] args) {
List<Person> people = Arrays.asList(new Person("Ronny", 20), new Person("Nick", 21),
new Person("Angelo", 21), new Person("Miguel", 22),
new Person("Juan", 22), new Person("Dani", 23),
new Person("Jose", 23), new Person("Ruben", 23));
List<Person> peopleOlderThanTwentyTwo = getPeopleOlderThanTwentyTwo(people);
System.out.println(peopleOlderThanTwentyTwo);
}
private static List<Person> getPeopleOlderThanTwentyTwo(List<Person> people) {
return people.stream()
.filter(person -> person.age() > 22)
.collect(Collectors.toList());
}
}
view raw Main.java hosted with ❤ by GitHub

Vamos a explicarlo un poco. Primero tenemos el método stream(), propio de las colecciones, que devuelve un Stream (conjunto inmutable de elementos y una serie de funciones con las que operar sobre el mismo). Seguimos con la función filter que, como su nombre indica, filtra los elementos a partir de un predicado (función que evalúa un argumento siendo el resultado verdadero o falso). Y por último_,_ tenemos que volver a transformar el stream en una lista, por lo que llamamos a la función collect y nos ayudamos de los Collectors para ello.

Podemos extraer el predicado en una variable para tener una mejor legibilidad:

private static List<Person> getPeopleOlderThanTwentyTwo(List<Person> people) {
Predicate<Person> isOlderThanTwentyTwo = person -> person.age() > 22;
return people.stream()
.filter(isOlderThanTwentyTwo)
.collect(Collectors.toList());
}

Ahora pongamos otro ejemplo. Seguimos teniendo la misma lista de personas pero ahora queremos un String que contenga los nombres de las personas que sean menores de 22 años separados por ”, “.

Java 7:

private static String getNamesOfPeopleYoungerThanTwentyTwo(List<Person> people) {
String names = "";
for (Person person : people) {
if (person.age() < 22) {
names += person.name() + ", ";
}
}
return names.substring(0, names.length() - 2);
}
view raw MapMethod.java hosted with ❤ by GitHub

Java 8:

private static String getNamesOfPeopleYoungerThanTwentyTwo(List<Person> people) {
Predicate<Person> isYoungerThanTwentyTwo = person -> person.age() < 22;
return people.stream()
.filter(isYoungerThanTwentyTwo)
.map(Person::name)
.collect(Collectors.joining(", "));
}
view raw MapMethod.java hosted with ❤ by GitHub

En este ejemplo, vemos varias cosas nuevas como la función map, que devuelve un nuevo Stream del mismo tamaño, pero con la diferencia que cada elemento es el resultado de aplicar la función pasada por parámetro sobre cada uno de ellos (en este caso, me devuelve un Stream<String>. Además, vemos como podemos sustituir nuestra función lambda por una referencia al método, es  decir, hemos sustituído:

person -> person.name() por Person::name

Ya que queda más compacto y fácil de leer. Y por último, unimos todos los nombres separados por ”, “ sin tener que estar preocupándonos en si se nos añade o no una ”, “ de más.

A modo de conclusión, me gusta más ésta forma de programar declarativamente, ya que mejora mucho la legibilidad del código e incluso es más fácil de mantener. Ya vimos por ejemplo que, para reducir la lista de personas a una lista de nombres, bastó con añadir una línea llamando a la función map y listo.

Publicado originalmente en el blog de José Déniz.

Volver a posts