JavaRush /Java Blog /Random-IT /Lambda e riferimenti ai metodi in ArrayList.forEach: come...

Lambda e riferimenti ai metodi in ArrayList.forEach: come funziona

Pubblicato nel gruppo Random-IT
L'introduzione alle espressioni lambda nella ricerca Java Syntax Zero inizia con un esempio molto specifico:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Gli autori della lezione analizzano lambda e riferimenti ai metodi utilizzando la funzione standard forEach della classe ArrayList. Personalmente ho trovato difficile comprendere il significato di quanto stava accadendo, poiché l'implementazione di questa funzione, così come l'interfaccia ad essa associata, rimane “sotto il cofano”. Da dove provengono gli argomenti , dove viene passata la funzione println() sono domande a cui dovremo rispondere da soli. Fortunatamente, con IntelliJ IDEA, possiamo facilmente esaminare gli interni della classe ArrayList e risolvere questo problema fin dall'inizio. Se anche tu non capisci niente e vuoi capirlo, cercherò di aiutarti almeno un po'. Espressione lambda e ArrayList.forEach - come funziona Dalla lezione sappiamo già che un'espressione lambda è un'implementazione di un'interfaccia funzionale . Cioè, dichiariamo un'interfaccia con una singola funzione e utilizziamo un lambda per descrivere cosa fa questa funzione. Per fare questo è necessario: 1. Creare un'interfaccia funzionale; 2. Creare una variabile il cui tipo corrisponde all'interfaccia funzionale; 3. Assegna a questa variabile un'espressione lambda che descrive l'implementazione della funzione; 4. Chiamare una funzione accedendo a una variabile (forse sono rozzo nella terminologia, ma questo è il modo più chiaro). Farò un semplice esempio tratto da Google, corredandolo di commenti dettagliati (grazie agli autori del sito 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
    }
}
Ora torniamo all'esempio della lezione. Diversi elementi di tipo String vengono aggiunti alla raccolta list . Gli elementi vengono quindi recuperati utilizzando la funzione standard forEach , che viene richiamata sull'oggetto list . Un'espressione lambda con alcuni parametri strani viene passata come argomento alla funzione .
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Se non hai capito immediatamente cosa è successo qui, allora non sei solo. Fortunatamente, IntelliJ IDEA ha un'ottima scorciatoia da tastiera: Ctrl+Left_Mouse_Button . Se passiamo il mouse su forEach e facciamo clic su questa combinazione, si aprirà il codice sorgente della classe ArrayList standard, in cui vedremo l'implementazione del metodo 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();
}
Vediamo che l'argomento di input è un'azione di tipo Consumer . Spostiamo il cursore sulla parola Consumatore e premiamo nuovamente la combinazione magica Ctrl+LMB . Si aprirà una descrizione dell'interfaccia Consumatore . Se rimuoviamo da esso l'implementazione predefinita (non è importante per noi ora), vedremo il seguente codice:
public interface Consumer<t> {
   void accept(T t);
}
COSÌ. Abbiamo un'interfaccia Consumer con una singola funzione di accettazione che accetta un argomento di qualsiasi tipo. Poiché esiste una sola funzione, l'interfaccia è funzionale e la sua implementazione può essere scritta tramite un'espressione lambda. Abbiamo già visto che ArrayList ha una funzione forEach che accetta un'implementazione dell'interfaccia Consumer come argomento dell'azione . Inoltre, nella funzione forEach troviamo il seguente codice:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
Il ciclo for essenzialmente scorre tutti gli elementi di un ArrayList. All'interno del ciclo vediamo una chiamata alla funzione accetta dell'oggetto azione : ricordi come abbiamo chiamato operazione.calculate? L'elemento corrente della raccolta viene passato alla funzione accetta . Ora possiamo finalmente tornare all'espressione lambda originale e capire cosa fa. Raccogliamo tutto il codice in una pila:
public interface Consumer<t> {
   void accept(T t); // Функция, которую мы реализуем лямбда-выражением
}

public void forEach(Consumer<? super E> action) // В action хранится an object 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 для каждого element коллекции
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

//...

list.forEach( (s) -> System.out.println(s) );
La nostra espressione lambda è un'implementazione della funzione di accettazione descritta nell'interfaccia Consumer . Utilizzando una lambda, abbiamo specificato che la funzione accetta accetta un argomento s e lo visualizza sullo schermo. L'espressione lambda è stata passata alla funzione forEach come argomento dell'azione , che memorizza l'implementazione dell'interfaccia Consumer . Ora la funzione forEach può chiamare la nostra implementazione dell'interfaccia Consumer con una riga come questa:
action.accept(elementAt(es, i));
Pertanto, l'argomento di input s nell'espressione lambda è un altro elemento della raccolta ArrayList , che viene passato alla nostra implementazione dell'interfaccia Consumer . Questo è tutto: abbiamo analizzato la logica dell'espressione lambda in ArrayList.forEach. Riferimento a un metodo in ArrayList.forEach: come funziona? Il passaggio successivo della lezione consiste nell'esaminare i riferimenti al metodo. È vero, lo capiscono in un modo molto strano: dopo aver letto la lezione, non ho avuto alcuna possibilità di capire cosa fa questo codice:
list.forEach( System.out::println );
Innanzitutto, ancora una piccola teoria. Un riferimento al metodo è, in parole povere, un'implementazione di un'interfaccia funzionale descritta da un'altra funzione . Ancora una volta, inizierò con un semplice esempio:
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
}
Torniamo all'esempio della lezione:
list.forEach( System.out::println );
Permettetemi di ricordarvi che System.out è un oggetto di tipo PrintStream che ha una funzione println . Passiamo il mouse su println e facciamo clic su Ctrl+LMB :
public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
Notiamo due caratteristiche chiave: 1. La funzione println non restituisce nulla (void). 2. La funzione println riceve un argomento come input. Non ti ricorda niente?
public interface Consumer<t> {
   void accept(T t);
}
Esatto: la firma della funzione accetta è un caso più generale della firma del metodo println ! Ciò significa che quest'ultimo può essere utilizzato con successo come riferimento a un metodo, ovvero println diventa un'implementazione specifica della funzione accetta :
list.forEach( System.out::println );
Abbiamo passato la funzione println dell'oggetto System.out come argomento alla funzione forEach . Il principio è lo stesso del lambda: ora forEach può passare un elemento di raccolta alla funzione println tramite una chiamata action.accept(elementAt(es, i)) . In effetti, ora può essere letto come System.out.println(elementAt(es, i)) .
public void forEach(Consumer<? super E> action) // В action хранится an object 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();
    }
Spero di aver chiarito almeno un po' la situazione per chi è nuovo alle lambda e ai riferimenti ai metodi. In conclusione, consiglio il famoso libro "Java: A Beginner's Guide" di Robert Schildt - secondo me, lambda e riferimenti alle funzioni sono descritti in modo abbastanza sensato.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION