domingo, 30 de noviembre de 2014

Acceso a datos con Spring y ORM (parte 1)

Acceso a datos ORM Spring

 El acceso a datos a través de ORM y Spring

1 ORMs y Spring

El Framework Spring 3 permite la integración con Hibernate, Java Persistence API (JPA) y Java Data Objects (JDO), para gestionar los recursos , el acceso a datos a través de implementaciones DAO, y transacciones. Un ejemplo, Hibernate, existe un API de soporte que resuelve de forma correcta la aplicación de IoC, concernientes a varios aspectos de integración con Hibernate. De ésta manera, podemos configurar todas las caracteristicas soportadas en un mapeo O/R con la caracteríscita de DI. Pueden aplicarse en la manera en la que Spring gestiona recursos y transacciones y ser compatibles con la implementación Spring 3 de trasacciones genericas y la jerarquia de excepciones para transacciones y DAOs. Para una integración con Spring 3, es recomendable implementar los DAO directamente en el API soportada, ya sea de: Hibernate, JPA o JDO. Recormamos que, en las primeras versiones de Spring 3, se aplicaba la integración a partir de "DAO templates", - no recomendado actualmente-, en la sección “Classic ORM usage” se encontrará más información acerca de esta técnica.

Se añaden mejoras significativas a la capa ORM, para las aplicaciones que acceden a la capa de recursos. Es posible, elegir el grado de integración deseado según la necesidad, para lo que debemos sopesar, el esfuerzo de integración con los costes y riesgos de "reimplementar lo existente" . Es posible reutilizar el soporte ORM como si se tratase de un componente más, ya que el diseño se basa en un conjunto de JavaBeans. ORM (ej. Hibernate) en un contenedor Spring 3 simplifica, la configuración y el despliegue.

Las ventajas de usar Spring 3 para manejar los DAO de un ORM son:

  • Facilita los test. La aproximación Spring nos permite intercambio de implementación y confugración en una instacia SessionFactory de Hibernate, o un DataSource JDBC, un gestor de transacciones u otra implementación de DAO (necesario, como un DAO tipo MockObject).
  • Excepciones de acceso a datos genéricas. Permite aplicar "wrappers" de las excepciones propias del ORM que utilizamos, pasándolas a la jerarquia común (DataAccessException) de excepciones de java runtime. Esto nos permite manejar la mayoría de excepciones de persistencia, que suelen ser críticas, no recuperables, en las capas apropiadas, es decir que nos evita la escritura de de complicados manejadores de excepciones de acceso a datos en capas de las que no son específicas. Sin perder, la posibilidad de manejar dichas excepciones, como se desee. Recordamos que tambien las excepciones JDBC, se convierten a la misma jerarquia). Lo que nos facilita el uso de JDBC y al mismo tiempo disponer una aplicación consistente y débilmente acoplada.
  • Un manejo de recursos genérico a partir del contenedor. Los contextos Spring permitenn manejar configuración y ubicación de las instancias : SessionFactory de Hibernate, EntityManagerFactory de JPA y DataSource de JDBC y otro tipo de recursos parecidos (ej: un base de datos de fichero) y realizar cambios de forma sencilla. Por ejemplo, es posible gestionar de forma sencilla y eficiente el manejo de recursos. El código que se usa generalmente en Hibernate, a menudo necesita compartir la mísma Session Hibernate para asegurar eficiencia y un manejo de transacciones correcto. Spring 3 permite un manejo simple que permite recuperar y asociar la Session a CurrentThread de forma transparente, mediante SessionFactory Hibernate. Así resolvemos varios problemas comunes en el uso de Hibernate clásico, para cualquier uso, ya sea de manera local o a través de JTA de las transacciones.
  • Manejo Integrado de Transacciones. Podemos crear un "wrapper" Spring de nuestro código de ORM, de manera declarativa, y aplicar, declarar, aspectos (AOP) al estilo de "interceptors", ya sea a partir de anotaciones, @Transactional, o bien declarar explícitamente un "advice" en el fichero de configuración XML. En ambos casos, la semántica transaccional y el manejo de excepciones (ej. ejecutar "rollback", "commit",...) se manejan de manera automática. Como se discute en este artículo Rersource and transaction management, vemos que es posible intercambiar entre distintos gestores de transacciones, sin tener que modificar nuestro código ORM. Por ejemplo, intercambiar entre transacciones Local o bien a través de JTA y por tanto disponer de la mísma funcionalidad (a través de la configuración de transacciones) en ambos escenarios. De forma adicional, el código JDBC puede integrarse a nivel de transacción con el código que manejamos en el ORM. Esta integración, "facilita que sea posible el acceso a aquellos datos que no son apropiados para ser accedidos directamente a través de ORM, como por ejemplo: datos manejados en procesos batch y acceso a BLOBs en streaming. Y por tanto, compartir el mísmo espacio de transacciones.

lunes, 24 de noviembre de 2014

Inyección de EJBs con Spring (Spring Integration)

 Enterprise JavaBeans (EJB) integration

1 Introducción

Como contenedor ligero, Spring se considera a menudo un sustituto a la tecnología EJB. Creemos que para muchas si no la mayoría de los casos de uso en una aplicación, un contenedor Spring, combinado junto con una rica funcionalidad en el area de transacciones, ORM y JDBC, es una mejor elección que implementar una funcionalidad equivalente a partir de un contenedor EJB.

Sin embargo, es importante tener en cuenta que Spring no impide el uso de EJBs. De hecho, como veremos a lo largo del artículo, Spring hace más fácil el acceso a los EJBs, permitiendo dicha funcionalidad. Además usar Spring para el acceso a los servicios que aporta la tecnología EJBs, nos permite implementar dichos servicios y para más adelante poder intercambiarlos entre EJBs locales o remotos o bien una variante POJO (plain old Java object), con la ventaja de no tener que modificar el código.

En este capítulo, veremos como Spring puede ayudarnos a acceder e implementar EJBs junto al valor añadido que Spring aporta en el acceso a los stateless session beans (SLSBs)..

2 Acceso a EJBs

2.1 Conceptos

Para invocar un método de un stateless session bean local o remoto, el código cliente ha de realizar una llamada al método JNDI lookup para obtener el objeto EJB Home (local ó remote), para a continuación llamar al método create method en dicho objeto y obtener la verdadera referencia al objeto (local ó remote) EJB. Una vez hecho esto, los métodos se invocan en el EJB.

Para poder evitar esta ejecución repetida de código de bajo nivel, muchas aplicaciones EJB utilizan los patrones "Service Locator" y "Business Delegate". Esto es mejor que lanzar JNDI lookups dentro del código cliente, pero el uso repetido de esta implementación tiene grandes desventajas. Por ejemplo:

  • El codigo para usar EJBs está asociado típicamente a la creación de "Singletons" a través de "Service Locator" o " Business Delegate", esto los hace difíciles de testear.
  • En el caso del patrón "Service Locator" usado sin "Business Delegate", el código de la aplicación debe además realizar una llamada al método "create()" del EJB Home, y manejar las posibles excepciones. Por esto, se aproxima al API EJB y la complejidad del dicho modelo de programación EJB.
  • Implementar el patrón "Business Delegate" resulta a menudo engorroso o redundante en código, cuando por ejemplo, necesitamos realizar una simple llamada a un servicio EJB.

La aproximación Spring, consiste en permitir la creación y el uso de objetos Proxy, que son generalmente se configuran dentor del contenedor Spring, que actúan como business delegates de código simplificado. Esto nos evita reescribir Service Locator, JNDI lookup, o duplicar métodos codificados a mano dentro de "Business Delegate", a menos que sea necesaria una verdadera modificación de la funcionalidad básica.

2.2 Acceso a Session Stateless local Beans

Supongamos que tiene un controlador web que necesita usar un EJB. Seguiremos una buena prática de EJB Business Methods Interface pattern, de manera que la interfaz local EJB’s hereda de la interfaz de EJB - la cual no es específicamente una interfaz de business methods. Llamemos esta interfaz no específica EJB MyComponent.

public interface MyComponent {
    ...
}

Una de las principales razones para utilizar el patrón Business Methods Interface, es asegurarnos que "la sincronización entre llamadas a métodos en la interfaz local y la implementación de dicho bean sea automática". Otra razón, es que, más adelante se haga mucho más sencillo para nosotros intercambiar entre una implementación POJO (plain old Java object) de nuestro servicio, si es necesario. Además debemos, implementar la interfaz local home y la clase que implementa el SessionBean con los métodos de la interfaz de negocio de MyComponent. De esta manera, el único código Java que debemos modifiar dentro del controlador de la capa web para la invocación de EJB es crear un métod setter de MyComponent. Esto guardará la referencia como una variable de instancia dentro del controlador.

private MyComponent myComponent;

public void setMyComponent(MyComponent myComponent) {
    this.myComponent = myComponent;
}

Podemos de esta manera usar dicha variable de instancia y cualquier método de negocio dentro del controlador.

Ahora, teniendo en cuenta que estamos recuperando un objeto de controlador a partir del contenedor Spring, podemos (de la mísma manera) configurar una instancia de tipo LocalStatelessSessionProxyFactoryBean, que sera un objeto proxy EJB. La configuración de dicho proxy, y el set de la propiedad myComponent del controlador se realiza de la forma siguiente:

<bean id="myComponent"
        class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
    <property name="jndiName" value="ejb/myBean"/>
    <property name="businessInterface" value="com.mycom.MyComponent"/>
</bean>

<bean id="myController" class="com.mycom.myController">
    <property name="myComponent" ref="myComponent"/>
</bean>

Lo que sucede por detrás, es gracias al framework "Spring AOP". La definición del bean myComponent crea un proxy para el EJB, que implementa la interfaz del método de negocio. La interfaz local EJB, se cachea en el arraque, así que solo existe un sólo JNDI lookup. Cada vez se llama a un EJB, el proxy realiza una llamada a un método classname dentro del EJB local y a continuación se llama al correspondiente método de negocio del EJB.

La definición del bean myController establece la propiedad property de myComponent en dicho controlador al proxy EJB.

De manera altenativa (y de preferencia en caso de existir muchas de dichas definiciones de proxy), podemos usar la configuración <jee:local-slsb> dentro del "namespace" Spring "jee":

<jee:local-slsb id="myComponent" jndi-name="ejb/myBean"
        business-interface="com.mycom.MyComponent"/>

<bean id="myController" class="com.mycom.myController">
    <property name="myComponent" ref="myComponent"/>
</bean>

Este acceso a los EJB proporciona una gran simplificación del código de la capa web (o cualquier otro código EJB cliente), para eliminar la dependencia al API propia cuando usamos EJBs. Si queremos reemplazar esta referencia a EJB con una referecia a POJO o un objeto mock u otro tipo de referencia a objeto para realización de test, esto sería posible cambiando la definición del bean myComponent sin cambiar una sola línea de código Java. Además, no tendríamos que escribir ni una sola línea de llamadas JNDI lookup u otro tipo de código comúnmente usado en la tecnología EJB, para nuestra aplicación.

En cuanto al rendimiento, los test y la experiencia, demuestran que la carga cuando referenciamos EJB a partir de Sping (lo que implica una llamada por "reflexión" a el EJB) es mínima y apenas se diferencia de la realización de una llamada clásica a EJB (lo que intentamos evitar además de evitar el coste asociado con la infraestructura del contenedor EJB en el servidor de aplicación).

Existe un punto respecto al JNDI lookup. En un contenedor bean, esta clase se usa de forma más eficiente como Sigleton (no existe una razón para crearla como Prototipo). Sin embargo, si este contenedor bean realiza pre-instanciaciones de Singletons (tal como hacen las diferentes versiones del XML ApplicationContext ), podemos tener un problema "si el contenedor bean se carga antes que el contenedor EJB haya cargado el EJB que referenciamos". Esto ocurre porque la llamada a JNDI lookup se realiza en el método init() de esta clase y a continuación se guarda en memoria, sin embargo el stub EJB no se habrá cargado aún en el destino. La solución pasa por no pre- instanciar este método factory object, sino permitir, "que se cree en la primera llamada". En los contenedores XML, esto se controla a través del atributo lazy-init.

Aunque esto no es relevante en el manejo de Spring en la mayoría de los casos, este problema de sincronización (entre cargas de contenedores y creación ) planteado, se resuelve a través de "AOP junto con EJBs", a través de la clase Interceptor: LocalSlsbInvokerInterceptor.

2.3 Acceso a remote State Less Session Bean

El acceso a un EJB de tipo remote es esencialmente idéntico al acceso a un EJB de tipo local, excepto que debemos usar la configuración SimpleRemoteStatelessSessionProxyFactoryBean or <jee:remote-slsb>. Desde luego, con o sin Spring, en una invocación remota se usa la semántica propia, "una llamada a un método de un objeto dentro de otra VM que se encuentra en una máquina distinta", debe tratarse a menudo de forma diferente según los distintos escenarios y el manejo de errores.

El soporte de Spring’s para los EJB cliente añade una ventaja sobre la aproximación no Spring. Normalmente, el código cliente de un EJB presenta complejidad a la hora de intercambiar entre invocaciones locales y remotas. Esto es debido a que los métodos de la interfaz remota junto a su invocación han de capturar una excepción de tipo RemoteException, el código cliente debe por lo tanto manejar esto, mientras que una invocación a un EJB local no. El código cliente escrito para los EJB de tipo local, puede o bien dejarlo igual pero haciendo un innecesario manejo de excepciones remotas, o bien necesita modificarse para eliminar este código. Con el proxy Spring EJB remoto, es posible evitar declarar la excepción de tipo RemoteException en su "Business Method Interface" y tener que implementar código EJB, ya que en Spring declarar una interfaz remota hace que por defecto lance una excepción de tipo RemoteException. De esta manera, podemos centrarnos en el proxy para tratar de forma dinámica las dos interfaces ya que ambas son la misma. Es decir, el código cliente no ha de gestionar con la indicada RemoteException. Ninguna RemoteException que se lance durante la invocación del EJB se re- lanzará como si fuese la no indicada RemoteAccessException class, lo que es una subclase de RunTimeException. Además el servicio que deseamos invocar puede ser intercambiado según deseemos, ya sea como una implementación EJB local , remote o un POJO, sin que tener que preocuparnos de conocer la implementación del código cliente. Desde luego, esto es opcional, no hay impedimento en declarar por nuestra parte una excepción RemoteException en la interfaz de negocio.

2.4 Acceso a EJB 2.x SLSBs versus EJB 3 SLSBs

El acceso a EJB 2.x beans de Session y EJB 3 de beans de Session a través de Spring es ampliamente transparente. La forma de acceder a Spring’s EJB, incluyendo el <jee:local-slsb> y <jee:remote-slsb> utilidades, adapta de forma transparente al verdadero componente en tiempo de ejecución. Se maneja un interfaz home (EJB 2.x style), o realizar invocaciones directas al componente si no existe ningún interfaz disponible (EJB 3 style).

Nota: Para los beans de session EJB 3, podemos efectivamente usar también un JndiObjectFactoryBean / <jee:jndi-lookup>, ya que se trata de un componente completamente disponible para los JNDI lookups. Declarar explicitamente lookups lo podemos hacer a través de <jee:local-slsb> / <jee:remote-slsb> sencillamente aporta una forma consistente y de forma más explícita acceso a la configuración EJB.

3 Usar las clases de soporte Spring’s EJB implementation

3.1 Interceptor para la inyección de EJB 3

Para los beans de session EJB 3 y los Message-Driven Beans, Spring trae un interceptor adecuado que resuelve la anotación Spring’s @Autowired en el componente EJB: org.springframework.ejb.interceptor.SpringBeanAutowiringInterceptor. Este interceptor puede aplicarse a través de la anotación @Interceptors en el componente EJB, o a través de un binding interceptor declarado a través de interceptor-binding XML en el descriptor de despliegue EJB.

@Stateless
@Interceptors(SpringBeanAutowiringInterceptor.class)
public class MyFacadeEJB implements MyFacadeLocal {

    // automatically injected with a matching Spring bean
    @Autowired
    private MyComponent myComp;

    // for business method, delegate to POJO service impl.
    public String myFacadeMethod(...) {
        return myComp.myMethod(...);
    }

    ...

}

SpringBeanAutowiringInterceptor por defecto obtiene beans a partir del ContextSingletonBeanFactoryLocator, con el contexto declarado en el fichero "beanRefContext.xml". Por defecto, se espera una sola definicion, lo que se obtiene por tipo en lugar de por nombre. Sin embargo, si necesitamos elegir entre varias definicions de contexto, es necesario una clave "locator". La clave locator (por ejemplo el nombre de la definición de contexto en beanRefContext.xml), lo que podemos declarar de forma explicita sobrecargando el método getBeanFactoryLocatorKey en una subclase de SpringBeanAutowiringInterceptor.

De forma alternativa, es posible sobrecargar el método de la clase, SpringBeanAutowiringInterceptor's getBeanFactory, por ejemplo, obteniendolo de un ApplicationContext a partir de una clase que lo recoja.

domingo, 23 de noviembre de 2014

Reduccion en Colletions (Java 8)

Reduction

La sección de Aggregate Operations explica la siguiente cadena de operaciones, que calcula la edad media de edad de hombres en la colección roster:

double average = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .mapToInt(Person::getAge)
    .average()
    .getAsDouble();

El JDK contiene muchas operaciones primitivas (como average, sum, min, max, y count) que devuelven un valor generado a partir de combinar el contenido de un stream. Estas operaciones se denominan reduction operations. El JDK contiene además operaciones que devuelven una colección en lugar de un valor simple. Muchas de las operaciones de reducción realizan una tarea concreta, como por ejemplo calcular la media o agrupar los elementos en categorias. Sin embargo, el JDK aporta las operaciones genericas de reduce y collect, que explicaremos en detalle..

Esta sección contiene los siguientes temas:

Puedes obtener el código fuente del ejemplo en ReductionExamples.

Método Stream.reduce

Stream.reduce es un método de propósito general para la operación de reducción. Consideremos la siguiente pipeline, que calcula la suma de hombres en la colección roster. Se realiza la llamada a la operación de reducción Stream.sum:

Integer totalAge = roster
    .stream()
    .mapToInt(Person::getAge)
    .sum();

Comparemos esto con la siguiente pipeline, que llama a Stream.reduce para realizar la mísma operación:

Integer totalAgeReduce = roster
   .stream()
   .map(Person::getAge)
   .reduce(
       0,
       (a, b) -> a + b);

La operación reduce en este ejemplo toma dos argumentos:

  • identity: El elemento identity es al mismo tiempo el valor inicial de la reducción y el valor de retorno por defecto si no existen otros elementos en el stream. En este ejemplo, el elemento identity es 0; este es el valor inicial de la suma de edades y el valor por defecto si no existen otros elementos en la colección roster.

  • accumulator: La función accumulator toma dos parámetros: como resultado parcial de la reducción (en este ejemplo, la suma de todos los números procesados hasta el momento) y el siguiente parámetro es el siguiente elemento del stream (en el ejemplo, el siguiente número). El valor de retorno es el resultado parcial. En el ejemplo, la función accumulator es una expresión lambda que suma dos Integer y devuelve un Integer:

    (a, b) -> a + b

La operación reduce devuelve siempre un nuevo valor. Sin embargo, la función accumulator retorna tambien un nuevo valor cada vez que se procesa un elemento del stream. Supongamos que queremos reducir los elementos de un stream para obtener un objeto más complejo, como una Collection. Esto puede hinder () el rendimiento de la aplicación. Si se aplica la operación reduce esto implica añadir los elementos a la colección, y a continuación cada vez que la función accumulator procesa el elemento, se crea una nueva colección que contiene el elemento, lo cual es ineficiente. Una solución más conveniente seria que actualizasemos una colección ya existente. Esto puede realizarse con Stream.collect , lo que describimos en el siguiente ejemplo:

Método Stream.collect

A diferencia del método reduce , que crea siempre un nuevo valor cuando procesamos un elemento, el método collect modifica o transforma un valor existente.

Veamos como calcular la media entre los valores de un stream. Necesitamos dos datos: el número total de valores y la suma de dichos valores. En contraposición a el método reduce u otro tipo de reducciones, collect mdevuelve un único valor. Puedes crear un nuevo tipo de dato que contenga variables internas que recojan el número de elementos procesados y la suma de dichos valores, tal como muestra la siguiente clase, Averager:

class Averager implements IntConsumer
{
    private int total = 0;
    private int count = 0;
        
    public double average() {
        return count > 0 ? ((double) total)/count : 0;
    }
        
    public void accept(int i) { total += i; count++; }
    public void combine(Averager other) {
        total += other.total;
        count += other.count;
    }
}

La siguiente pipeline incluye la clase Averager y el método collect para calcular la media de edad de hombres.

Averager averageCollect = roster.stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(Person::getAge)
    .collect(Averager::new, Averager::accept, Averager::combine);
                   
System.out.println("Average age of male members: " +
    averageCollect.average());

collect en este ejemplo toma tres parametros:

  • supplier: supplier es una función factory; contruye nuevas instancias. La operación collect, crea instancias del resultado, en este caso una nueva instacia de la clase Averager.
  • accumulator: Incorpora un elemento de tipo stream dentro de un objeto resultado. En este caso, modifica el resultado Averager incrementando la variable count en uno y sumando el valor a la variable total con el valor del elemento del stream, que es el entero que contiene la edad de un hombre.
  • combiner: Toma dos objetos resultado y mezcla sus contenidos. En este ejemplo, modifica un resultado Averager incrementando su variable count con el valor de la variable count del otro miembro Averager y sumando la variable total con la otra variable total del otro miembro Averager.

Veamos los siguiente :

  • Supplier es una expresión lambda (o una referencia a método) en contraposición a lo que vemos en la operación reduce con el elemento identity.
  • Las funciones accumulator y combiner no devuelven resultado.
  • Podemos usar operaciones de tipo collect junto con parallel streams; ver la sección Parallelismo para más información. (Si lanzamos un método collect con un parallel stream, JDK crea un nuevo thread cada vez que la función combiner crea un nuevo objeto, como el objeto Averager en el example. Como consecuencia, no es nesario que tengamos que ocuparnos de problemas de sincronizacion.)

Aunque el JDK contiene un método average por defecto para calcular la media de elementos en un stream, podemos usar la operación collect y una clase propia si lo que queremos es calcular varios valores a partir de los elementos de un stream.

Para el manejo de colecciones es recomendable usar la operación collect . El ejemplo siguiente añade los nombres de hombres de los objetos persona leidos, dentro de una colección a través de la operación collect:

List<String> namesOfMaleMembersCollect = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(p -> p.getName())
    .collect(Collectors.toList());

Esta versión de collect toma un parámetro de tipo Collector. Esta clase encapsula las funciones que se usan como argumentos en la operación collect la cual requiere tres parámetros (supplier, accumulator, y combiner functions).

La clase Collectors contiene muchas operaciones de reducción, como almacenar (accumulate) dentro de colecciones y resumirlos (procesar un filtrado sobre dichos elementos) según varios criterios. Estas operaciones de reducción devuelven instancias de la clase Collector, que podemos usar como parámetro de la operación collect.

En este ejemplo se usa Collectors.toList , que acumula los elementos del stream dentro de una nueva instacia de List. Como la mayoria de operaciones de la clase Collectors , el operador toList devuelve una instancia de Collector, y no una colección.

El ejemplo siguiente agrupa los miembre de la colección roster por género:

Map<Person.Sex, List<Person>> byGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(Person::getGender));

La operación groupingBy devuelve un map cuyas claves son los valores que resultan de aplicar la expresión lambda que se pasa como parámetro (llamada classification function). En el ejemplo, el map devuelto contiene dos claves, Person.Sex.MALE y Person.Sex.FEMALE. Los valores asociados a las claves son instancias de tipo List que contienen los elementos del stream que, una vez procesados por la función de clasificación, se asocian al valor de la clave. Por ejemplo, el valor que se corresponde con Person.Sex.MALE es una instancia de tipo List que contiene todo los elementos persona de género hombre.

El siguiente ejemplo recupera los nombres de cada miembro de la colección roster y los agrupa por género:

Map<Person.Sex, List<String>> namesByGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(
                Person::getGender,                      
                Collectors.mapping(
                    Person::getName,
                    Collectors.toList())));

La operación groupingBy en este ejemplo toma dos parámetros, una función de clasificación y una instancia de Collector. El parámetro Collector es llamado downstream collector. Esto es un collector que el runtime Java aplica a los resultados de otro collector. En consecuencia, esta operación groupingBy nos permite aplicar el método collect a los valores del List que han sido creados por el operador groupingBy . Este ejemplo aplica el collector mapping, que realiza un maping de la función Person::getName a cada uno de los elementos del stream. De esta manera, el stream resultante contiene únicamente los nombres de los elementos. El pipeline que contiene uno o más collectors del stream de bajada, se denomina un multilevel reduction.

El siguiente ejemplo recupera la edad de los miembros agrupados por género:

Map<Person.Sex, Integer> totalAgeByGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(
                Person::getGender,                      
                Collectors.reducing(
                    0,
                    Person::getAge,
                    Integer::sum)));

La operación de reduccion toma tres parámetros:

  • identity: Como la operación Stream.reduce , el elemento identity es el elemento inicial de la reducción y el resultado si no existen elementos en el stream. En el ejemplo, el elemento identity es 0; este es el valor inicial de la suma de edades y el valor por defecto si no existen miembros.
  • mapper: La operación de reduccion aplica la función de mapeo a todos los elementos del stream. En el ejemplo, el mapper recupera la edad de cada elemento.
  • operation: La función operation se usa para reducir los valores mapeados. En el ejemplo, la función de operación suma valores Integer.

El ejemplo siguiente recupera la media de edad de los elementos de cada uno de los géneros:

Map<Person.Sex, Double> averageAgeByGender = roster
    .stream()
    .collect(
        Collectors.groupingBy(
            Person::getGender,                      
            Collectors.averagingInt(Person::getAge)));