Llevamos mucho tiempo esperando que lambda traiga el concepto de cierres a Java, pero si no lo usamos en colecciones, perdemos mucho valor. El problema de migrar las interfaces existentes al estilo lambda se ha resuelto mediante métodos predeterminados. En este artículo, analizaremos en profundidad la operación de datos masivos (operación masiva) en las colecciones de Java y desentrañaremos el misterio del papel más poderoso de lambda.
1.Acerca de JSR335
JSR es la abreviatura de Solicitudes de especificación de Java, que significa solicitud de especificación de Java. La principal mejora de la versión Java 8 es el proyecto Lambda (JSR 335), que tiene como objetivo hacer que Java sea más fácil de escribir código para procesadores multinúcleo. JSR 335 = expresión lambda + mejora de la interfaz (método predeterminado) + operación de datos por lotes. Junto con los dos artículos anteriores, hemos aprendido completamente el contenido relevante de JSR335.
2. Iteración externa versus interna
En el pasado, las colecciones de Java no podían expresar iteraciones internas, sino que solo proporcionaban una forma de iteración externa, es decir, bucles for o while.
Copie el código de código de la siguiente manera:
Listar personas = asList(nueva Persona("Joe"), nueva Persona("Jim"), nueva Persona("John"));
para (Persona p: personas) {
p.setLastName("Doe");
}
El ejemplo anterior es nuestro método anterior, que es la llamada iteración externa. El bucle es un bucle de secuencia fija. En la era actual de múltiples núcleos, si queremos realizar un bucle en paralelo, debemos modificar el código anterior. Aún no se sabe cuánto se puede mejorar la eficiencia y traerá ciertos riesgos (problemas de seguridad de subprocesos, etc.).
Para describir la iteración interna, necesitamos usar una biblioteca de clases como Lambda. Reescribamos el bucle anterior usando lambda y Collection.forEach.
Copie el código de la siguiente manera: people.forEach(p->p.setLastName("Doe"));
Ahora la biblioteca jdk controla el bucle. No necesitamos preocuparnos de cómo se configura el apellido de cada objeto de persona. La biblioteca puede decidir cómo hacerlo según el entorno de ejecución, en paralelo, fuera de orden o de forma diferida. cargando. Esta es una iteración interna y el cliente pasa el comportamiento p.setLastName como datos a la API.
De hecho, la iteración interna no está estrechamente relacionada con la operación por lotes de colecciones. Con su ayuda, podemos sentir los cambios en la expresión gramatical. Lo realmente interesante relacionado con las operaciones por lotes es la nueva API de transmisión. El nuevo paquete java.util.stream se agregó a JDK 8.
3.Transmitir API
Stream solo representa un flujo de datos y no tiene estructura de datos, por lo que ya no se puede atravesar después de haberlo recorrido una vez (se debe prestar atención a esto al programar, a diferencia de Collection, todavía hay datos en él sin importar cuántas veces se atraviesa). La fuente puede ser Colección, matriz, io, etc.
3.1 Métodos intermedios y de punto final
Streaming proporciona una interfaz para operar big data, haciendo que las operaciones de datos sean más fáciles y rápidas. Tiene métodos como filtrado, mapeo y reducción del número de recorridos. Estos métodos se dividen en dos tipos: métodos intermedios y métodos terminales. La abstracción de "flujo" debe ser continua por naturaleza. Los métodos intermedios siempre devuelven un flujo. queremos obtener el resultado final. Si es así, se deben utilizar operaciones de punto final para recopilar los resultados finales producidos por la secuencia. La diferencia entre estos dos métodos es observar su valor de retorno. Si es un Stream, es un método intermedio; de lo contrario, es un método final. Consulte la API de Stream para obtener más detalles.
Presente brevemente varios métodos intermedios (filtro, mapa) y métodos de punto final (recopilación, suma)
3.1.1Filtro
Implementar funciones de filtrado en flujos de datos es la operación más natural que se nos ocurre. La interfaz Stream expone un método de filtro, que acepta una implementación de Predicado que representa una operación para usar una expresión lambda que define las condiciones del filtro.
Copie el código de código de la siguiente manera:
Listar personas =…
Stream peopleOver18 = people.stream().filter(p -> p.getAge() > 18);//Filtrar personas mayores de 18 años
3.1.2Mapa
Supongamos que filtramos algunos datos ahora, como al convertir objetos. La operación Mapa nos permite ejecutar una implementación de una Función (las T y R genéricas de Función<T, R> representan la entrada de ejecución y los resultados de la ejecución respectivamente), que acepta parámetros de entrada y los devuelve. Primero, veamos cómo describirla como una clase interna anónima:
Copie el código de código de la siguiente manera:
Corriente adulto = personas
.arroyo()
.filtro(p -> p.getAge() > 18)
.map(nueva función() {
@Anular
público Adulto aplicar(Persona persona) {
return new Adulto(persona);//Convertir una persona mayor de 18 años en adulto
}
});
Ahora, convierta el ejemplo anterior en una expresión lambda:
Copie el código de código de la siguiente manera:
Mapa de corriente = personas.corriente()
.filtro(p -> p.getAge() > 18)
.map(persona -> nuevo Adulto(persona));
3.1.3Contar
El método de conteo es el método de punto final de una transmisión, que puede generar las estadísticas finales de los resultados de la transmisión y devolver un int. Por ejemplo, calculemos el número total de personas de 18 años o más:
Copie el código de código de la siguiente manera:
int recuentoDeAdult=personas.stream()
.filtro(p -> p.getAge() > 18)
.map(persona -> nuevo Adulto(persona))
.contar();
3.1.4 Recoger
El método de recopilación también es un método de punto final de una secuencia, que puede recopilar los resultados finales.
Copie el código de código de la siguiente manera:
Lista ListaAdultos= personas.stream()
.filtro(p -> p.getAge() > 18)
.map(persona -> nuevo Adulto(persona))
.collect(Collectors.toList());
O si queremos utilizar una clase de implementación específica para recopilar los resultados:
Copie el código de código de la siguiente manera:
Lista lista de adultos = personas
.arroyo()
.filtro(p -> p.getAge() > 18)
.map(persona -> nuevo Adulto(persona))
.collect(Collectors.toCollection(ArrayList::new));
Debido al espacio limitado, no se presentarán otros métodos intermedios y métodos de punto final uno por uno. Después de leer los ejemplos anteriores, solo necesita comprender la diferencia entre estos dos métodos y podrá decidir utilizarlos según sus necesidades. más tarde.
3.2 Flujo secuencial y flujo paralelo
Cada Stream tiene dos modos: ejecución secuencial y ejecución paralela.
Flujo de secuencia:
Copie el código de código de la siguiente manera:
Lista <Persona> personas = list.getStream.collect(Collectors.toList());
Corrientes paralelas:
Copie el código de código de la siguiente manera:
Lista <Persona> personas = list.getStream.parallel().collect(Collectors.toList());
Como su nombre lo indica, cuando se utiliza el método secuencial para recorrer, cada elemento se lee antes de leer el siguiente. Cuando se utiliza el recorrido paralelo, la matriz se dividirá en varios segmentos, cada uno de los cuales se procesará en un subproceso diferente y luego los resultados se generarán juntos.
3.2.1 Principio de flujo paralelo:
Copie el código de código de la siguiente manera:
Lista lista original = algunosdatos;
split1 = originalList(0, mid);//Dividimos los datos en partes pequeñas
split2 = lista original(medio,fin);
new Runnable(split1.process());//Ejecutar operaciones en partes pequeñas
nuevo ejecutable(split2.process());
Lista revisadaLista = split1 + split2;//Fusionar los resultados
3.2.2 Comparación de pruebas de desempeño secuenciales y paralelas
Si se trata de una máquina de múltiples núcleos, en teoría el flujo paralelo será dos veces más rápido que el flujo secuencial. El siguiente es el código de prueba.
Copie el código de código de la siguiente manera:
largo t0 = System.nanoTime();
//Inicializa una secuencia de enteros con un rango de 1 millón y encuentra un número que pueda ser divisible por 2. toArray() es el método de punto final
int a[]=IntStream.range(0, 1_000_000).filter(p -> p % 2==0).toArray();
largo t1 = System.nanoTime();
//La misma función que arriba, aquí usamos flujo paralelo para calcular
int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray();
largo t2 = System.nanoTime();
// Los resultados de mi máquina local son en serie: 0,06 s, paralelo 0,02 s, lo que demuestra que el flujo paralelo es de hecho más rápido que el flujo secuencial.
System.out.printf("serie: %.2fs, paralelo %.2fs%n", (t1 - t0) * 1e-9, (t2 - t1) * 1e-9);
3.3 Acerca del marco Folk/Join
El paralelismo de hardware de aplicaciones está disponible en Java 7. Una de las nuevas características del paquete java.util.concurrent es un marco de descomposición paralela estilo bifurcación. También es muy poderoso y eficiente. entra en detalles aquí. En comparación con Stream.parallel(), prefiero este último.
4. Resumen
Sin lambda, Stream es bastante incómodo de usar. Generará una gran cantidad de clases internas anónimas, como en el ejemplo 3.1.2map anterior. Si no hay un método predeterminado, los cambios en el marco de la colección inevitablemente causarán muchos cambios. entonces el método lambda+default hace que la biblioteca jdk sea más poderosa y flexible, las mejoras de Stream y el marco de colección son la mejor prueba.