JavaRush /Blog Java /Random-FR /Lambdas et références de méthodes dans ArrayList.forEach ...

Lambdas et références de méthodes dans ArrayList.forEach - comment ça marche

Publié dans le groupe Random-FR
L'introduction aux expressions lambda dans la quête Java Syntax Zero commence par un exemple très précis :
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Les auteurs de la conférence analysent les lambdas et les références de méthodes à l'aide de la fonction standard forEach de la classe ArrayList. Personnellement, j'ai eu du mal à comprendre le sens de ce qui se passait, puisque la mise en œuvre de cette fonction, ainsi que l'interface qui lui est associée, restent « sous le capot ». D'où viennent le ou les arguments , où est passée la fonction println() sont des questions auxquelles nous devrons répondre nous-mêmes. Heureusement, avec IntelliJ IDEA, nous pouvons facilement examiner les composants internes de la classe ArrayList et dérouler cette nouille dès le début. Si vous ne comprenez rien non plus et que vous voulez le comprendre, je vais essayer de vous aider au moins un peu. Expression lambda et ArrayList.forEach - comment ça marche Depuis le cours, nous savons déjà qu'une expression lambda est une implémentation d'une interface fonctionnelle . Autrement dit, nous déclarons une interface avec une seule fonction et utilisons un lambda pour décrire ce que fait cette fonction. Pour ce faire, vous devez : 1. Créer une interface fonctionnelle ; 2. Créez une variable dont le type correspond à l'interface fonctionnelle ; 3. Attribuez à cette variable une expression lambda qui décrit l'implémentation de la fonction ; 4. Appelez une fonction en accédant à une variable (je suis peut-être grossier en terminologie, mais c'est le moyen le plus clair). Je vais donner un exemple simple de Google, en l'accompagnant de commentaires détaillés (merci aux auteurs du site 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
    }
}
Revenons maintenant à l'exemple du cours. Plusieurs éléments de type String sont ajoutés à la collection de listes . Les éléments sont ensuite récupérés à l'aide de la fonction standard forEach , qui est appelée sur l' objet liste . Une expression lambda avec un paramètre étrange s est passée en argument à la fonction .
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Si vous n’avez pas immédiatement compris ce qui s’est passé ici, alors vous n’êtes pas seul. Heureusement, IntelliJ IDEA dispose d'un excellent raccourci clavier : Ctrl+Left_Mouse_Button . Si nous survolons forEach et cliquons sur cette combinaison, le code source de la classe standard ArrayList s'ouvrira, dans lequel nous verrons l'implémentation de la méthode 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();
}
Nous voyons que l'argument d'entrée est une action de type Consumer . Déplaçons le curseur sur le mot Consommateur et appuyons à nouveau sur la combinaison magique Ctrl+LMB . Une description de l' interface Consommateur s'ouvrira . Si nous en supprimons l'implémentation par défaut (ce n'est plus important pour nous maintenant), nous verrons le code suivant :
public interface Consumer<t> {
   void accept(T t);
}
Donc. Nous avons une interface consommateur avec une seule fonction d'acceptation qui accepte un argument de n'importe quel type. Puisqu'il n'y a qu'une seule fonction, alors l'interface est fonctionnelle et son implémentation peut être écrite via une expression lambda. Nous avons déjà vu qu'ArrayList possède une fonction forEach qui prend une implémentation de l' interface Consumer comme argument d'action . De plus, dans la fonction forEach on retrouve le code suivant :
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
La boucle for parcourt essentiellement tous les éléments d’une ArrayList. À l'intérieur de la boucle, nous voyons un appel à la fonction accept de l' objet action - rappelez-vous comment nous avons appelé operation.calculate ? L'élément actuel de la collection est transmis à la fonction accept . Nous pouvons maintenant enfin revenir à l'expression lambda originale et comprendre ce qu'elle fait. Rassemblons tout le code en une seule pile :
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) );
Notre expression lambda est une implémentation de la fonction accept décrite dans l' interface Consumer . À l'aide d'un lambda, nous avons précisé que la fonction accept prend un argument s et l'affiche à l'écran. L'expression lambda a été transmise à la fonction forEach comme argument d'action , qui stocke l'implémentation de l' interface Consumer . Désormais, la fonction forEach peut appeler notre implémentation de l'interface Consumer avec une ligne comme celle-ci :
action.accept(elementAt(es, i));
Ainsi, l'argument d'entrée s dans l'expression lambda est un autre élément de la collection ArrayList , qui est transmis à notre implémentation de l'interface Consumer . C'est tout : nous avons analysé la logique de l'expression lambda dans ArrayList.forEach. Référence à une méthode dans ArrayList.forEach - comment ça marche ? La prochaine étape du cours consiste à examiner les références de méthodes. C'est vrai, ils le comprennent d'une manière très étrange - après avoir lu la conférence, je n'avais aucune chance de comprendre ce que fait ce code :
list.forEach( System.out::println );
Tout d’abord, encore un peu de théorie. Une référence de méthode est, en gros, une implémentation d'une interface fonctionnelle décrite par une autre fonction . Encore une fois, je vais commencer par un exemple 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
}
Revenons à l'exemple du cours :
list.forEach( System.out::println );
Permettez-moi de vous rappeler que System.out est un objet de type PrintStream qui possède une fonction println . Passons la souris sur println et cliquons Ctrl+LMB :
public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
Notons deux fonctionnalités clés : 1. La fonction println ne renvoie rien (void). 2. La fonction println reçoit un argument en entrée. Cela ne vous rappelle rien ?
public interface Consumer<t> {
   void accept(T t);
}
C'est vrai - la signature de la fonction accept est un cas plus général de la signature de la méthode println ! Cela signifie que cette dernière peut être utilisée avec succès comme référence à une méthode - c'est-à-dire que println devient une implémentation spécifique de la fonction accept :
list.forEach( System.out::println );
Nous avons passé la fonction println de l'objet System.out comme argument à la fonction forEach . Le principe est le même qu'avec le lambda : désormais forEach peut passer un élément de collection à la fonction println via un appel action.accept(elementAt(es, i)) . En fait, cela peut maintenant être lu comme 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();
    }
J'espère avoir clarifié au moins un peu la situation pour ceux qui sont nouveaux dans les lambdas et les références de méthodes. En conclusion, je recommande le célèbre livre "Java: A Beginner's Guide" de Robert Schildt - à mon avis, les lambdas et les références de fonctions y sont décrits de manière assez judicieuse.
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION