¿Para quién es este artículo?
- Para aquellos que creen que ya conocen bien Java Core, pero no tienen idea de las expresiones lambda en Java. O quizás ya hayas oído algo sobre lambdas, pero sin detalles.
- para aquellos que tienen algún conocimiento de las expresiones lambda, pero todavía tienen miedo y son inusuales al usarlas.
Si no pertenece a ninguna de estas categorías, es posible que este artículo le resulte aburrido, incorrecto y, en general, "no interesante". En este caso, no dude en pasar por alto o, si conoce bien el tema, sugiera en los comentarios cómo podría mejorar o complementar el artículo. El material no pretende ningún valor académico, y mucho menos novedad. Más bien, al contrario: en él intentaré describir cosas complejas (para algunos) de la forma más sencilla posible. Me inspiré para escribir en una solicitud para explicar la API de transmisión. Lo pensé y decidí que sin entender las expresiones lambda, algunos de mis ejemplos sobre "corrientes" serían incomprensibles. Entonces comencemos con lambdas.
Qué conocimientos se requieren para comprender este artículo:
- Comprensión de la programación orientada a objetos (en lo sucesivo, POO), a saber:
- conocimiento de qué son clases y objetos, cuál es la diferencia entre ellos;
- conocimiento de qué son las interfaces, en qué se diferencian de las clases, cuál es la conexión entre ellas (interfaces y clases);
- conocimiento de qué es un método, cómo llamarlo, qué es un método abstracto (o un método sin implementación), cuáles son los parámetros/argumentos de un método, cómo pasarlos allí;
- modificadores de acceso, métodos/variables estáticos, métodos/variables finales;
- herencia (clases, interfaces, herencia múltiple de interfaces).
- Conocimientos de Java Core: genéricos, colecciones (listas), threads.
Bueno, comencemos.
Una pequeña historia
Las expresiones lambda llegaron a Java desde la programación funcional y allí desde las matemáticas. A mediados del siglo XX en América trabajaba en la Universidad de Princeton un tal Alonzo Church, muy aficionado a las matemáticas y todo tipo de abstracciones. Fue Alonzo Church a quien se le ocurrió el cálculo lambda, que al principio era un conjunto de algunas ideas abstractas y no tenía nada que ver con la programación. Al mismo tiempo, en la misma Universidad de Princeton trabajaban matemáticos como Alan Turing y John von Neumann. Todo salió bien: a Church se le ocurrió el sistema de cálculo lambda, Turing desarrolló su máquina informática abstracta, ahora conocida como la “máquina de Turing”. Bueno, von Neumann propuso un diagrama de la arquitectura de las computadoras, que formó la base de las computadoras modernas (y ahora se llama "arquitectura de von Neumann"). En ese momento, las ideas de Alonzo Church no ganaron tanta fama como el trabajo de sus colegas (con excepción del campo de las matemáticas “puras”). Sin embargo, un poco más tarde, un tal John McCarthy (también graduado de la Universidad de Princeton, en el momento de la historia, empleado del Instituto Tecnológico de Massachusetts) se interesó por las ideas de Church. A partir de ellos, en 1958 creó el primer lenguaje de programación funcional, Lisp. Y 58 años después, las ideas de programación funcional se filtraron en Java como el número 8. No han pasado ni 70 años... De hecho, este no es el período de tiempo más largo para aplicar una idea matemática en la práctica.
La esencia
Una expresión lambda es una de esas funciones. Puedes considerarlo como un método normal en Java, la única diferencia es que se puede pasar a otros métodos como argumento. Sí, ahora es posible pasar no sólo números, cadenas y gatos a métodos, ¡sino también otros métodos! ¿Cuándo podríamos necesitar esto? Por ejemplo, si queremos pasar alguna devolución de llamada. Necesitamos que el método que llamamos pueda llamar a algún otro método que le pasemos. Es decir, para que tengamos la oportunidad de transmitir una devolución de llamada en unos casos y otra en otros. Y nuestro método, que aceptaría nuestras devoluciones de llamada, las llamaría. Un ejemplo sencillo es la clasificación. Digamos que escribimos algún tipo de clasificación complicada que se parece a esta:
public void mySuperSort() {
if(compare(obj1, obj2) > 0)
}
Donde
if
llamamos al método
compare()
, pasamos dos objetos que comparamos y queremos saber cuál de estos objetos es "mayor". Pondremos el que es “más” antes del que es “menor”. Escribí “más” entre comillas porque estamos escribiendo un método universal que podrá ordenar no solo en orden ascendente sino también descendente (en este caso, “más” será el objeto que es esencialmente más pequeño, y viceversa) . Para establecer la regla de cómo queremos ordenar exactamente, debemos pasarla de alguna manera a nuestro archivo
mySuperSort()
. En este caso, podremos “controlar” de alguna manera nuestro método mientras se llama. Por supuesto, puede escribir dos métodos separados
mySuperSortAsc()
para
mySuperSortDesc()
ordenar en orden ascendente y descendente. O pase algún parámetro dentro del método (por ejemplo,
boolean
if
true
, ordene en orden ascendente y if
false
en orden descendente). Pero, ¿qué pasa si no queremos ordenar una estructura simple, sino, por ejemplo, una lista de matrices de cadenas? ¿ Cómo
mySuperSort()
sabrá nuestro método cómo ordenar estas matrices de cadenas? ¿Medir? ¿Por longitud total de palabras? ¿Quizás alfabéticamente, dependiendo de la primera fila de la matriz? Pero, ¿qué pasa si, en algunos casos, necesitamos ordenar una lista de matrices por el tamaño de la matriz y, en otro caso, por la longitud total de las palabras de la matriz? Creo que ya has oído hablar de los comparadores y que en tales casos simplemente pasamos un objeto comparador a nuestro método de clasificación, en el que describimos las reglas por las cuales queremos ordenar. Dado que el método estándar
sort()
se implementa según el mismo principio que ,
mySuperSort()
en los ejemplos usaré el estándar
sort()
.
String[] array1 = {"Madre", "jabón", "marco"};
String[] array2 = {"I", "Muy", "Amo", "java"};
String[] array3 = {"mundo", "trabajar", "Puede"};
List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);
Comparator<String[]> sortByLength = new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
};
Comparator<String[]> sortByWordsLength = new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
int length1 = 0;
int length2 = 0;
for (String s : o1) {
length1 += s.length();
}
for (String s : o2) {
length2 += s.length();
}
return length1 - length2;
}
};
arrays.sort(sortByLength);
Resultado:
- mamá lavó el marco
- paz laborista puede
- Realmente amo java
Aquí las matrices están ordenadas por la cantidad de palabras en cada matriz. Una matriz con menos palabras se considera "más pequeña". Por eso viene al principio. Aquel en el que hay más palabras se considera “más” y acaba al final. Si
sort()
le pasamos otro comparador al método
(sortByWordsLength)
, entonces
el resultado será diferente:
- paz laborista puede
- mamá lavó el marco
- Realmente amo java
Ahora las matrices se ordenan por el número total de letras de las palabras de dicha matriz. En el primer caso hay 10 letras, en el segundo 12 y en el tercero 15. Si usamos solo un comparador, entonces no podemos crear una variable separada para él, sino simplemente crear un objeto de una clase anónima justo en el momento de llamar al método
sort()
. Como eso:
String[] array1 = {"Madre", "jabón", "marco"};
String[] array2 = {"I", "Muy", "Amo", "java"};
String[] array3 = {"mundo", "trabajar", "Puede"};
List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);
arrays.sort(new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
});
El resultado será el mismo que en el primer caso. Tarea 1 . Vuelva a escribir este ejemplo para que ordene las matrices no en orden ascendente según el número de palabras en la matriz, sino en orden descendente. Todo esto ya lo sabemos. Sabemos cómo pasar objetos a métodos, podemos pasar tal o cual objeto a un método dependiendo de lo que necesitemos en ese momento, y dentro del método donde pasamos dicho objeto, se llamará el método para el cual escribimos la implementación. . Surge la pregunta: ¿qué tienen que ver las expresiones lambda con esto?
Dado que una lambda es un objeto que contiene exactamente un método. Es como un objeto de método. Un método envuelto en un objeto. Simplemente tienen una sintaxis ligeramente inusual (pero hablaremos de eso más adelante). Echemos otro vistazo a esta entrada.
arrays.sort(new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
});
Aquí tomamos nuestra lista
arrays
y llamamos a su método
sort()
, donde pasamos un objeto comparador con un solo método
compare()
(no nos importa cómo se llame, porque es el único en este objeto, no lo perderemos). Este método toma dos parámetros, con los que trabajaremos a continuación. Si trabajas en
IntelliJ IDEA , probablemente hayas visto cómo te ofrece este código para acortar significativamente:
arrays.sort((o1, o2) -> o1.length - o2.length);
Así fue como seis líneas se convirtieron en una corta. Se reescribieron 6 líneas en una corta. Algo ha desaparecido, pero te garantizo que no ha desaparecido nada importante y este código funcionará exactamente igual que con una clase anónima.
Tarea 2 . Descubra cómo reescribir la solución al problema 1 usando lambdas (como último recurso, solicite a
IntelliJ IDEA que convierta su clase anónima en una lambda).
Hablemos de interfaces
Básicamente, una interfaz es sólo una lista de métodos abstractos. Cuando creamos una clase y decimos que implementará algún tipo de interfaz, debemos escribir en nuestra clase una implementación de los métodos que se enumeran en la interfaz (o, como último recurso, no escribirla, sino hacer que la clase sea abstracta ). Hay interfaces con muchos métodos diferentes (por ejemplo
List
), hay interfaces con un solo método (por ejemplo, el mismo Comparador o Runnable). Hay interfaces que no tienen ningún método (las llamadas interfaces de marcador, por ejemplo Serializable). Aquellas interfaces que tienen un solo método también se denominan
interfaces funcionales . En Java 8 incluso están marcados con una anotación especial
@FunctionalInterface . Son las interfaces con un solo método las que son adecuadas para su uso con expresiones lambda. Como dije anteriormente, una expresión lambda es un método envuelto en un objeto. Y cuando pasamos un objeto así a alguna parte, de hecho, pasamos este único método. Resulta que no nos importa cómo se llame este método. Lo único que nos importa son los parámetros que toma este método y, de hecho, el código del método en sí. Una expresión lambda es, esencialmente. Implementación de una interfaz funcional. Cuando vemos una interfaz con un método, significa que podemos reescribir dicha clase anónima usando una lambda. Si la interfaz tiene más/menos de un método, entonces la expresión lambda no nos conviene y usaremos una clase anónima, o incluso una normal. Es hora de profundizar en las lambdas. :)
Sintaxis
La sintaxis general es algo como esto:
(параметры) -> {тело метода}
Es decir, paréntesis, dentro de ellos están los parámetros del método, una "flecha" (estos son dos caracteres seguidos: menos y mayor), después de lo cual el cuerpo del método está entre llaves, como siempre. Los parámetros corresponden a los especificados en la interfaz al describir el método. Si el compilador puede definir claramente el tipo de variables (en nuestro caso, se sabe con certeza que estamos trabajando con matrices de cadenas, porque se
List
escribe precisamente mediante matrices de cadenas), entonces el tipo de variables
String[]
no necesita ser escrito.
Si no está seguro, especifique el tipo y IDEA lo resaltará en gris si no es necesario. |
Puedes leer más en
el tutorial de Oracle , por ejemplo. Esto se llama
"escritura de destino" . Puede dar cualquier nombre a las variables, no necesariamente los especificados en la interfaz. Si no hay parámetros, solo paréntesis. Si solo hay un parámetro, solo el nombre de la variable sin paréntesis. Hemos ordenado los parámetros, ahora sobre el cuerpo de la expresión lambda. Dentro de las llaves, escriba el código como para un método normal. Si todo su código consta de una sola línea, no es necesario escribir llaves en absoluto (como ocurre con los if y los bucles). Si su lambda devuelve algo, pero su cuerpo consta de una línea,
return
no es necesario escribirlo en absoluto. Pero si tiene llaves, entonces, como en el método habitual, debe escribir explícitamente
return
.
Ejemplos
Ejemplo 1.
() -> {}
La opción más sencilla. Y el que menos sentido tiene :) Porque no hace nada.
Ejemplo 2.
() -> ""
También una opción interesante. No acepta nada y devuelve una cadena vacía (
return
se omite por ser innecesaria). Lo mismo pero con
return
:
() -> {
return "";
}
Ejemplo 3. Hola mundo usando lambdas
() -> System.out.println("Hello world!")
No recibe nada, no devuelve nada (no podemos ponerlo
return
antes de la llamada
System.out.println()
, ya que el tipo de retorno en el método
println() — void)
, simplemente muestra una inscripción en la pantalla. Ideal para implementar una interfaz
Runnable
. El mismo ejemplo es más completo:
public class Main {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello world!")).start();
}
}
O así:
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> System.out.println("Hello world!"));
t.start();
}
}
O incluso podemos guardar la expresión lambda como un objeto de tipo
Runnable
y luego pasarla al constructor
thread’а
:
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> System.out.println("Hello world!");
Thread t = new Thread(runnable);
t.start();
}
}
Echemos un vistazo más de cerca al momento de guardar una expresión lambda en una variable. La interfaz
Runnable
nos dice que sus objetos deben tener un método
public void run()
. Según la interfaz, el método de ejecución no acepta nada como parámetro. Y no devuelve nada
(void)
. Por lo tanto al escribir de esta manera se creará un objeto con algún método que no acepta ni devuelve nada. Lo cual es bastante consistente con el método
run()
en la interfaz
Runnable
. Es por eso que pudimos poner esta expresión lambda en una variable como
Runnable
.
Ejemplo 4
() -> 42
Nuevamente, no acepta nada, pero devuelve el número 42. Esta expresión lambda se puede colocar en una variable de tipo
Callable
, porque esta interfaz solo define un método, que se parece a esto:
V call(),
donde
V
está el tipo de valor de retorno (en nuestro caso
int
). En consecuencia, podemos almacenar dicha expresión lambda de la siguiente manera:
Callable<Integer> c = () -> 42;
Ejemplo 5. Lambda en varias líneas
() -> {
String[] helloWorld = {"Hello", "world!"};
System.out.println(helloWorld[0]);
System.out.println(helloWorld[1]);
}
Nuevamente, esta es una expresión lambda sin parámetros y su tipo de retorno
void
(ya que no hay
return
).
Ejemplo 6
x -> x
Aquí tomamos algo en una variable
х
y lo devolvemos. Tenga en cuenta que si solo se acepta un parámetro, no es necesario escribir los paréntesis que lo rodean. Lo mismo, pero entre paréntesis:
(x) -> x
Y aquí está la opción con una explícita
return
:
x -> {
return x;
}
O así, entre paréntesis y
return
:
(x) -> {
return x;
}
O con una indicación explícita del tipo (y, en consecuencia, entre paréntesis):
(int x) -> x
Ejemplo 7
x -> ++x
Lo aceptamos
х
y lo devolvemos, pero por
1
más. También puedes reescribirlo así:
x -> x + 1
En ambos casos, no indicamos paréntesis alrededor del parámetro, cuerpo del método y palabra
return
, ya que esto no es necesario. Las opciones entre paréntesis y retorno se describen en el ejemplo 6.
Ejemplo 8
(x, y) -> x % y
Aceptamos una parte
х
y
у
devolvemos el resto de la división
x
por
y
. Aquí ya se requieren paréntesis alrededor de los parámetros. Son opcionales sólo cuando hay un solo parámetro. Así con indicación explícita de tipos:
(double x, int y) -> x % y
Ejemplo 9
(Cat cat, String name, int age) -> {
cat.setName(name);
cat.setAge(age);
}
Aceptamos un objeto Cat, una cadena con un nombre y una edad entera. En el método en sí, establecemos el nombre y la edad pasados en Cat.
cat
Dado que nuestra variable es un tipo de referencia, el objeto Cat fuera de la expresión lambda cambiará (recibirá el nombre y la edad pasados dentro). Una versión un poco más complicada que utiliza una lambda similar:
public class Main {
public static void main(String[] args) {
Cat myCat = new Cat();
System.out.println(myCat);
Settable<Cat> s = (obj, name, age) -> {
obj.setName(name);
obj.setAge(age);
};
changeEntity(myCat, s);
System.out.println(myCat);
}
private static <T extends WithNameAndAge> void changeEntity(T entity, Settable<T> s) {
s.set(entity, "Murzik", 3);
}
}
interface WithNameAndAge {
void setName(String name);
void setAge(int age);
}
interface Settable<C extends WithNameAndAge> {
void set(C entity, String name, int age);
}
class Cat implements WithNameAndAge {
private String name;
private int age;
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Resultado: Cat{name='null', age=0} Cat{name='Murzik', age=3} Como puede ver, al principio el objeto Cat tenía un estado, pero después de usar la expresión lambda, el estado cambió . Las expresiones lambda funcionan bien con los genéricos. Y si necesitamos crear una clase
Dog
, por ejemplo, que también implementará
WithNameAndAge
, entonces en el método
main()
podemos hacer las mismas operaciones con Dog, sin cambiar la expresión lambda en absoluto.
Tarea 3 . Escriba una interfaz funcional con un método que tome un número y devuelva un valor booleano. Escriba una implementación de dicha interfaz en forma de expresión lambda que devuelva
true
si el número pasado es divisible por 13 sin resto.Tarea
4 . Escriba una interfaz funcional con un método que tome dos cadenas y devuelva la misma cadena. Escriba una implementación de dicha interfaz en forma de lambda que devuelva la cadena más larga.
Tarea 5 . Escriba una interfaz funcional con un método que acepte tres números fraccionarios:
a
,
b
y
c
devuelva el mismo número fraccionario. Escriba una implementación de dicha interfaz en forma de expresión lambda que devuelva un discriminante. ¿Quién lo olvidó?
D = b^2 - 4ac .
Tarea 6 . Usando la interfaz funcional de la tarea 5, escriba una expresión lambda que devuelva el resultado de la operación
a * b^c
.
Popular sobre las expresiones lambda en Java. Con ejemplos y tareas. Parte 2.
GO TO FULL VERSION