Java спочатку повністю об'єктно-орієнтована мова. За винятком примітивних типів, все в Java – це об'єкти. Навіть масиви є об'єктами. Примірники кожного класу – об'єкти. Не існує жодної можливості визначити окремо (поза класом – прим. перекл. ) якусь функцію. І немає жодної можливості передати метод як аргумент або повернути тіло методу як наслідок іншого методу. Все так. Але так було до Java 8. З часів старого доброго 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-вираження в байт-код
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ