Java початково повністю об'єктно-орієнтована мова. За винятком примітивних типів, все в Java – це об'єкти. Навіть масиви є об'єктами. Екземпляри кожного класу – об'єкти. Не існує жодної можливості визначити окремо (поза класом –
прим. пер.) яку-небудь функцію. І немає жодної можливості передати метод як аргумент або повернути тіло методу як результат іншого методу. Все так. Але так було до Java 8.
![Lambda-вирази на прикладах - 1]()
З часів старого доброго Swing, треба було писати анонімні класи, коли потрібно було передати якусь функціональність у який-небудь метод. Наприклад, так виглядало додавання обробника подій:
someObject.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
// Реалізація обробника подій...
}
});
Тут ми хочемо додати певний код у слухач подій від миші. Ми визначили анонімний клас
MouseAdapter і одразу створили об'єкт з нього. Цим способом ми передали додаткову функціональність у метод
addMouseListener.
Коротше кажучи, не так-то просто передати простий метод (функціональність) у Java через аргументи. Це обмеження змусило розробників Java 8 додати в специфікацію мови таку можливість як Lambda-вирази.
Навіщо Java Lambda-вирази?
З самого початку, мова Java особливо не розвивалася, якщо не враховувати такі речі як анотації (Annotations), дженерики (Generics) тощо. У першу чергу, Java завжди залишався об'єктно-орієнтованим. Після роботи з функціональними мовами, такими як JavaScript, можна зрозуміти наскільки Java суворо об'єктно-орієнтована і суворо типізована. Функції у Java не потрібні. Самі по собі вони не зустрічаються у світі Java.
У функціональних мовах програмування на перше місце виходять функції. Вони існують самі по собі. Можна присвоювати їх змінним і передавати через аргументи іншим функціям. JavaScript – один із найкращих прикладів функціональних мов програмування. У просторах Інтернету можна знайти добрі статті, у яких детально описані переваги JavaScript як функціональної мови. Функціональні мови мають у своєму арсеналі такі потужні інструменти як замикання (Closure), які забезпечують низку переваг порівняно з традиційними способами написання додатків. Замикання – це функція з прив'язаним до неї середовищем — таблицею, що зберігає посилання на всі нелокальні змінні функції. У 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();
}
Помилка
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-методи класу. При цьому використовується інструкція
invokedynamic, яка з'явилася у Java 7 для динамічного зв’язування методу. Тал Вайс (Tal Weiss) описав у своєму блозі, як Java компілює lambda-вирази у байт-код.
Висновок
Марк Рейнхолд (Mark Reinhold - Oracle’s Chief Architect), назвав Lambda-вирази найзначнішим зміною в моделі програмування, яке коли-небудь відбувалося — навіть більш значущим, ніж дженеріки (generics). Напевно, він має рацію, бо вони дають Java програмістам можливості функціональних мов програмування, яких так давно всі чекали. Поряд із такими нововведеннями, як методи віртуального розширення (Virtual extension methods), Lambda-вирази дозволяють писати дуже якісний код.
Я сподіваюся, що ця стаття дозволила вам зазирнути під капот Java 8. Удачі :)
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ