JavaRush /Blog Java /Random-ES /Lambdas y referencias de métodos en ArrayList.forEach: có...

Lambdas y referencias de métodos en ArrayList.forEach: cómo funciona

Publicado en el grupo Random-ES
La introducción a las expresiones lambda en la búsqueda Java Syntax Zero comienza con un ejemplo muy específico:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hola", "Cómo", "дела?");

list.forEach( (s) -> System.out.println(s) );
Los autores de la conferencia analizan lambdas y referencias de métodos utilizando la función estándar forEach de la clase ArrayList. Personalmente, me resultó difícil entender el significado de lo que estaba sucediendo, ya que la implementación de esta función, así como la interfaz asociada a ella, permanece "bajo el capó". De dónde viene el (los) argumento(s) , de dónde se pasa la función println() son preguntas que tendremos que responder nosotros mismos. Afortunadamente, con IntelliJ IDEA, podemos examinar fácilmente las partes internas de la clase ArrayList y desenredar este problema desde el principio. Si tú tampoco entiendes nada y quieres resolverlo, intentaré ayudarte al menos un poco con esto. Expresión lambda y ArrayList.forEach: cómo funciona De la lección ya sabemos que una expresión lambda es una implementación de una interfaz funcional . Es decir, declaramos una interfaz con una única función y usamos una lambda para describir lo que hace esta función. Para hacer esto necesita: 1. Crear una interfaz funcional; 2. Cree una variable cuyo tipo corresponda a la interfaz funcional; 3. Asigne a esta variable una expresión lambda que describa la implementación de la función; 4. Llamar a una función accediendo a una variable (tal vez estoy siendo tosco en terminología, pero esta es la forma más clara). Daré un ejemplo sencillo de Google, proporcionándole comentarios detallados (gracias a los autores del sitio metanit.com):
interface Operationable {
    int calculate(int x, int y);
    // Единственная функция в интерфейсе — значит, это функциональный интерфейс,
    // который можно реализовать с помощью лямбды
}

public class LambdaApp {

    public static void main(String[] args) {

        // Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
        Operationable operation;
        // Прописываем реализацию функции calculate с помощью лямбды, на вход подаём x и y, на выходе возвращаем их сумму
        operation = (x,y)->x+y;

        // Теперь мы можем обратиться к функции calculate через переменную operation
        int result = operation.calculate(10, 20);
        System.out.println(result); //30
    }
}
Ahora volvamos al ejemplo de la conferencia. Se agregan varios elementos de tipo String a la colección de listas . Luego, los elementos se recuperan utilizando la función estándar forEach , que se llama en el objeto de lista . Una expresión lambda con algunos parámetros extraños se pasa como argumento a la función .
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hola", "Cómo", "дела?");

list.forEach( (s) -> System.out.println(s) );
Si no entendiste de inmediato lo que sucedió aquí, entonces no estás solo. Afortunadamente, IntelliJ IDEA tiene un excelente método abreviado de teclado: Ctrl+Left_Mouse_Button . Si pasamos el cursor sobre forEach y hacemos clic en esta combinación, se abrirá el código fuente de la clase estándar ArrayList, en el que veremos la implementación del método forEach :
public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    final Object[] es = elementData;
    final int size = this.size;
    for (int i = 0; modCount == expectedModCount && i < size; i++)
        action.accept(elementAt(es, i));
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
Vemos que el argumento de entrada es una acción de tipo Consumidor . Movamos el cursor sobre la palabra Consumidor y presionemos nuevamente la combinación mágica Ctrl+LMB . Se abrirá una descripción de la interfaz del consumidor . Si eliminamos la implementación predeterminada (ahora no es importante para nosotros), veremos el siguiente código:
public interface Consumer<t> {
   void accept(T t);
}
Entonces. Tenemos una interfaz de consumidor con una única función de aceptación que acepta un argumento de cualquier tipo. Dado que solo hay una función, la interfaz es funcional y su implementación se puede escribir mediante una expresión lambda. Ya hemos visto que ArrayList tiene una función forEach que toma una implementación de la interfaz del Consumidor como argumento de acción . Además, en la función forEach encontramos el siguiente código:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
El bucle for esencialmente itera a través de todos los elementos de un ArrayList. Dentro del bucle vemos una llamada a la función de aceptación del objeto de acción . ¿Recuerdas cómo llamamos a operación.calcular? El elemento actual de la colección se pasa a la función de aceptación . Ahora finalmente podemos volver a la expresión lambda original y comprender qué hace. Recopilemos todo el código en una pila:
public interface Consumer<t> {
   void accept(T t); // Функция, которую мы реализуем лямбда-выражением
}

public void forEach(Consumer<? super E> action) // В action хранится un objeto Consumer, в котором функция accept реализована нашей лямбдой {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    final Object[] es = elementData;
    final int size = this.size;
    for (int i = 0; modCount == expectedModCount && i < size; i++)
        action.accept(elementAt(es, i)); // Вызываем нашу реализацию функции accept интерфейса Consumer для каждого elemento коллекции
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

//...

list.forEach( (s) -> System.out.println(s) );
Nuestra expresión lambda es una implementación de la función de aceptación descrita en la interfaz del consumidor . Usando una lambda, especificamos que la función de aceptación toma un argumento s y lo muestra en la pantalla. La expresión lambda se pasó a la función forEach como argumento de acción , que almacena la implementación de la interfaz del consumidor . Ahora la función forEach puede llamar a nuestra implementación de la interfaz del Consumidor con una línea como esta:
action.accept(elementAt(es, i));
Por lo tanto, el argumento de entrada s en la expresión lambda es otro elemento de la colección ArrayList , que se pasa a nuestra implementación de la interfaz Consumer . Eso es todo: hemos analizado la lógica de la expresión lambda en ArrayList.forEach. Referencia a un método en ArrayList.forEach: ¿cómo funciona? El siguiente paso en la conferencia es mirar las referencias de métodos. Es cierto que lo entienden de una manera muy extraña: después de leer la conferencia, no tuve ninguna posibilidad de entender qué hace este código:
list.forEach( System.out::println );
Primero, un poco de teoría nuevamente. Una referencia de método es, en términos muy generales, una implementación de una interfaz funcional descrita por otra función . Nuevamente, comenzaré con un ejemplo simple:
public interface Operationable {
    int calculate(int x, int y);
    // Единственная функция в интерфейсе — значит, это функциональный интерфейс
}

public static class Calculator {
    // Создадим статический класс Calculator и пропишем в нём метод methodReference.
    // Именно он будет реализовывать функцию calculate из интерфейса Operationable.
    public static int methodReference(int x, int y) {
        return x+y;
    }
}

public static void main(String[] args) {
    // Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
    Operationable operation;
    // Теперь реализацией интерфейса будет не лямбда-выражение, а метод methodReference из нашего класса Calculator
    operation = Calculator::methodReference;

    // Теперь мы можем обратиться к функции интерфейса через переменную operation
    int result = operation.calculate(10, 20);
    System.out.println(result); //30
}
Volvamos al ejemplo de la conferencia:
list.forEach( System.out::println );
Permítanme recordarles que System.out es un objeto de tipo PrintStream que tiene una función println . Coloquemos el cursor sobre println y hagamos clic en Ctrl+LMB :
public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
Notemos dos características clave: 1. La función println no devuelve nada (void). 2. La función println recibe un argumento como entrada. ¿No te recuerda a nada?
public interface Consumer<t> {
   void accept(T t);
}
Así es: ¡la firma de la función de aceptación es un caso más general de la firma del método println ! Esto significa que este último se puede utilizar con éxito como referencia a un método, es decir, println se convierte en una implementación específica de la función de aceptación :
list.forEach( System.out::println );
Pasamos la función println del objeto System.out como argumento de la función forEach . El principio es el mismo que con lambda: ahora forEach puede pasar un elemento de colección a la función println mediante una llamada action.accept(elementAt(es, i)) . De hecho, esto ahora se puede leer como System.out.println(elementAt(es, i)) .
public void forEach(Consumer<? super E> action) // В action хранится un objeto Consumer, в котором функция accept реализована методом println {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        final Object[] es = elementData;
        final int size = this.size;
        for (int i = 0; modCount == expectedModCount && i < size; i++)
            action.accept(elementAt(es, i)); // Функция accept теперь реализована методом System.out.println!
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
Espero haber aclarado la situación al menos un poco para aquellos que son nuevos en lambdas y referencias de métodos. En conclusión, recomiendo el famoso libro "Java: una guía para principiantes" de Robert Schildt; en mi opinión, en él se describen las lambdas y las referencias a funciones con bastante sensatez.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION