JavaRush /Java Blog /Random EN /Lambdas and method references in ArrayList.forEach - how ...

Lambdas and method references in ArrayList.forEach - how it works

Published in the Random EN group
The introduction to lambda expressions in the Java Syntax Zero quest begins with a very specific example:

ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
The authors of the lecture parse lambdas and method references using the standard forEach function of the ArrayList class. Personally, I found it difficult to understand the meaning of what was happening, since the implementation of this function, as well as the interface associated with it, remains “under the hood”. Where the argument (s) comes from , where the println() function is passed are questions that we will have to answer ourselves. Fortunately, with IntelliJ IDEA, we can easily look into the internals of the ArrayList class and unwind this noodle from the very beginning. If you also don’t understand anything and want to figure it out, I’ll try to help you with this at least a little. Lambda expression and ArrayList.forEach - how it works From the lecture we already know that a lambda expression is an implementation of a functional interface . That is, we declare an interface with one single function, and use a lambda to describe what this function does. To do this you need: 1. Create a functional interface; 2. Create a variable whose type corresponds to the functional interface; 3. Assign this variable a lambda expression that describes the implementation of the function; 4. Call a function by accessing a variable (perhaps I’m being crude in terminology, but this is the clearest way). I will give a simple example from Google, providing it with detailed comments (thanks to the authors of the 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
    }
}
Now let's return to the example from the lecture. Several elements of type String are added to the list collection . The elements are then retrieved using the standard forEach function , which is called on the list object . A lambda expression with some strange parameter s is passed as an argument to the function .

ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
If you didn't immediately understand what happened here, then you are not alone. Luckily, IntelliJ IDEA has a great keyboard shortcut: Ctrl+Left_Mouse_Button . If we hover over forEach and click this combination, the source code of the standard ArrayList class will open, in which we will see the implementation of the forEach method :

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();
}
We see that the input argument is action of type Consumer . Let's move the cursor over the word Consumer and press the magic combination Ctrl+LMB again . A description of the Consumer interface will open . If we remove the default implementation from it (it is not important to us now), we will see the following code:

public interface Consumer<t> {
   void accept(T t);
}
So. We have a Consumer interface with a single accept function that accepts one argument of any type. Since there is only one function, then the interface is functional, and its implementation can be written through a lambda expression. We've already seen that ArrayList has a forEach function that takes an implementation of the Consumer interface as an action argument . In addition, in the forEach function we find the following code:

for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
The for loop essentially iterates through all the elements of an ArrayList. Inside the loop we see a call to the accept function of the action object - remember how we called operation.calculate? The current element of the collection is passed to the accept function . Now we can finally go back to the original lambda expression and understand what it does. Let's collect all the code in one 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) );
Our lambda expression is an implementation of the accept function described in the Consumer interface . Using a lambda, we specified that the accept function takes an argument s and displays it on the screen. The lambda expression was passed to the forEach function as its action argument , which stores the implementation of the Consumer interface . Now the forEach function can call our implementation of the Consumer interface with a line like this:

action.accept(elementAt(es, i));
Thus, the input argument s in the lambda expression is another element of the ArrayList collection , which is passed to our implementation of the Consumer interface . That's all: we have analyzed the logic of the lambda expression in ArrayList.forEach. Reference to a method in ArrayList.forEach - how does it work? The next step in the lecture is to look at method references. True, they understand it in a very strange way - after reading the lecture, I had no chance of understanding what this code does:

list.forEach( System.out::println );
First, a little theory again. A method reference is, very roughly speaking, an implementation of a functional interface described by another function . Again, I'll start with a simple example:

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
}
Let's return to the example from the lecture:

list.forEach( System.out::println );
Let me remind you that System.out is an object of type PrintStream that has a println function . Let's hover over println and click Ctrl+LMB :

public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
Let's note two key features: 1. The println function does not return anything (void). 2. The println function receives one argument as input. Doesn't remind you of anything?

public interface Consumer<t> {
   void accept(T t);
}
That's right - the accept function signature is a more general case of the println method signature ! This means that the latter can be successfully used as a reference to a method - that is, println becomes a specific implementation of the accept function :

list.forEach( System.out::println );
We passed the System.out object's println function as an argument to the forEach function . The principle is the same as with the lambda: now forEach can pass a collection element to the println function via an action.accept(elementAt(es, i)) call . In fact, this can now be read as 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();
    }
I hope that I have clarified the situation at least a little for those who are new to lambdas and method references. In conclusion, I recommend the famous book "Java: A Beginner's Guide" by Robert Schildt - in my opinion, lambdas and function references are described quite sensibly in it.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION