Introducción a Lambda
Las expresiones Lambda son una nueva característica importante en Java SE 8. Las expresiones Lambda le permiten reemplazar las interfaces funcionales por expresiones. La expresión de Lambda es al igual que el método, que proporciona una lista de parámetros normal y un cuerpo (cuerpo, que puede ser una expresión o un bloque de código) que usa estos parámetros.
Las expresiones Lambda también mejoran la biblioteca de colección. Java SE 8 agrega 2 paquetes que operan operaciones por lotes en los datos de recopilación: java.util.function Package y java.util.stream Package. Una transmisión es como un iterador, pero con muchas características adicionales adjuntas. En general, las expresiones y transmisiones de Lambda son los mayores cambios ya que el lenguaje Java agrega genéricos y anotaciones.
Las expresiones lambda son esencialmente métodos anónimos, y su capa subyacente se implementa a través de directivas invokedynamic para generar clases anónimas. Proporciona una sintaxis más simple y un método de escritura, lo que le permite reemplazar las interfaces funcionales con expresiones. A los ojos de algunas personas, Lambda puede hacer que su código sea más conciso y no usarlo en absoluto; esta opinión está bien, pero lo importante es que Lambda trae cierres a Java. Gracias al soporte de Lamdba para las colecciones, Lambda ha mejorado enormemente el rendimiento al atravesar colecciones en condiciones de procesador de múltiples núcleos. Además, podemos procesar colecciones en forma de flujos de datos, lo cual es muy atractivo.
Sintaxis lambda
La sintaxis de Lambda es extremadamente simple, similar a la siguiente estructura:
(parámetros) -> expresión
o
(parámetros) -> {declaraciones; }Las expresiones lambda están compuestas por tres partes:
1. Paramás: una lista de parámetros formales en métodos similares, los parámetros aquí son parámetros en la interfaz funcional. Los tipos de parámetros aquí pueden ser declarados o no declarados explícitamente pero implícitamente inferidos por el JVM. Además, cuando solo hay un tipo de inferencia, se pueden omitir paréntesis.
2. ->: Se puede entender como "ser utilizado"
3. Método Cuerpo: puede ser una expresión o un bloque de código, es la implementación del método en la interfaz funcional. Un bloque de código puede devolver un valor o reversar nada. El bloque de código aquí es equivalente al cuerpo del método del método. Si es una expresión, también puede devolver un valor o no devolver nada.
Usemos los siguientes ejemplos para ilustrar:
// Ejemplo 1: No es necesario aceptar parámetros, devolver directamente 10 ()-> 10 // Ejemplo 2: Acepte dos parámetros de tipo int y devuelve la suma de estos dos parámetros (int x, int y)-> x+y; // Ejemplo 2: Aceptar dos parámetros de x e y, el tipo de este parámetro es inferido por el JVM basado en el contexto, y devuelve la suma de los dos parámetros (x, y). Accept a string and print the string to control, without reverse the result (String name)->System.out.println(name);//Example 4: Accept an inferred type parameter name and print the string to console name->System.out.println(name);//Example 5: Accept two String type parameters and output them separately, without reverse the result (String name,String name,String Sex)-> {System.out.println (nombre); System.out.println (Sex)} // Ejemplo 6: Acepte un parámetro x y devuelva el doble del parámetro x-> 2*xDonde usar lambda
En [interfaz funcional] [1] sabemos que el tipo de expresión de Lambda de destino es una interfaz funcional: cada lambda puede coincidir con un tipo dado a través de una interfaz funcional específica. Por lo tanto, se puede aplicar una expresión de Lambda en cualquier lugar que coincida con su tipo de objetivo. La expresión de Lambda debe tener el mismo tipo de parámetro que la descripción de la función abstracta de la interfaz funcional, su tipo de retorno también debe ser compatible con el tipo de retorno de la función abstracta, y las excepciones que puede lanzar se limitan al rango de descripción de la función.
A continuación, veamos un ejemplo de interfaz funcional personalizado:
@FunctionalInterface Interface Converter <f, t> {t convert (f from);}Primero, use la interfaz de la manera tradicional:
Converter <String, Integer> Converter = New Converter <String, Integer> () {@Override public Integer Convert (String From) {return Integer.ValueOf (desde); }}; Resultado entero = convertidor.convert ("200"); System.out.println (resultado);Obviamente, no hay problema con esto, por lo que lo siguiente es el momento en que Lambda entra en el campo, usando Lambda para implementar la interfaz convertidor:
Convertidor <String, Integer> Converter = (Param) -> Integer.valueOf (param); Resultado entero = convertidor.convert ("101"); System.out.println (resultado);A través del ejemplo anterior, creo que tiene una simple comprensión del uso de Lambda. A continuación, estamos utilizando un Runnable comúnmente usado para demostrar:
En el pasado podríamos haber escrito este código:
new Thread (new Runnable () {@Override public void run () {System.out.println ("Hello Lambda");}}). Start ();En algunos casos, una gran cantidad de clases anónimas pueden hacer que el código parezca desordenado. Ahora puedes usar Lambda para simplificarlo:
new Thread (() -> System.out.println ("Hello Lambda")). Start ();Referencia de método
La referencia del método es una forma simplificada de escribir expresiones Lambda. El método referenciado es en realidad una implementación del cuerpo del método de la expresión de lambda, y su estructura de sintaxis es:
Objectref :: MethodName
El lado izquierdo puede ser el nombre de clase o el nombre de la instancia, el medio es el símbolo de referencia del método "::", y el lado derecho es el nombre del método correspondiente.
Las referencias del método se dividen en tres categorías:
1. Referencia del método estático
En algunos casos, podríamos escribir código como este:
public class referenceTest {public static void main (string [] args) {convertidor <string, integer> converter = new Converter <String, Integer> () {@Override public Integer Convert (String From) {return referenceTest.String2Int (desde); }}; convertidor.convert ("120"); } @FunctionalInterface Interface Converter <f, t> {t convert (f from); } static int string2Int (string from) {return integer.valueOf (desde); }}En este momento, si usa referencias estáticas, el código será más conciso:
Convertidor <string, integer> converter = referenceTest :: string2Int; convertidor.convert ("120");2. Referencia de método de instancia
También podríamos escribir código como este:
public class referenceTest {public static void main (string [] args) {convertidor <string, integer> converter = new Converter <String, Integer> () {@Override Public Integer Convert (String From) {return New Helper (). String2Int (desde); }}; convertidor.convert ("120"); } @FunctionalInterface Interface Converter <f, t> {t convert (f from); } static class aelper {public int String2Int (string from) {return integer.valueOf (from); }}}Además, el uso de métodos de ejemplo para hacer referencia aparecerá más conciso:
Helper Helper = New Helper (); Convertidor <String, Integer> Converter = Helper :: String2Int; convertidor.convert ("120");3. Referencia del método del constructor
Ahora demostremos referencias a constructores. Primero definimos un animal de clase padre:
Clase Animal {Nombre de cadena privada; edad privada int; Public Animal (nombre de cadena, int Age) {this.name = name; this.age = edad; } Public void comportamiento () {}} A continuación, estamos definiendo dos subclases de animales: Dog、Bird
Public Class Bird extiende Animal {public Bird (Nombre de cadena, int Age) {super (nombre, edad); } @Override public void comportamiento () {system.out.println ("volar"); }} El perro de clase extiende animal {perro público (nombre de cadena, int ave) {super (nombre, edad); } @Override public void comportamiento () {system.out.println ("run"); }}Luego definimos la interfaz de fábrica:
interfaz fábrica <t extiende animal> {t create (name de cadena, int age); }A continuación, utilizaremos el método tradicional para crear objetos de clases de perros y aves:
Fábrica fábrica = new Factory () {@Override public animal create (nombre de cadena, int age) {return new dog (nombre, edad); }}; fábrica.create ("alias", 3); fábrica = new Factory () {@Override public animal create (nombre de cadena, int age) {return new Bird (nombre, edad); }}; fábrica.create ("Smook", 2);Escribí más de diez códigos solo para crear dos objetos. Ahora intentemos usar la referencia del constructor:
Factory <imeny> dogFactory = perro :: nuevo; Perro animal = dogfactory.create ("alias", 4); Factory <Dird> BirdFactory = Bird :: new; Bird Bird = BirdFactory.Create ("Smook", 3); De esta manera, el código parece limpio y ordenado. Cuando use Dog::new para penetrar los objetos, seleccione la función de creación correspondiente firmando la función Factory.create .
Restricciones de dominio y acceso de Lambda
El dominio es el alcance, y los parámetros en la lista de parámetros en la expresión de lambda son válidos dentro del alcance de la expresión de lambda (dominio). En la expresión de Lambda, se puede acceder a variables externas: variables locales, variables de clase y variables estáticas, pero el grado de limitaciones de operación es diferente.
Acceder a variables locales
Las variables locales fuera de la expresión de Lambda serán compiladas implícitamente por el tipo JVM al tipo final, por lo que solo se puede acceder pero no modificarse.
clase pública referenceTest {public static void main (string [] args) {int n = 3; Calcular calculación = param -> {// n = 10; Compilar el error return n + param; }; calcular.calcule (10); } @FunctionalInterface Interface Calcule {int Calculate (int value); }}Acceder a las variables estáticas y miembros
Dentro de las expresiones Lambda, las variables estáticas y miembros son legibles y escritas.
clase pública referenceTest {public int count = 1; Public static int num = 2; public void test () {calcule calcule = param -> {num = 10; // modificar el recuento de variables estáticas = 3; // modificar el retorno variable del miembro n + param; }; calcular.calcule (10); } public static void main (string [] args) {} @FunctionalInterface Interface Calcule {int calculado (int value); }}Lambda no puede acceder al método predeterminado de interfaz de función
Java8 mejora las interfaces, incluidos los métodos predeterminados que pueden agregar definiciones de palabras clave predeterminadas a las interfaces. Debemos tener en cuenta aquí que el acceso a los métodos predeterminados no admite internamente.
Práctica de lambda
En la sección [Interfaz funcional] [2], mencionamos que muchas interfaces funcionales están integradas en el paquete java.util.function , y ahora explicaremos las interfaces funcionales comúnmente utilizadas.
Interfaz predicado
Ingrese un parámetro y devuelva un valor Boolean , que contiene muchos métodos predeterminados para el juicio lógico:
@Test public void predictTest () {predicate <string> predict = (s) -> s.length ()> 0; test boolean = predic.test ("test"); System.out.println ("La longitud de la cadena es mayor que 0:" + prueba); test = predic.test (""); System.out.println ("La longitud de la cadena es mayor que 0:" + prueba); Predicado <ject> pre = Objects :: Nonnull; Objeto ob = nulo; test = pre.test (ob); System.out.println ("El objeto no está vacío:" + prueba); ob = nuevo objeto (); test = pre.test (ob); System.out.println ("El objeto no está vacío:" + prueba); }Interfaz de función
Reciba un parámetro y devuelve un solo resultado. El método predeterminado ( andThen ) puede unir múltiples funciones juntas para formar un resultado de Funtion compuesta (con entrada, salida).
@Test public void functionTest () {function <string, integer> toInteger = integer :: valueOf; // El resultado de ejecución de ToInteger se usa como entrada a la segunda función de backtostring <String, String> BackToString = ToInteger.andthen (String :: ValueOf); String result = backToString.apply ("1234"); System.out.println (resultado); Function <integer, integer> add = (i) -> {system.out.println ("Frist Entrada:" + i); regresar i * 2; }; Function <integer, integer> cero = add.andthen ((i) -> {System.out.println ("Segunda entrada:" + i); return i * 0;}); Entero res = cero.apply (8); System.out.println (res); }Interfaz de proveedor
Devuelve el resultado de un tipo dado. A diferencia de Function , Supplier no necesita aceptar parámetros (proveedor, con salida pero sin entrada)
@Test public void SupplierTest () {proveedor <String> Suministro = () -> "Valor de tipo especial"; Cadena s = proveedor.get (); System.out.println (s); }Interfaz de consumo
Representa las operaciones que deben realizarse en un solo parámetro de entrada. A diferencia de Function , Consumer no devuelve el valor (consumidor, entrada, sin salida)
@Test public void ConsumerTest () {Consumer <integer> add5 = (p) -> {System.out.println ("Valor antiguo:" + P); P = P + 5; System.out.println ("New Value:" + P); }; add5.accept (10); } El uso de las cuatro interfaces anteriores representa los cuatro tipos en el paquete java.util.function . Después de comprender estas cuatro interfaces funcionales, otras interfaces serán fáciles de entender. Ahora hagamos un resumen simple:
Predicate se usa para el juicio lógico, Function se usa en lugares donde hay entradas y salidas, Supplier se usa en lugares donde no hay entradas y salidas, y Consumer se usa en lugares donde hay entradas y no salidas. Puede conocer los escenarios de uso basados en el significado de su nombre.
Arroyo
Lambda trae cierres para Java 8, que es particularmente importante en las operaciones de recolección: Java 8 admite operaciones funcionales en la corriente de objetos de recolección. Además, la API de la corriente también se integra en la API de la colección, lo que permite operaciones por lotes en objetos de recolección.
Conozcamos a Stream.
Stream representa un flujo de datos. No tiene estructura de datos y no almacena elementos ellos mismos. Sus operaciones no cambiarán el flujo de origen, sino que generarán una nueva secuencia. Como interfaz para operar datos, proporciona filtrado, clasificación, mapeo y regulación. Estos métodos se dividen en dos categorías de acuerdo con el tipo de retorno: cualquier método que devuelva el tipo de secuencia se denomina método intermedio (operación intermedia), y el resto son métodos de finalización (operación completa). El método de finalización devuelve un valor de algún tipo, mientras que el método intermedio devuelve una nueva secuencia. La llamada de los métodos intermedios generalmente está encadenado, y el proceso formará una tubería. Cuando se llama al método final, hará que el valor se consuma inmediatamente desde la tubería. Aquí debemos recordar: las operaciones de transmisión se ejecutan lo más "retrasadas" como sea posible, que es lo que a menudo llamamos "operaciones perezosas", lo que ayudará a reducir el uso de recursos y mejorar el rendimiento. Para todas las operaciones intermedias (excepto las clasificadas) se ejecutan en modo de retraso.
Stream no solo proporciona potentes capacidades de operación de datos, sino que, lo que es más importante, Stream admite tanto en serie como paralelismo. El paralelismo permite que Stream tenga un mejor rendimiento en los procesadores de múltiples núcleos.
El proceso de uso de la transmisión tiene un patrón fijo:
1. Crea una transmisión
2. A través de operaciones intermedias, "cambiar" la transmisión original y generar una nueva transmisión
3. Use la operación de finalización para generar el resultado final
Eso es
Crear -> Cambiar -> Completar
Creación de la corriente
Para una colección, se puede crear llamando a la stream() o parallelStream() . Además, estos dos métodos también se implementan en la interfaz de recopilación. Para las matrices, se pueden crear mediante el método estático de Stream of(T … values) . Además, las matrices también proporcionan soporte para transmisiones.
Además de crear transmisiones basadas en colecciones o matrices anteriores, también puede crear una transmisión vacía a través de Steam.empty() , o usar Stream generate() para crear flujos infinitos.
Tomemos la transmisión en serie como un ejemplo para ilustrar varios métodos intermedios y de finalización de flujo de uso intermedio comúnmente utilizados. Primero cree una colección de listas:
List <String> lists = new ArrayList <String> (); lists.Add ("A1"); lists.Add ("A2"); lists.Add ("B1"); lists.Add ("B2"); lists.Add ("B3"); lists.Add ("O1");Método intermedio
Filtrar
Combinado con la interfaz de predicado, el filtro filtra todos los elementos en el objeto de transmisión. Esta operación es una operación intermedia, lo que significa que puede realizar otras operaciones en función del resultado devuelto por la operación.
public static void streamfilterTest () {lists.stream (). Filter ((S -> S.Startswith ("A"))). foreach (System.out :: println); // equivalente a la operación anterior predicada <string> predicate = (s) -> s.startswith ("a"); lists.stream (). Filter (predicado) .ForEach (System.out :: println); // predicado de filtrado continuo <String> predicate1 = (s -> s.endswith ("1")); lists.stream (). Filtro (predicado) .filter (predicate1) .forEach (System.out :: println); }Ordenar (ordenado)
Combinado con la interfaz de comparación, esta operación devuelve una vista de la transmisión ordenada, y el orden de la secuencia original no cambiará. Las reglas de recopilación se especifican a través del comparador, y el valor predeterminado es ordenarlas en orden natural.
public static void streamSortedTest () {System.out.println ("Comparador predeterminado"); lists.stream (). Sorted (). Filter ((S -> S.Startswith ("A"))). Foreach (System.out :: Println); System.out.println ("Comparador personalizado"); lists.stream (). Sorted ((P1, P2) -> P2.compareto (P1)). Filter ((S -> S.Startswith ("A"))). foreach (System.out :: println); }Mapa (mapa)
Combinado con la interfaz Function , esta operación puede asignar cada elemento en el objeto de transmisión en otro elemento, realizando la conversión de tipo de elemento.
public static void streamMaptest () {lists.stream (). map (string :: toupperCase) .sorted ((a, b) -> b.comppareto (a)). foreach (system.out :: println); System.out.println ("Reglas de mapeo personalizado"); Function <string, string> function = (p) -> {return p + ".txt"; }; lists.stream (). map (string :: toupperCase) .map (función) .sorted ((a, b) -> b.comppareto (a)). foreach (system.out :: println); }El anterior presenta brevemente tres operaciones de uso común, que simplifican en gran medida el procesamiento de la colección. A continuación, presentamos varias formas de completar:
Método de finalización
Después del proceso de "transformación", se debe obtener el resultado, es decir, la operación se completa. Veamos las operaciones relacionadas a continuación:
Fósforo
Se utiliza para determinar si un predicate coincide con el objeto de transmisión y finalmente devuelve un resultado de tipo Boolean , por ejemplo:
public static void streammatchTest () {// return true siempre que un elemento en el objeto de transmisión coincida con boolean anystartwitha = lists.stream (). Anymatch ((s -> s.startswith ("a"))); System.out.println (AnyStartwitha); // Devuelve verdadero cuando cada elemento en el objeto de transmisión coincide con boolean allstartwitha = lists.stream (). AllMatch ((s -> s.startswith ("a"))); System.out.println (AllStartwitha); }Recolectar
Después de la transformación, recolectamos los elementos de la corriente transformada, como guardar estos elementos en una colección. En este momento, podemos usar el método de recolección proporcionado por Stream, por ejemplo:
public static void streamCollectTest () {list <string> list = lists.stream (). Filter ((p) -> p.startswith ("A")). Sorted (). Collectors.tolist ()); System.out.println (lista); }Contar
El recuento similar a SQL se usa para contar el número total de elementos en la secuencia, por ejemplo:
public static void streamCountTest () {long count = lists.stream (). Filter ((S -> S.Startswith ("A"))). Count (); System.out.println (Count); }Reducir
reduce nos permite calcular elementos a nuestra manera o elementos asociados en una corriente con algún patrón, por ejemplo:
public static void streamreducetest () {opcional <string> opcional = lists.stream (). Sorted (). Reduce ((S1, S2) -> {System.out.println (S1 + "|" + S2); return S1 + "|" + S2;}); }Los resultados de la ejecución son los siguientes:
a1 | a2a1 | a2 | b1a1 | a2 | b1 | b2a1 | a2 | b1 | b2 | b3a1 | a2 | b1 | b2 | b3 | o1
Transmisión paralela vs transmisión en serie
Hasta ahora, hemos introducido las operaciones intermedias y completadas de uso común. Por supuesto, todos los ejemplos se basan en la transmisión en serie. A continuación, presentaremos el drama clave: la transmisión paralela (transmisión paralela). Paralelia se implementa en función del marco de descomposición paralelo de la bifurcación, y divide el conjunto de datos de big data en múltiples datos pequeños y lo entrega a diferentes hilos para el procesamiento. De esta manera, el rendimiento mejorará en gran medida en la situación del procesamiento de múltiples núcleos. Esto es consistente con el concepto de diseño de MapReduce: las tareas grandes se vuelven más pequeñas y las tareas pequeñas se reasignan a diferentes máquinas para la ejecución. Pero la pequeña tarea aquí se entrega a diferentes procesadores.
Cree una corriente paralela a través de parallelStream() . Para verificar si las transmisiones paralelas realmente pueden mejorar el rendimiento, ejecutamos el siguiente código de prueba:
Primero crea una colección más grande:
List <String> bigLists = new ArrayList <> (); para (int i = 0; i <10000000; i ++) {uuid uuid = uuid.randomuuid (); bigLists.add (uuid.ToString ()); }Pruebe el tiempo para ordenar en las transmisiones en serie:
Void estático privado NotParallelStreamSortedTest (List <String> BigLists) {Long StartTime = System.nanotime (); Long count = bigLists.stream (). Sorted (). Count (); Long Time = System.nanotime (); Long Millis = TimeUnit.nanoseConds.tomillis (ENDTime - Starttime); System.out.println (System.out.printf ("Sorteo de serie: %D MS", Millis)); }Pruebe el tiempo para ordenar en transmisiones paralelas:
Private static void parallelStreamSortedTest (List <String> bigLists) {long starttime = system.nanotime (); Long Count = BigLists.ParallelStream (). Sorted (). Count (); Long Time = System.nanotime (); Long Millis = TimeUnit.nanoseConds.tomillis (ENDTime - Starttime); System.out.println (System.out.printf ("Parallelsorting: %d MS", milis)); }Los resultados son los siguientes:
Sorteo en serie: 13336 MS
Sorteo paralelo: 6755 ms
Después de ver esto, encontramos que el rendimiento ha mejorado en aproximadamente un 50%. ¿También crees que podrás usar parallel Stream en el futuro? De hecho, no es el caso. Si todavía es un procesador de un solo núcleo y el volumen de datos no es grande, la transmisión en serie sigue siendo una buena opción. También encontrará que en algunos casos, el rendimiento de las transmisiones en serie es mejor. En cuanto al uso específico, debe probarlo primero y luego decidir de acuerdo con el escenario real.
Operación perezosa
Arriba hablamos sobre la corredera lo más tarde posible, y aquí lo explicamos creando un flujo infinito:
Primero, use el método generate transmisión para crear una secuencia de números naturales y luego transforme la transmisión a través map :
// La clase de secuencia incremental Natureseq implementa el proveedor <Along> {Long Value = 0; @Override public Long get () {valor ++; valor de retorno; }} public void streamCreateTest () {stream <long> stream = stream.generate (new Natureseq ()); System.out.println ("Número de elementos:"+stream.map ((param) -> {return param;}). Limit (1000) .count ()); }El resultado de la ejecución es:
Número de elementos: 1000
Descubrimos que al principio, las operaciones intermedias (como filter,map , etc., pero sorted se pueden hacer) están bien. Es decir, el proceso de realizar operaciones intermedias en la transmisión y sobrevivir a una nueva corriente no entra en efecto de inmediato (o la operación map en este ejemplo se ejecutará para siempre y se bloqueará), y la corriente comienza a calcular cuándo se encuentra el método de finalización. A través limit() , convierta esta corriente infinita en una corriente finita.
Resumir
Lo anterior es todo el contenido de la rápida introducción a Java Lambda. Después de leer este artículo, ¿tiene una comprensión más profunda de Java Lambda? Espero que este artículo sea útil para que todos aprendan Java Lambda.