JavaRush/Java блог/Java Developer/Популярно о лямбда-выражениях в Java. С примерами и задач...
Стас Пасинков
26 уровень

Популярно о лямбда-выражениях в Java. С примерами и задачами. Часть 2

Статья из группы Java Developer
участников
Для кого предназначена эта статья?
  • Для тех, кто читал первую часть этой статьи;

  • для тех, кто считает, что уже неплохо знает Java Core, но понятия не имеет о лямбда-выражениях в Java. Или, возможно, что-то уже слышал про лямбды, но без подробностей.

  • для тех, у кого есть какое-то понимание лямбда-выражений, но использовать их до сих пор боязно и непривычно.

Доступ к внешним переменным

Скомпилируется ли такой код с анонимным классом?
int counter = 0;
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter++;
    }
};
Нет. Переменная counter должна быть final. Или не обязательно final, но в любом случае изменять свое значение она не может. Тот же принцип используется и в лямбда-выражениях. Они имеют доступ ко всем переменным, которые им «видны» из того места, где они объявлены. Но лямбда не должна их изменять (присваивать новое значение). Правда, есть вариант обхода этого ограничения в анонимных классах. Достаточно лишь создать переменную ссылочного типа и менять внутреннее состояние объекта. При этом сама переменная будет указывать на тот же объект, и в таком случае можно смело указывать её как final.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Здесь у нас переменная counter является ссылкой на объект типа AtomicInteger. А для изменения состояния этого объекта используется метод incrementAndGet(). Значение самой переменной во время работы программы не меняется и всегда указывает на один и тот же объект, что позволяет нам объявить переменную сразу с ключевым словом final. Эти же примеры, но с лямбда-выражениями:
int counter = 0;
Runnable r = () -> counter++;
Не скомпилируется по той же причине, что и вариант с анонимным классом: counter не должна меняться во время работы программы. Зато вот так — все нормально:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
Это касается и вызова методов. Изнутри лямбда-выражения можно не только обращаться ко всем «видимым» переменным, но и вызывать те методы, к которым есть доступ.
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    }

    private static void staticMethod() {
        System.out.println("Я - метод staticMethod(), и меня только-что кто-то вызвал!");
    }
}
Хотя метод staticMethod() и приватный, но он «доступен» для вызова внутри метода main(), поэтому точно так же доступен для вызова изнутри лямбды, которая создана в методе main.

Момент выполнения кода лямбда-выражения

Возможно, вам этот вопрос покажется слишком простым, но его всё следует задать: когда выполнится код внутри лямбда-выражения? В момент создания? Или же в тот момент, когда (еще и неизвестно где) оно будет вызвано? Проверить довольно просто.
System.out.println("Запуск программы");

// много всякого разного кода
// ...

System.out.println("Перед объявлением лямбды");

Runnable runnable = () -> System.out.println("Я - лямбда!");

System.out.println("После объявления лямбды");

// много всякого другого кода
// ...

System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();
Вывод на экран:
Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!
Видно, что код лямбда-выражения выполнился в самом конце, после того, как был создан тред и лишь когда процесс выполнения программы дошел до фактического выполнения метода run(). А вовсе не в момент его объявления. Объявив лямбда-выражение, мы лишь создали объект типа Runnable и описали поведение его метода run(). Сам же метод был запущен значительно позже.

Method References (Ссылки на методы)?

Не имеет прямого отношения к самим лямбдам, но я считаю, что будет логично сказать об этом пару слов в этой статье. Допустим, у нас есть лямбда-выражение, которое не делает ничего особенного, а просто вызывает какой-то метод.
x -> System.out.println(x)
Ему передали некий х, а оно — просто вызвало System.out.println() и передало туда х. В таком случае, мы можем заменить его на ссылку на нужный нам метод. Вот так:
System.out::println
Да, без скобок в конце! Более полный пример:
List<String> strings = new LinkedList<>();
strings.add("мама");
strings.add("мыла");
strings.add("раму");

strings.forEach(x -> System.out.println(x));
В последней строке мы используем метод forEach(), который принимает объект интерфейса Consumer. Это снова же функциональный интерфейс, у которого только один метод void accept(T t). Соответственно, мы пишем лямбда-выражение, которое принимает один параметр (поскольку он типизирован в самом интерфейсе, тип параметра мы не указываем, а указываем, что называться он у нас будет х). В теле лямбда-выражения пишем код, который будет выполняться при вызове метода accept(). Здесь мы просто выводим на экран то, что попало в переменную х. Сам же метод forEach() проходит по всем элементам коллекции, вызывает у переданного ему объекта интерфейса Consumer (нашей лямбды) метод accept(), куда и передает каждый элемент из коллекции. Как я уже сказал, такое лямбда-выражение (просто вызывающее другой метод) мы можем заменить ссылкой на нужный нам метод. Тогда наш код будет выглядеть так:
List<String> strings = new LinkedList<>();
strings.add("мама");
strings.add("мыла");
strings.add("раму");

strings.forEach(System.out::println);
Главное, чтобы совпадали принимаемые параметры методов (println() и accept()). Поскольку метод println() может принимать что угодно (он перегружен для всех примитивов и для любых объектов, мы можем вместо лямбда-выражения передать в forEach() просто ссылку на метод println(). Тогда forEach() будет брать каждый элемент коллекции и передавать его напрямую в метод println(). Кто сталкивается с этим впервые, обратите внимание, что мы не вызываем метод System.out.println() (с точками между словами и со скобочками в конце), а именно передаем саму ссылку на этот метод. При такой записи
strings.forEach(System.out.println());
у нас будет ошибка компиляции. Поскольку перед вызовом forEach() Java увидит, что вызывается System.out.println(), поймет, что возвращается void и будет пытаться этот void передать в forEach(), который там ждет объект типа Consumer.

Синтаксис использования Method References

Он довольно прост:
  1. Передаем ссылку на статический метод ИмяКласса:: имяСтатическогоМетода?

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("мама");
            strings.add("мыла");
            strings.add("раму");
    
            strings.forEach(Main::staticMethod);
        }
    
        private static void staticMethod(String s) {
            // do something
        }
    }
  2. Передаем ссылку на не статический метод используя существующий объект имяПеременнойСОбъектом:: имяМетода

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("мама");
            strings.add("мыла");
            strings.add("раму");
    
            Main instance = new Main();
            strings.forEach(instance::nonStaticMethod);
        }
    
        private void nonStaticMethod(String s) {
            // do something
        }
    }
  3. Передаем ссылку на не статический метод используя класс, в котором реализован такой метод ИмяКласса:: имяМетода

    public class Main {
        public static void main(String[] args) {
            List<User> users = new LinkedList<>();
            users.add(new User("Вася"));
            users.add(new User("Коля"));
            users.add(new User("Петя"));
    
            users.forEach(User::print);
        }
    
        private static class User {
            private String name;
    
            private User(String name) {
                this.name = name;
            }
    
            private void print() {
                System.out.println(name);
            }
        }
    }
  4. Передаем ссылку на конструктор ИмяКласса::new
    Использование ссылок на методы очень удобно, когда есть готовый метод , который вас полностью устраивает, и вы бы хотели использовать его в качестве callback-а. В таком случае, вместо того чтобы писать лямбда-выражение с кодом того метода, или же лямбда-выражение, где мы просто вызываем этот метод, мы просто передаем ссылку на него. И всё.

Интересное различие между анонимным классом и лямбда-выражением

В анонимном классе ключевое слово this указывает на объект этого анонимного класса. А если использовать this внутри лямбды, мы получим доступ к объекту обрамляющего класса. Того, где мы это выражение, собственно, и написали. Так происходит потому, что лямбда-выражения при компиляции превращаются в приватный метод того класса, где они написаны. Использовать эту «фичу» я бы не рекомендовал, поскольку у неё есть побочный эффект (side effect), что противоречит принципам функционального программирования. Зато такой подход вполне соответствует ООП. ;)

Откуда я брал информацию или что почитать еще

Комментарии (39)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
BlackLine
Уровень 28
3 сентября 2023, 17:33
Лямбда-выражения при компиляции превращаются во внутренний анонимный класс, а не в приватный метод. Хотя с точки зрения функциональности они могут вести себя подобно приватным методам внутри классов. Важным отличием между лямбда-выражениями и анонимными классами является то, что лямбда-выражения имеют доступ к переменным обрамляющего метода или блока, даже если они являются final или effectively final. В то время как в анонимных классах необходимо явно передавать значения через параметры конструктора класса. Таким образом, при вызове this внутри лямбда-выражения, вы обращаетесь к объекту обрамляющего класса, а не к объекту самого лямбда-выражения, как это происходит внутри анонимных классов. Хотелось бы выразить это эмодзи: 😊
BlackLine
Уровень 28
3 сентября 2023, 17:36
А происходит так потому, что в Java ключевое слово this внутри лямбда-выражения ссылается на объект обрамляющего класса, в котором это выражение определено. Это правило было введено для обеспечения совместимости с существующим кодом и для удобства программистов. Когда компилятор встречает лямбда-выражение, он создает новый объект-реализацию функционального интерфейса, представленного лямбда-выражением. Внутри этого объекта создается метод, соответствующий телу лямбда-выражения. Однако, чтобы лямбда-выражение могло получить доступ к переменным обрамляющего метода, компилятор автоматически захватывает значения этих переменных и передает их в новый объект-реализацию. Таким образом, когда внутри лямбда-выражения вы используете this, компилятор интерпретирует его как ссылку на объект обрамляющего класса, чтобы обеспечить доступ к переменным этого класса. Например, рассмотрим следующий код: В данном случае, при вызове this.message внутри лямбда-выражения реализации интерфейса Runnable, мы обращаемся к полю message объекта Main, который является обрамляющим классом для лямбда-выражения. Это позволяет лямбда-выражениям использовать переменные обрамляющего класса без явной передачи их значений через параметры конструктора, как это требуется для анонимных классов.
Стас Пасинков Software Developer в Zipy Master
3 сентября 2023, 18:19
щодо анонімний клас/приватний метод - то в моїй джаві, на якій я тестив всі ті приклади (якась з 8х) - компілілось саме в метод. тому я так і написав. зараз може інакше, не знаю
BlackLine
Уровень 28
3 сентября 2023, 18:26
Ого, а как посмотреть во что компилится в итоге? Просто где ни читал везде говорят что внутренний анонимный класс. Статья у вас очень хорошая, спасибо вам огромное. Дякую!
Стас Пасинков Software Developer в Zipy Master
3 сентября 2023, 19:42
відкриваєте не MyClass.java, а MyClass.class
Alexander Rozenberg
Уровень 32
27 июля 2023, 18:56
fine
Ислам
Уровень 33
7 июня 2023, 09:23
Nice
SWK
Уровень 26
21 июля 2022, 07:04
"Переменная counter должна быть final. Или не обязательно final, но в любом случае изменять свое значение она не может." Просто, клинический нефанат точных формулировок....
Стас Пасинков Software Developer в Zipy Master
21 июля 2022, 08:01
вы тут видите проблему? вам не понятно как это так, что не final переменная не должна изменять свое значение?
SWK
Уровень 26
21 июля 2022, 09:08
Разумеется, вижу. Вот она: Первое предложение противоречит второму.
30 ноября 2022, 17:11
У Стаса всё верно написано. Смысл прост, но надо вчитаться: Переменная не должна быть изменена. Всё. Есть 2 способа: 1) Для этого её надо сознательно не менять, хоть она и обладает свойством изменяемости. 2) Либо сделать её сразу final, чтоб искушения не было ни у кого. Такая вот идея.
Иван
Уровень 28
6 марта 2023, 10:28
Сначала тоже не сосем это понял: "Переменная counter должна быть final. Или не обязательно final, но в любом случае изменять свое значение она не может." "Не может" - не получится или не должна. По ходу - не должна внешняя переменная изменяться в лямде (в общем случае)
hamster🐹 ClipMaker в TikTok
8 ноября 2021, 17:27
Годнота 👍 спасибо
БелК в труселях
Уровень 35
24 октября 2021, 11:57
СПАСИБО.
just_DO_it
Уровень 18
12 октября 2021, 20:19
Передаем ссылку на не статический метод используя класс, в котором реализован такой метод User::print Не очень понимаю тут. 1)Чтобы передать ссылку на статический метод мы обращаемся через имя класса 2)Чтобы передать ссылку на не статический метод мы обращаемся через объект Тут с точки зрения классического подхода все понятно, а вот почему в 3 варианте так?? - User::print Объясните plz И еще я так понял, чтобы делать ссылки такого рода, нужно чтобы метод имел возвращаемый тип и набор параметров такой же как метод в функциональном интерфейсе, который он "реализует"
Стас Пасинков Software Developer в Zipy Master
13 октября 2021, 01:52
первый вариант - это частный случай третьего :) ну или наоборот)) суть в том, что и там, и там класс, в котором есть статический метод. просто в первом случае это класс Main, внутри которого мы это все и пишем, а в третьем - это какой-то другой класс User (он вообще может быть в другом пакете, даже просто из какой-то библиотеке подключенной) да, чтобы использовать Method References - достаточно, чтобы параметры и возвращаемый тип были такие же, как и у метода функционального интерфейса. в случае с foreach() например, там подходит какой угодно метод, который принимает какой-то один параметр и ничего не возвращает) потому что в foreach() надо передать любой метод, который удовлетворял бы метод accept() интерфейса Consumer
Стас Пасинков Software Developer в Zipy Master
13 октября 2021, 02:00
кстати, я там приводил ссылочку на оракловский туториал именно по Method References по опыту могу сказать, что у оракла самые короткие, простые и понятные объяснения, которые я встречал))
Edward Morgan
Уровень 30
16 января 2022, 17:35
У меня как раз по поводу этогопримера есть вопрос. Цепочка следующая: - Метод forEach принимает Consumer - Consumer имеет метод accept возвращающий void и принимающий T - Вы делаете метод референс User::print, на метод print, который , так же как accept возвращает void, но...НЕ ПРИНИМАЕТ НИЧЕГО. Ну то есть консьюмеровсвий accept по параметрам и возвращаемому типу не бьет вашему референсу на метод print. Этот код оаюотает вообще Если да, то плчему? (не могу сейчас проверить работоспособность кода сам)
Стас Пасинков Software Developer в Zipy Master
17 января 2022, 22:45
это вызов нестатического метода. обычно мы привыкли вызывать нестатический метод используя переменную объекта, а не название класса. но в лямдах сделано так, что и вызов статического, и вызов не статического методов - через название класса. конечно, если речь идет об объектах, которые перебирает форич. в данном случае это можно было бы разложить на лямбду типа:
users.forEach(user -> user.print());
то-есть, это лямбда, которая принимает юзера (перед стрелочкой) и ничего не возвращает. что согласуется с интерфейсом Consumer и его методом accept(). то-есть, в лямбде участвует (или даже "является") не сам метод print(), а просто вызов этого метода :) без разницы что там этот метод возвращал бы, или нет. сама лямбда наша ничего не вернет после вызова этого метода :) ЗЫ: а вопрос хороший)) я даже полез в идею проверять, работает ли код из примера))
Edward Morgan
Уровень 30
18 января 2022, 13:47
users.forEach(user -> user.print());
Это я понимаю. внутрь forEach() нужно поместить консьюмер который должен принимать один параметр (user) и не возвращать ничего ( user.print() - имеет возвращаемый тип void). Так что это понятно и идеально согласовывается с консьюмером... Я до сих пор не могу понять почему работает это:
users.forEach(User::print);
Тут все тот же метод forEach который принимает консьюмера, а User::print ссылается на метод который: - возвращает void - принимает void А консьюмер: - возвращает void - принимает Т Я уже проверил в ide и это реально работает...Но я не понимаю как ссылка на метод void,void подходит под консьюмер который void,T. Я не знаю понятно ли я выражась. Поэтому второй вариант вопроса: Вот консьюмер:
public interface Consumer<T> {
    void accept(T t);
}
а вот метод print :
private static class User {
.
.
.
    private void print() {
        System.out.println(name);
    }
}
Метод forEach() принимает КОНСЬЮМЕР либо ССЫЛКУ на метод который по ВОЗВРАЩАЕМОМУ ТИПУ и по ПРИНИМАЕМЫМ ПАРМЕТРАМ такой же как и accept у КОНСЬЮМЕРА. Поэтому я не могу понять почему на место accept нормально подошел print
Стас Пасинков Software Developer в Zipy Master
18 января 2022, 15:45
если вот это
User::print
записать в виде лямды - то получится
user -> user.print()
ну или
user -> {
    user.print();
    // no return
}
ссылка на метод - это НЕ просто вызов метода. это лямда, внутри которой происходит вызов метода. метод print() мог бы принимать кучу разных параметров, мог бы возвращать какое-нибудь значение, но сама лямда - все-равно принимала бы одного юзера, и ничего не возвращала
user -> {
    user.print(1, "test", new Object());
    // no return
}
Artem
Уровень 30
13 мая 2021, 11:14
Приятное дополнения к первой части, которая ,к слову, впервые показала мне как работать с дженерик-интерфейсами. Было трудно, но понял.
7 декабря 2020, 11:38
Статьи огонь! очень доходчиво!
Sha Man
Уровень 1
10 ноября 2020, 10:33
Спасибо за вторую часть )