Java 8, implementación y utilización de java.util.stream.Collector

Java

Una de las nuevas interfaces que ha traído Java 8 es la interface

java.util.stream.Collector<T,A,R>

Muchos nos preguntaremos para qué sirve, y como podemos utilizarla, y para ello no hay nada mejor que entender y comprender cómo funciona.

La interface java.util.stream.Collector

Un Collector los vamos a utilizar en un Stream de elementos T para construir una nueva Colección (List, Set, etc) R, para reducir los elementos del Stream en una nueva colección R más pequeña o para obtener un único resultado R.

En esta interface tenemos que entender qué es cada elemento que lo forma.

  • T, representa cada uno de los elementos del Stream que va siendo consumido o entrando en el Collector.

  • A, representa una estructura de datos dentro del Collector donde iremos almacenando los elementos T del Stream. Este tipo de dato puede ser del mismo tipo que R, pero puede ser cualquier otro tipo de estructrura o clase.

  • R, representa la estructura de datos final con el resultado del Collector.

Y para lograr este resultado, la clase que implementa esta interface debe implementar los siguientes métodos abstractos.

  • supplier(), devuelve una función que crea el acumulador A para almacenar los elementos T del Stream.

  • accumulator(), devuelve una función que coge el acumulador A y un elemento T y lo añade en el acumulador A.

  • combiner(), devuelve una función que combina dos acumuladores A en uno solo. Se utiliza cuando el Stream es ejecutado en paralelo.

  • finisher(), devuelve una función que transforma el acumulador A en el resultado R esperado.

  • characteristics(), informa de las características que nuestra implementación Collector va a tener. Estas pueden ser cualquiera de las definidas en java.util.stream.Collector.Characteristics, que son CONCURRENT, IDENTITY_FINISH, UNORDERED.

Ejemplo

Y como siempre para comprender mejor estos conceptos vamos a crear un ejemplo. Sabemos que la misma funcionalidad implementada en este ejemplo puede ser implementada sin hacer uso de un Collector.

El ejemplo consiste en, partiendo de una lista de Personas (clase User) obtener una lista clasificada de departamentos (clase Departament), donde veamos el número de empleados de cada uno de ellos y quien es su CEO.

Clase User

package sample.collectors;

public class User {
    private String name;
    private String departament;
    private Boolean ceo;
    public String getName() {
        return name;
    }
    public User setName(String name) {
        this.name = name;
        return this;
    }
    public String getDepartament() {
        return departament;
    }
    public User setDepartament(String departament) {
        this.departament = departament;
        return this;
    }
    public Boolean getCeo() {
        return ceo;
    }
    public User setCeo(Boolean ceo) {
        this.ceo = ceo;
        return this;
    }   
}

Clase Departament

package sample.collectors;

public class Departament {
    private String name;
    private User chiefExecutiveOfficer;
    private Integer employeeNumber;

    public User getChiefExecutiveOfficer() {
        return chiefExecutiveOfficer;
    }
    public Departament setChiefExecutiveOfficer(User chiefExecutiveOfficer) {
        this.chiefExecutiveOfficer = chiefExecutiveOfficer;
        return this;
    }
    public Integer getEmployeeNumber() {
        return employeeNumber;
    }
    public Departament setEmployeeNumber(Integer employeeNumber) {
        this.employeeNumber = employeeNumber;
        return this;
    }
    public String getName() {
        return name;
    }
    public Departament setName(String name) {
        this.name = name;
        return this;
    }   
}

Implementación de la interface Collector

package sample.collectors;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

public class DepartamentCollector implements Collector<User, Map<String, List<User>>, List<Departament>> {

    @Override
    public BiConsumer<Map<String, List<User>>, User> accumulator() {
        return (acc, user) -> {
            if (acc.get(user.getDepartament()) == null ) {
                acc.put(user.getDepartament(), new ArrayList<User>());
            }
            acc.get(user.getDepartament()).add(user);
        };
    }

    @Override
    public Set<java.util.stream.Collector.Characteristics> characteristics() {
        return EnumSet.of(Characteristics.UNORDERED);
    }

    @Override
    public BinaryOperator<Map<String, List<User>>> combiner() {
        //
        // Implementar este metodo para soportar Stream en paralelo. La implemetacion
        // devuelve una interface funcional responsable de combinar dos
        // estructuras de tipo Map<String, List<User>> en una sola
        //
        return null;
    }

    @Override
    public Function<Map<String, List<User>>, List<Departament>> finisher() {
        return (acc) -> {        
            List<Departament> departaments = new ArrayList<Departament>();
            Iterator<String> depts = acc.keySet().iterator();
            while(depts.hasNext()) {
                String dptoName = depts.next();
                Departament dpto = new Departament();
                dpto.setName(dptoName);
                Optional<User> uu = acc.get(dptoName).stream().filter((u)->u.getCeo()).findFirst();
                if (uu.isPresent()) {
                    dpto.setChiefExecutiveOfficer(uu.get());
                }
                dpto.setEmployeeNumber(acc.get(dptoName).size());
                departaments.add(dpto);
            }
            return departaments;
        };
    }

    @Override
    public Supplier<Map<String, List<User>>> supplier() {
        return () -> new HashMap<String, List<User>>();
    }   
}

Clase principal

package sample.collectors;

import java.util.ArrayList;
import java.util.List;

public class FilterUsers {

    public static void main(String[] args) {
        List<User> users = new ArrayList<User>();
        users.add(new User().setName("Jordan").setDepartament("recursos humanos").setCeo(true));
        users.add(new User().setName("Larry").setDepartament("recursos humanos").setCeo(false));
        users.add(new User().setName("Jose").setDepartament("recursos humanos").setCeo(false));
        users.add(new User().setName("Cooper").setDepartament("ventas").setCeo(true));
        users.add(new User().setName("Eduardo").setDepartament("ventas").setCeo(false));        
        users.add(new User().setName("Antonio").setDepartament("ventas").setCeo(false));
        users.add(new User().setName("Anthony").setDepartament("ventas").setCeo(false));
        users.add(new User().setName("Michael").setDepartament("contabilidad").setCeo(true));
        users.add(new User().setName("Anderson").setDepartament("contabilidad").setCeo(false));
        users.add(new User().setName("Alan").setDepartament("contabilidad").setCeo(false));

        List<Departament> dptos = users.stream().collect(new DepartamentCollector());
        dptos.stream().forEach((dd) -> System.out.println(dd.getName() + " (" + dd.getEmployeeNumber()+") ceo: " + dd.getChiefExecutiveOfficer().getName() ));

    }

}

Y eso es todo. Podemos crear implementaciones de Collector más simples que esta o más complejas. Todo depende de la necesidad o de lo cómodo que nos sintamos con la utilización de esta interface.