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.

Genéricos en Java, declaración y uso de los tipos y métodos genéricos

Java

En Java hemos utilizado desde la JDK 5.0 los tipos genéricos o Generic Types, y aunque estamos acostumbrados a utilizarlos, en este artículo vamos a resumir su sintaxis y utilización.

  • Tipos genéricos o Generic Types son clases o interfaces parametrizadas por un tipo
  • Métodos genéricos o Generic Methods son métodos donde se definen y utilizan los tipos genéricos pero que están limitados al ámbito del método donde se declara

Por tanto en una clase genérica podemos tener métodos genéricos y métodos normales, y en una clase normal podemos tener métodos genéricos y métodos normales.

Sigue leyendo

Genéricos en Java, donde y cómo utilizar los wildcards o comodines

Java

Los wildcards o comodines representan en los tipos genéricos de Java (Java Generics) un tipo desconocido. Deberíamos evitar su utilización y ser siempre lo más especificos posible. Cuando esto no es posible utilizaremos los wildcards o comodines.

¿Cómo y donde podemos utilizar los comodines o wildcards?

  • cómo tipo de un parámetro, es decir, en las definiciones de los parámetros de los métodos
  • cómo variable local, es decir, en las definiciones de las variables locales
  • cómo campo o propiedad de una clase, es decir, en las definiciones de las propiedades de una clase
  • cómo tipo devuelto por un método, es decir, en las definiciones del tipo devuelto por un método

El código de ejemplo no tiene lógica o sentido práctico, solo pretende mostrar donde y como utilizar los wildcards.

public class MiTipo<T> {

    private T t;

    public MiTipo(T t) {
        this.t = t;
    }

    public T getT() {
        return this.t;
    }

}
import java.io.File;
import java.io.Reader;
import java.util.List;

public class UsoDeWildcards {

    /* como campo o propiedad de clase */
    public MiTipo<? extends Reader> tipo1;
    public List<? extends File> ficheros;

    /* como tipo devuelto por un metodo */
    public MiTipo<? extends String> usoUno() {
        /* ... */
        return null;
    }

    /* como tipo de parametro o argumento de un metodo */
    public void usoDos(MiTipo<? super Integer> mitipo) {
        /* ... */
    }

    /* como variable local */
    public void usoTres() {
        @SuppressWarnings("unused")
        MiTipo<? extends String> mitipo;
        /* ... */
    }

}