JavaRush /Blog Java /Random-PL /Lambdy i referencje do metod w ArrayList.forEach - jak to...

Lambdy i referencje do metod w ArrayList.forEach - jak to działa

Opublikowano w grupie Random-PL
Wprowadzenie do wyrażeń lambda w zadaniu Java Syntax Zero rozpoczyna się od bardzo konkretnego przykładu:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Cześć", "Jak", "дела?");

list.forEach( (s) -> System.out.println(s) );
Autorzy wykładu analizują lambdy i odwołania do metod, korzystając ze standardowej funkcji forEach klasy ArrayList. Osobiście trudno mi było zrozumieć sens tego, co się działo, ponieważ implementacja tej funkcji, a także związany z nią interfejs, pozostają „pod maską”. Skąd pochodzą argumenty , skąd przekazywana jest funkcja println(), to pytania, na które będziemy musieli sami sobie odpowiedzieć. Na szczęście dzięki IntelliJ IDEA możemy łatwo zajrzeć do wnętrza klasy ArrayList i rozwinąć ten makaron od samego początku. Jeśli i Ty nic nie rozumiesz i chcesz to zrozumieć, postaram się Ci w tym choć trochę pomóc. Wyrażenie lambda i ArrayList.forEach - jak to działa Z wykładu wiemy już, że wyrażenie lambda jest implementacją interfejsu funkcjonalnego . Oznacza to, że deklarujemy interfejs z jedną funkcją i używamy lambdy do opisania działania tej funkcji. Aby to zrobić, potrzebujesz: 1. Utwórz funkcjonalny interfejs; 2. Utwórz zmienną, której typ odpowiada interfejsowi funkcjonalnemu; 3. Przypisz tej zmiennej wyrażenie lambda opisujące implementację funkcji; 4. Wywołaj funkcję, uzyskując dostęp do zmiennej (być może jestem surowy w terminologii, ale jest to najjaśniejszy sposób). Podam prosty przykład z Google, opatrzony szczegółowymi komentarzami (podziękowania dla autorów serwisu 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
    }
}
Wróćmy teraz do przykładu z wykładu. Do kolekcji list dodano kilka elementów typu String . Elementy są następnie pobierane przy użyciu standardowej funkcji forEach , która jest wywoływana w obiekcie listy . Wyrażenie lambda z pewnym dziwnym parametrem s jest przekazywane jako argument do funkcji .
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Cześć", "Jak", "дела?");

list.forEach( (s) -> System.out.println(s) );
Jeśli nie od razu zrozumiałeś, co się tutaj wydarzyło, to nie jesteś sam. Na szczęście IntelliJ IDEA ma świetny skrót klawiaturowy: Ctrl+Left_Mouse_Button . Jeśli najedziemy kursorem na forEach i klikniemy w tę kombinację, otworzy się kod źródłowy standardowej klasy ArrayList, w którym zobaczymy implementację metody 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();
}
Widzimy, że argumentem wejściowym jest akcja typu Consumer . Najedźmy kursorem na słowo Konsument i ponownie naciśnijmy magiczną kombinację Ctrl+LMB . Otworzy się opis interfejsu konsumenta . Jeśli usuniemy z niego domyślną implementację (nie jest to dla nas teraz istotne), zobaczymy następujący kod:
public interface Consumer<t> {
   void accept(T t);
}
Więc. Mamy interfejs konsumenta z pojedynczą funkcją akceptacji , która akceptuje jeden argument dowolnego typu. Ponieważ istnieje tylko jedna funkcja, interfejs jest funkcjonalny, a jego implementację można zapisać za pomocą wyrażenia lambda. Widzieliśmy już, że ArrayList ma funkcję forEach , która jako argument akcji przyjmuje implementację interfejsu Consumer . Dodatkowo w funkcji forEach znajdziemy następujący kod:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
Pętla for zasadniczo iteruje po wszystkich elementach tablicy ArrayList. Wewnątrz pętli widzimy wywołanie funkcji Accept obiektu akcji - pamiętasz, jak nazywaliśmy operację.calculate? Bieżący element kolekcji jest przekazywany do funkcji Accept . Teraz możemy w końcu wrócić do pierwotnego wyrażenia lambda i zrozumieć, co ono robi. Zbierzmy cały kod w jednym stosie:
public interface Consumer<t> {
   void accept(T t); // Функция, которую мы реализуем лямбда-выражением
}

public void forEach(Consumer<? super E> action) // В action хранится obiekt 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) );
Nasze wyrażenie lambda jest implementacją funkcji akceptowania opisanej w interfejsie konsumenta . Używając lambdy, określiliśmy, że funkcja Accept pobiera argument s i wyświetla go na ekranie. Wyrażenie lambda zostało przekazane do funkcji forEach jako argument akcji , który przechowuje implementację interfejsu Consumer . Teraz funkcja forEach może wywołać naszą implementację interfejsu Consumer za pomocą linii takiej jak ta:
action.accept(elementAt(es, i));
Zatem argument wejściowy s w wyrażeniu lambda jest kolejnym elementem kolekcji ArrayList , który jest przekazywany do naszej implementacji interfejsu Consumer . To wszystko: przeanalizowaliśmy logikę wyrażenia lambda w ArrayList.forEach. Odniesienie do metody w ArrayList.forEach - jak to działa? Następnym krokiem wykładu jest zapoznanie się z odniesieniami do metod. Co prawda oni to rozumieją w bardzo dziwny sposób – po przeczytaniu wykładu nie miałem szans zrozumieć, co robi ten kod:
list.forEach( System.out::println );
Na początek znowu trochę teorii. Odwołanie do metody to, mówiąc najprościej, implementacja interfejsu funkcjonalnego opisanego przez inną funkcję . Znów zacznę od prostego przykładu:
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
}
Wróćmy do przykładu z wykładu:
list.forEach( System.out::println );
Przypomnę, że System.out jest obiektem typu PrintStream, który posiada funkcję println . Najedźmy kursorem na println i kliknij Ctrl+LMB :
public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
Zwróćmy uwagę na dwie kluczowe cechy: 1. Funkcja println niczego nie zwraca (void). 2. Funkcja println otrzymuje na wejściu jeden argument. Nic Ci nie przypomina?
public interface Consumer<t> {
   void accept(T t);
}
Zgadza się — sygnatura funkcji Accept jest bardziej ogólnym przypadkiem sygnatury metody println ! Oznacza to, że tę ostatnią z powodzeniem można wykorzystać jako odniesienie do metody - czyli println staje się specyficzną implementacją funkcji Accept :
list.forEach( System.out::println );
Przekazaliśmy funkcję println obiektu System.out jako argument funkcji forEach . Zasada jest taka sama jak w przypadku lambdy: teraz forEach może przekazać element kolekcji do funkcji println poprzez wywołanie action.accept(elementAt(es, i)) . W rzeczywistości można to teraz odczytać jako System.out.println(elementAt(es, i)) .
public void forEach(Consumer<? super E> action) // В action хранится obiekt 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();
    }
Mam nadzieję, że choć trochę wyjaśniłem sytuację tym, którzy nie mają doświadczenia z lambdami i odniesieniami do metod. Podsumowując, polecam słynną książkę „Java: A Beginner's Guide” Roberta Schildta – moim zdaniem lambdy i odniesienia do funkcji są w niej dość sensownie opisane.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION