JavaRush /جاوا بلاگ /Random-UR /Популярно о лямбда-выражениях в Java. С примерами и задач...

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

گروپ میں شائع ہوا۔
Для кого предназначена эта статья?
  • Для тех, кто читал первую часть этой статьи;

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

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

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

Скомпorруется ли такой code с анонимным классом?

int counter = 0;
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter++;
    }
};
Нет. Переменная counter должна быть final. Или не обязательно final, но в любом случае изменять свое meaning она не может. Тот же принцип используется и в лямбда-выражениях. Они имеют доступ ко всем переменным, которые им «видны» из того места, где они объявлены. Но лямбда не должна их изменять (присваивать новое meaning). Правда, есть вариант обхода этого ограничения в анонимных классах. Достаточно лишь создать переменную ссылочного типа и менять внутреннее состояние an object. При этом сама переменная будет указывать на тот же an object, и в таком случае можно смело указывать её How final.

final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Здесь у нас переменная counter является ссылкой на an object типа AtomicInteger. А для изменения состояния этого an object используется метод incrementAndGet(). Значение самой переменной во время работы программы не меняется и всегда указывает на один и тот же an object, что позволяет нам объявить переменную сразу с ключевым словом final. Эти же примеры, но с лямбда-выражениями:

int counter = 0;
Runnable r = () -> counter++;
Не скомпorруется по той же причине, что и вариант с анонимным классом: 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.

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

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

System.out.println("Запуск программы");

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

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

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

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

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

System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();

Вывод на экран:

Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!
Видно, что code лямбда-выражения выполнился в самом конце, после того, How был создан тред и лишь когда процесс выполнения программы дошел до фактического выполнения метода run(). А вовсе не в момент его объявления. Объявив лямбда-выражение, мы лишь создали an object типа Runnable и описали поведение его метода run(). Сам же метод был запущен значительно позже.

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

Не имеет прямого отношения к самим лямбдам, но я считаю, что будет логично сказать об этом пару слов в этой статье. Допустим, у нас есть лямбда-выражение, которое не делает ничего особенного, а просто вызывает Howой-то метод.

x -> System.out.println(x)
Ему передали некий х, а оно — просто вызвало System.out.println() и передало туда х. В таком случае, мы можем заменить его на ссылку на нужный нам метод. Вот так:

System.out::println
Да, без скобок в конце! Более полный пример:

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

List<String> strings = new LinkedList<>();
strings.add("Mother");
strings.add("soap");
strings.add("frame");

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

strings.forEach(System.out.println());
у нас будет ошибка компиляции. Поскольку перед вызовом forEach() Java увидит, что вызывается System.out.println(), поймет, что возвращается void и будет пытаться этот void передать в forEach(), который там ждет an object типа Consumer.

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

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

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

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

    
    public class Main {
        public static void main(String[] args) {
            List<User> users = new LinkedList<>();
            users.add(new User("Vasya"));
            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. Передаем ссылку на конструктор NameКласса::new
    Использование ссылок на методы очень удобно, когда есть готовый метод , который вас fully устраивает, и вы бы хотели использовать его в качестве callback-а. В таком случае, instead of того чтобы писать лямбда-выражение с codeом того метода, or же лямбда-выражение, где мы просто вызываем этот метод, мы просто передаем ссылку на него. И всё.

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

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

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

تبصرے
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION