JavaRush /Java блог /Random UA /Lambda-вирази на прикладах
Автор
Aditi Nawghare
Инженер-программист в Siemens

Lambda-вирази на прикладах

Стаття з групи Random UA
Java спочатку повністю об'єктно-орієнтована мова. За винятком примітивних типів, все в Java – це об'єкти. Навіть масиви є об'єктами. Примірники кожного класу – об'єкти. Не існує жодної можливості визначити окремо (поза класом – прим. перекл. ) якусь функцію. І немає жодної можливості передати метод як аргумент або повернути тіло методу як наслідок іншого методу. Все так. Але так було до Java 8. Lambda-вирази на прикладах - 1З часів старого доброго Swing треба було писати анонімні класи, коли потрібно було передати якусь функціональність у якийсь метод. Наприклад, так виглядало додавання оброблювача подій:
someObject.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {

                //Event listener implementation goes here...

            }
        });
Тут ми хочемо додати деякий код до слухача подій від миші. Ми визначабо анонімний клас MouseAdapterі одразу створабо об'єкт із нього. У такий спосіб ми передали додаткову функціональність у метод addMouseListener. Коротше кажучи, не просто передати простий метод (функціональність) в Java через аргументи. Це обмеження змусило розробників Java 8 додати специфікацію мови таку можливість як Lambda-вираження.

Навіщо яві Lambda-вирази?

З самого початку, мова Java особливо не розвивалася, якщо не вважати такі речі як анотації (Annotations), дженерики (Generics) та ін. Насамперед Java завжди залишався об'єктно-орієнтованим. Після роботи з функціональними мовами, такими як JavaScript, можна зрозуміти, наскільки Java строго об'єктно-орієнтований і строго типізований. Функції Java не потрібні. Самі собою їх не можна зустріти у світі Java. У функціональних мовах програмування першому плані виходять функції. Вони існують власними силами. Можна надавати їх змінним і передавати через аргументи іншим функціям. JavaScript один із найкращих прикладів функціональних мов програмування. На просторах Інтернету можна знайти хороші статті, в яких детально описано переваги JavaScript як функціональної мови. Функціональні мови мають у своєму арсеналі такі потужні інструменти як замикання (Closure), які забезпечують ряд переваг традиційними способами написання додатків. Замикання - це функція з прив'язаним до неї середовищем - таблицею, що зберігає посилання на всі нелокальні змінні функції. У Java замикання можна імітувати через Lambda-вирази. Безумовно, між замиканнями та Lambda-виразами є відмінності і не малі, але лямбда вирази є гарною альтернативою замиканням. У своєму саркастичному і кумедному блозі, Стів Ієг (Steve Yegge) описує наскільки світ Java суворо зав'язаний на іменники (сутності, об'єкти) зберігає посилання на всі нелокальні змінні функції. У Java замикання можна імітувати через Lambda-вирази. Безумовно, між замиканнями та Lambda-виразами є відмінності і не малі, але лямбда вирази є гарною альтернативою замиканням. У своєму саркастичному і кумедному блозі, Стів Ієг (Steve Yegge) описує наскільки світ Java суворо зав'язаний на іменники (сутності, об'єкти) зберігає посилання на всі нелокальні змінні функції. У Java замикання можна імітувати через Lambda-вирази. Безумовно, між замиканнями та Lambda-виразами є відмінності і не малі, але лямбда вирази є гарною альтернативою замиканням. У своєму саркастичному і кумедному блозі, Стів Ієг (Steve Yegge) описує наскільки світ Java суворо зав'язаний на іменники (сутності, об'єкти)прим. перев.). Якщо ви не читали його блог, рекомендую. Він цікаво і цікаво описує точну причину того, чому в Java додали Lambda-вирази. Lambda-вирази привносять в Java функціональну ланку, якої так давно не вистачало. Lambda-вирази вносять у мову функціональність на рівні з об'єктами. Хоча це і не на 100% правильно, можна бачити, що Lambda-вирази не будучи замикання надають подібні можливості. У функціональній мові lambda-вирази – це функції; але в Java, lambda-вирази - представляються об'єктами, і повинні бути пов'язані з конкретним об'єктним типом, який називається функціональним інтерфейсом. Далі ми розглянь, що він собою представляє. У статті Маріо Фаско (Mario Fusco) "Навіщо в Java потрібні Lambda-вирази" ("Why we need Lambda Expression in Java") докладно описано,

Введення в Lambda-вирази

Lambda-вирази – це анонімні функції (може і не 100% правильне визначення для Java, зате привносить деяку ясність). Простіше кажучи, це спосіб без оголошення, тобто. без модифікаторів доступу, що повертають значення та ім'я. Коротше кажучи, вони дозволяють написати метод і відразу ж використовувати його. Особливо корисно у разі одноразового виклику методу, т.к. скорочує час на оголошення та написання методу без необхідності створювати клас. Lambda-вирази в Java зазвичай мають наступний синтаксис (аргументы) -> (тело). Наприклад:
(арг1, арг2...) -> { тело }

(тип1 арг1, тип2 арг2...) -> { тело }
Далі йде кілька прикладів справжніх Lambda-виразів:
(int a, int b) -> {  return a + b; }

() -> System.out.println("Hello World");

(String s) -> { System.out.println(s); }

() -> 42

() -> { return 3.1415 };

Структура Lambda-виразів

Давайте вивчимо структуру lambda-виразів:
  • Lambda-вираження можуть мати від 0 і більше вхідних параметрів.
  • Тип параметрів можна вказувати явно або можна отримати з контексту. Наприклад ( int a) можна записати і так ( a)
  • Параметри полягають у круглі дужки та розділяються комами. Наприклад ( a, b) або ( int a, int b) або ( String a, int b, float c)
  • Якщо параметрів немає, потрібно використовувати порожні круглі дужки. Наприклад() -> 42
  • Коли параметр один, якщо тип не вказується явно, можна опустити дужки. Приклад:a -> return a*a
  • Тіло Lambda-вирази може містити від 0 і більше виразів.
  • Якщо тіло складається з одного оператора, його можна не укладати у фігурні дужки, а значення, що повертається, можна вказувати без ключового слова return.
  • В іншому випадку фігурні дужки обов'язкові (блок коду), а в кінці треба вказувати значення, що повертається з використанням ключового слова return(інакше типом значення, що повертається буде void).

Що таке функціональний інтерфейс

У Java, маркерні інтерфейси (Marker interface) - це інтерфейси без оголошення методів та полів. Тобто маркерні інтерфейси - це порожні інтерфейси. Так само, функціональні інтерфейси (Functional Interface) – це інтерфейси лише з одним абстрактним методом, оголошеним у ньому. java.lang.Runnable- Це приклад функціонального інтерфейсу. У ньому оголошено лише один метод void run(). Також є інтерфейс ActionListener– також функціональний. Раніше нам доводилося використовувати анонімні класи для створення об'єктів, що реалізують функціональний інтерфейс. З Lambda-виразами, все стало простіше. Кожен lambda-вираз може бути неявно прив'язаний до якогось функціонального інтерфейсу. Наприклад, можна створити посилання на Runnableінтерфейс, як показано в наступному прикладі:
Runnable r = () -> System.out.println("hello world");
Подібне перетворення завжди здійснюється неявно, коли ми не вказуємо функціональний інтерфейс:
new Thread(
    () -> System.out.println("hello world")
).start();
У прикладі вище компілятор автоматично створює lambda-вираз як реалізацію Runnableінтерфейсу з конструктора класу Thread: public Thread(Runnable r) { }. Наведу кілька прикладів lambda-виразів та відповідних функціональних інтерфейсів:
Consumer<Integer> c = (int x) -> { System.out.println(x) };

BiConsumer<Integer, String> b = (Integer x, String y) -> System.out.println(x + " : " + y);

Predicate<String> p = (String s) -> { s == null };
Анотація @FunctionalInterface, додана в Java 8 згідно Java Language Specification, перевіряє, чи є інтерфейс функціональним. Крім того, Java 8 включений ряд готових функціональних інтерфейсів для використання з Lambda-виразами. @FunctionalInterfaceвидасть помилку компіляції, якщо інтерфейс, що оголошується, не буде функціональним. Далі наводиться приклад визначення функціонального інтерфейсу:
@FunctionalInterface
public interface WorkerInterface {

    public void doSomeWork();

}
Як випливає з визначення, функціональний інтерфейс може мати лише абстрактний метод. Якщо додати ще один абстрактний метод, то вилізе помилка компіляції. Приклад:
@FunctionalInterface
public interface WorkerInterface {

    public void doSomeWork();

    public void doSomeMoreWork();

}
Error
Unexpected @FunctionalInterface annotation
    @FunctionalInterface ^ WorkerInterface is not a functional interface multiple
    non-overriding abstract methods found in interface WorkerInterface 1 error
После определения функционального интерфейса, мы можем его использовать и получать все преимущества Lambda-выражений. Пример:// Визначення функціонального інтерфейсу
@FunctionalInterface
public interface WorkerInterface {

    public void doSomeWork();

}
public class WorkerInterfaceTest {

    public static void execute(WorkerInterface worker) {
        worker.doSomeWork();
    }

    public static void main(String [] args) {

      // виклик методу doSomeWork через анонімний клас
      //(Класичний спосіб)
      execute(new WorkerInterface() {
            @Override
            public void doSomeWork() {
               System.out.println("Worker викликаний через анонімний клас");
            }
        });

      // виклик методу doSomeWork через Lambda-вирази
      // (нововведення Java 8)
      execute( () -> System.out.println("Worker викликаний через Lambda") );
    }

}
Висновок:
Worker вызван через анонимный класс
Worker вызван через Lambda
Тут ми визначабо свій власний функціональний інтерфейс та скористалися lambda-виразом. Метод execute()здатний приймати lambda-вирази як аргумент.

Приклади Lambda-виразів

Найкращий спосіб вникнути в Lambda-вирази – це розглянути кілька прикладів: Потік Threadможна проініціалізувати двома способами:
// Старий метод:
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello from thread");
    }
}).start();
// Новий спосіб:
new Thread(
    () -> System.out.println("Hello from thread")
).start();
Управління подіями в Java 8 також можна здійснювати через Lambda-вирази. Далі представлені два способи додавання оброблювача події ActionListenerв компонент інтерфейсу користувача:
// Старий метод:
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Кнопка натиснута. Старий спосіб!");
    }
});
// Новий спосіб:
button.addActionListener( (e) -> {
        System.out.println("Кнопка натиснута. Lambda!");
});
Простий приклад виведення всіх елементів заданого масиву. Зауважте, що є більше одного способу використання lambda-виразу. Нижче ми створюємо lambda-вираз звичайним способом, використовуючи синтаксис стрілки, а також ми використовуємо оператор подвійного двокрапки (::), який у Java 8 конвертує звичайний метод в lambda-вираз:
// Старий метод:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for(Integer n: list) {
    System.out.println(n);
}
// Новий спосіб:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> System.out.println(n));
// Новий спосіб з використанням оператора подвійної двокрапки ::
list.forEach(System.out::println);
У наступному прикладі ми використовуємо функціональний інтерфейс Predicateдля створення тесту та друку елементів, що пройшли цей тест. У такий спосіб ви можете поміщати логіку в lambda-вирази і робити щось на її основі.
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class Main {

    public static void main(String [] a)  {

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);

        System.out.print("Виводить всі числа:");
        evaluate(list, (n)->true);

        System.out.print("Не виводить жодного числа:");
        evaluate(list, (n)->false);

        System.out.print("Виведення парних чисел: ");
        evaluate(list, (n)-> n%2 == 0 );

        System.out.print("Виведення непарних чисел: ");
        evaluate(list, (n)-> n%2 == 1 );

        System.out.print("Виведення чисел більше 5: ");
        evaluate(list, (n)-> n > 5 );

    }

    public static void evaluate(List<Integer> list, Predicate<Integer> predicate) {
        for(Integer n: list)  {
            if(predicate.test(n)) {
                System.out.print(n + " ");
            }
        }
        System.out.println();
    }

}
Висновок:
Выводит все числа: 1 2 3 4 5 6 7
Не выводит ни одного числа:
Вывод четных чисел: 2 4 6
Вывод нечетных чисел: 1 3 5 7
Вывод чисел больше 5: 6 7
Зачаровавши над Lambda-виразами можна вивести квадрат кожного елемента списку. Зауважте, що ми використовуємо метод stream(), щоб перетворити звичайний список на потік. Java 8 надає розкішний клас Stream( java.util.stream.Stream). Він містить тонни корисних методів, з якими можна використовувати lambda-вирази. Ми передаємо lambda-вираз x -> x*xу метод map(), який застосовує його до всіх елементів у потоці. Після цього ми використовуємо forEachдля друку всіх елементів списку.
// Старий метод:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer n : list) {
    int x = n * n;
    System.out.println(x);
}
// Новий спосіб:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map((x) -> x*x).forEach(System.out::println);
Даний список потрібно вивести суму квадратів всіх елементів списку. Lambda-вирази дозволяє досягти цього написанням всього одного рядка коду. У цьому прикладі застосовано метод згортки (редукції) reduce(). Ми використовуємо метод map()для зведення квадрат кожного елемента, а потім застосовуємо метод reduce()для згортки всіх елементів в одне число.
// Старий метод:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = 0;
for(Integer n : list) {
    int x = n * n;
    sum = sum + x;
}
System.out.println(sum);
// Новий спосіб:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().map(x -> x*x).reduce((x,y) -> x + y).get();
System.out.println(sum);

Відмінність Lambda-виразів від анонімних класів

Головна відмінність полягає у використанні ключового слова this. Для анонімних класів ключове слово ' this' означає об'єкт анонімного класу, тоді як у lambda-вираженні ' this' позначає об'єкт класу, у якому lambda-вираз використовується. Інша їх відмінність полягає у способі компіляції. Java компілює lambda-вирази з перетворенням їх на private-методи класу. При цьому використовується інструкція invokednamic , що з'явилася в Java 7 для динамічної прив'язки методу. Тал Вайс (Tal Weiss) описав у своєму блозі як Java компілює lambda-вираження в байт-код

Висновок

Марк Рейнхолд (Mark Reinhold - Oracle's Chief Architect), назвав Lambda-вираження найзначнішою зміною в моделі програмування, яка коли-небудь відбувалася - навіть значнішою, ніж дженерики (generics). Має бути правий, т.к. вони дають Java програмістам можливості функціональних мов програмування, на які так давно всі чекали. Поряд із такими нововведеннями як методи віртуального розширення (Virtual extension methods), Lambda-вирази дозволяють писати дуже якісний код. Я сподіваюся, що ця стаття дозволила вам глянути під капот Java 8. Удачі :)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ