Лямбда-вирази в Java 8

    У новій версії Java 8 нарешті з'явилися довгоочікувані лямбда-вирази. Можливо, це найважливіша нова можливість останньої версії; вони дозволяють писати швидше і роблять код більш ясним, а також відкривають двері у світ функціонального програмування. У цій статті я розповім, як це працює.
 
Java замислювалася як об'єктно-орієнтована мова в 90-ті роки, коли об'єктно-орієнтоване програмування було головною парадигмою в розробці додатків. Задовго до цього було об'єктно-орієнтоване програмування, були функціональні мови програмування, такі, як Lisp і Scheme, але їх переваги не були оцінені за межами академічного середовища. Останнім часом функціональне програмування сильно виросло в значимості, тому що воно добре підходить для паралельного програмування та програмування, заснованого на подіях («reactive»). Це не означає, що об'єктна орієнтованість — погано. Навпаки, замість цього, виграшна стратегія — змішувати об'єктно-орієнтоване програмування і функціональне. Це має сенс, навіть якщо вам не потрібна паралельність. Наприклад, бібліотеки колекцій можуть отримати потужне API, якщо мова має зручний синтаксис для функціональних виразів.
 
Головним поліпшенням в Java 8 є додавання підтримки функціональних програмних конструкцій до його об'єктно-орієнтованої основі. У цій статті я продемонструю основний синтаксис і як використовувати його в декількох важливих контекстах. Ключові моменти поняття лямбда:
 
 
     
  • Лямбда-вираз є блоком коду з параметрами.
  •  
  • Використовуйте лямбда-вираз, коли хочете виконати блок коду в пізніший момент часу.
  •  
  • Лямбда-вирази можуть бути перетворені в функціональні інтерфейси.
  •  
  • Лямбда-вирази мають доступ до final змінним з охоплює області видимості.
  •  
  • Посилання на метод і конструктор посилаються на методи або конструктори без їх виклику.
  •  
  • Тепер ви можете додати методи за замовчуванням і статичні методи до інтерфейсів, які забезпечують конкретні реалізації.
  •  
  • Ви повинні дозволяти будь-які конфлікти між методами за замовчуванням з декількох інтерфейсів.
  •  
 

Навіщо потрібні лямбда?

Лямбда-вираз являє собою блок коду, який можна передати в інше місце, тому він може бути виконаний пізніше, один або кілька разів. Перш ніж заглиблюватися в синтаксис (і цікава назва), давайте зробимо крок назад і побачимо, де ви використовували аналогічні блоки коду в Java до цього.
 
Якщо ви хочете виконати дії в окремому потоці, ви ставите їх в метод
run
з
Runnable
, ось так:
 
class MyRunner implements Runnable {
    public void run() {
        for (int i = 0; i < 1000; i++)
           doWork();
        }
        ...
}

Потім, коли ви хочете виконати цей код, ви створюєте екземпляр класу
MyRunner
. Ви можете помістити екземпляр в пул потоків, або поступити простіше і запустити новий потік:
 
MyRunner r = new MyRunner();
new Thread®.start();

Ключовим моментом є те, що метод
run
містить код, який потрібно виконати в окремому потоці.
 
Розглянемо сортування з використанням користувальницького компаратора. Якщо ви хочете відсортувати рядки по довжині, а не за замовчуванням, ви можете передати об'єкт
Comparator
в метод
sort
:
 
class LengthStringComparator implements Comparator<String> {
    public int compare(String firstStr, String secondStr) {
        return Integer.compare(firstStr.length(),secondStr.length());
    }
}
   
Arrays.sort(strings, new LengthStringComparator ());

Метод
sort
все так же викликає метод
compare
, переставляючи елементи, якщо вони стоять не по порядку, поки маса не буде відсортований. Ви надаєте методу
sort
фрагмент коду, необхідний для порівняння елементів, і цей код вбудовується в іншу частину логіки сортування, яку вам, ймовірно, не потрібно перевизначати. Зверніть увагу, що виклик
Integer.compare (х, у)
повертає нуль, якщо х і у рівні, негативне число, якщо х < у, і позитивне число, якщо х> у. Цей статичний метод був доданий в Java 7. Ви не повинні обчислювати х — y, щоб порівнювати х і у, тому що розрахунок може викликати переповнення для великих операндів протилежного знака.
 
В якості іншого прикладу відкладеного виконання, розглянемо коллбек для кнопки. Ви ставите дію зворотного виклику в метод класу, що реалізовує інтерфейс слухача, створюєте екземпляр, і реєструєте екземпляр. Це настільки поширений сценарій, що багато програмісти використовують синтаксис «анонімний примірник анонімного класу»:
 
button.setOnAction(new EventHandler<ActionEvent>() {
    public void handle(ActionEvent event) {
        System.out.println("The button has been clicked!");
    }
});

Тут важливий код всередині методу
handle
. Цей код виконується всякий раз, коли натискається кнопка.
 
Оскільки Java 8 позиціонує JavaFX як наступника інструментарію Swing GUI, я використовую JavaFX в цих прикладах. Деталі не мають значення. У кожній бібліотеці для користувача інтерфейсу, будь то Swing, JavaFX або Android, ви передаєте кнопці деякий код, який ви хочете запустити, коли кнопка натиснута.
 
У всіх трьох прикладах ви бачили один і той же підхід. Блок коду комусь передавався — пулу потоків, методу сортування або кнопці. Цей код викликався через якийсь час.
 
Досі передача коду не була простою в Java. Ви не могли просто передати блоки коду куди завгодно. Java є об'єктно-орієнтованою мовою, так що ви повинні були створити об'єкт, що належить до класу, у якого є метод з потрібним кодом.
В інших мовах можна працювати з блоками коду безпосередньо. Проектувальники Java пручалися додаванню цієї функції протягом тривалого часу. Зрештою, велика сила Java в її простоті і послідовності. Мова може стати вкрай безладним, якщо буде включати в себе всі функції, які дають трохи більше короткий код. Проте, в тих інших мовах, це не просто легше породжувати потік або зареєструвати обробник кнопки клацання; багато їх API простіше, більш послідовні і потужні. У Java, можна було б написати подібні інтерфейси, які беруть об'єкти класів, що реалізують певну функцію, але такі API було б незручно використовувати.
 
Останнім часом питання було не в тому, розширювати Java для функціонального програмування чи ні, а як це зробити. Знадобилося кілька років експериментів, перш ніж з'ясувалося, що це добре підходить для Java. У наступному розділі ви побачите, як можна працювати з блоками коду в Java 8.
 
 

Синтаксис лямбда-виразів

Розглянемо попередній приклад сортування ще раз. Ми передаємо код, який перевіряє, який рядок коротше. Ми обчислюємо
 
Integer.compare(firstStr.length(), secondStr.length())

Що таке
firstStr
і
secondStr
? Вони обидва рядки! Java є строго універсальна мова, і ми повинні вказати типи:
 
(String firstStr, String secondStr)
     -> Integer.compare(firstStr.length(),secondStr.length())

 
Ви тільки що бачили ваше перше лямбда-вираз! Такий вираз є просто блоком коду разом зі специфікацією будь-яких змінних, які повинні бути передані в код.
 
Чому така назва? Багато років тому, коли ще не було ніяких комп'ютерів, логік Алонзо Черч хотів формалізувати, що значить для математичної функції бути ефективно обчислюється. (Цікаво, що є функції, які, як відомо, існують, але ніхто не знає, як обчислити їх значення.) Він використовував грецьку букву лямбда (λ), щоб відзначити параметри. Якби він знав про Java API, він написав би щось не сильно схоже на те, що ви бачили, швидше за все.
 
Чому буква λ? Хіба Черч використовував всі букви алфавіту? Насправді, поважний праця Principia Mathematica використовує символ для позначення вільних змінних, які надихнули Черча використовувати заголовну лямбда (Λ) для параметрів. Але, врешті-решт, він переключився на рядкової варіант літери. З тих пір, вираз із змінними параметрами було названо «лямбда-вираз».
 
Ви тільки що бачили одну форму лямбда-виразів в Java: параметри, стрілку -> і вираз. Якщо код виконує обчислення, яке не вписується в один вираз, запишіть його так само, як ви б написали метод: ув'язнений в {} і з явними виразами
return
. Наприклад,
 
(String firstStr, String secondStr) -> {
    if (firstStr.length() < secondStr.length()) return -1;
    else if (firstStr.length() > secondStr.length()) return 1;
    else return 0;
}

Якщо лямбда-вираз не має параметрів, ви все одно ставите порожні дужки, так само, як з методом без параметрів:
 
() -> { for (int i = 0; i < 1000; i++) doWork(); }

Якщо типи параметрів лямбда-вирази можна вивести, можна опустити їх. Наприклад,
 
Comparator<String> comp
    = (firstStr, secondStr) // Same as (String firstStr, String secondStr)
        -> Integer.compare(firstStr.length(),secondStr.length());

Тут компілятор може зробити висновок, що
firstStr
і
secondStr
повинні бути рядками, бо лямбда-вираз присвоюється компаратору рядків. (Ми подивимося на це привласнення уважніше пізніше.)
 
Якщо метод має один параметр виведеного типу, ви можете навіть опустити дужки:
 
EventHandler<ActionEvent> listener = event ->
    System.out.println("The button has been clicked!");
        // Instead of (event) -> or (ActionEvent event) ->

 
Ви можете додати анотації або модифікатор
final
до параметрів лямбда таким же чином, як і для параметрів методу:
 
(final String var) -> ...
(@NonNull String var) -> ...

Ви ніколи не вказуєте тип результату лямбда-вирази. Це завжди з'ясовується з контексту. Наприклад, вираз
 
(String firstStr, String secondStr) -> Integer.compare(firstStr.length(), secondStr.length())

може бути використано в контексті, де очікується результат типу
int
.
 
Зверніть увагу, що лямбда-вираз не може повертати значення в якихось гілках, а в інших не повертати. Наприклад,
(int x) -> { if (x <= 1) return -1; }
є неприпустимим.
 
 

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

Як ми вже обговорювали, в Java є багато існуючих інтерфейсів, які інкапсулюють блоки коду, такі, як
Runnable
або
Comparator
. Лямбда-вирази мають зворотну сумісність з цими інтерфейсами.
 
Ви можете поставити лямбда-вираз всякий раз, коли очікується об'єкт інтерфейсу з одним абстрактним методом. Такий інтерфейс називається функціональним інтерфейсом.
 
Ви можете здивуватися, чому функціональний інтерфейс повинен мати єдиний абстрактний метод. Хіба не всі методи в інтерфейсі абстрактні? Насправді, завжди було можливо для інтерфейсу перевизначити методи класу
Object
, наприклад,
toString
або
clone
, і ці оголошення не роблять методи абстрактними. (Деякі інтерфейси в Java API перевизначають методи
Object
, щоб приєднати javadoc-коментарі. Подивіться Comparator API для прикладу.) Що ще більш важливо, як ви незабаром побачите, в Java 8 інтерфейси можуть оголошувати неабстрактне методи.
 
Щоб продемонструвати перетворення в функціональний інтерфейс, розглянемо метод
Arrays.sort
. Його другий параметр потрібно примірник
Comparator
, інтерфейсу з єдиним методом. Просто надайте лямбду:
 
Arrays.sort(strs,
    (firstStr, secondStr) -> Integer.compare(firstStr.length(), secondStr.length()));

За лаштунками, метод
Arrays.sort
отримує об'єкт деякого класу, що реалізовує
Comparator <String>
. Виклик методу
compare
на цьому об'єкті виконує тіло лямбда-вирази. Управління цими об'єктами і класами повністю залежить від реалізації, і це може бути щось набагато більш ефективне, ніж використання традиційних внутрішніх класів. Найкраще думати про лямбда-вираженні як про функції, а не про об'єкт, і визнати, що він може бути переданий функціональному інтерфейсу.
 
Це перетворення в інтерфейси — це те, що робить лямбда-вирази настільки потужними. Синтаксис короткий і простий. Ось ще один приклад:
 
button.setOnAction(event ->
    System.out.println("The button has been clicked!"));

Цей код дуже легко читати.
 
Справді, перетворення в функціональний інтерфейс — це єдине, що ви можете зробити з лямбда-виразом в Java. В інших мовах програмування, які підтримують функціональні літерали, можна оголосити типи функцій, таких як
(String, String) -> int
, оголошувати змінні цих типів, і використовувати змінні для збереження функціональних виразів. У Java ви не можете навіть привласнити лямбда-вираз змінної типу
Object
, тому
Object
не є функціональним інтерфейсом. Проектувальники Java вирішили строго дотримуватися знайомої концепції інтерфейсів, а не додавати типи функцій в мову.
 
Java API визначає кілька універсальних функціональних інтерфейсів в пакеті java.util.function. Один з інтерфейсів,
BiFunction <T, U, R>
, описує функції з типами Т і U і типом значення, що повертається R. Ви можете зберегти вашу лямбду порівняння рядків у змінній цього типу:
 
BiFunction<String, String, Integer> compareFunc
    = (firstStr, secondStr) -> Integer.compare(firstStr.length(), secondStr.length());

Тим не менш, це не допоможе вам з сортуванням. Не існує методу
Arrays.sort
, який приймає
BiFunction
. Якщо ви використовували функціональний мова програмування і раніше, ви можете знайти це цікавим. Але для Java програмістів це досить природно. Такий інтерфейс, як
Comparator
, має конкретну мету, а не просто метод із заданим параметром і повертаним типом. Java 8 зберігає цей стиль. Якщо ви хочете зробити щось з лямбда-виразами, ви все ще повинні розуміти призначення цього виразу, і мати конкретний функціональний інтерфейс для цього.
 
Інтерфейси з java.util.function використовуються в декількох Java 8 інтерфейсах API, і ви, ймовірно, побачите їх в інших місцях в майбутньому. Але майте на увазі, що ви можете однаково добре перетворити лямбда-вираз в функціональний інтерфейс, який є частиною будь-якого API, який ви використовуєте сьогодні. Крім того, ви можете помітити будь-який функціональний інтерфейс за допомогою анотації
@FunctionalInterface
. Це має дві переваги. Компілятор перевіряє, що анотована сутність являє собою інтерфейс з одним абстрактним методом. І сторінка Javadoc включає в себе твердження, що ваш інтерфейс є функціональним інтерфейсом. Ви не зобов'язані використовувати анотацію. Будь інтерфейс з одним абстрактним методом є, за визначенням, функціональним інтерфейсом. Але використання анотації
@FunctionalInterface
— це хороша ідея.
 
Нарешті, зауважимо, що checked виключення можуть виникнути при перетворенні лямбда в екземпляр функціонального інтерфейсу. Якщо тіло лямбда-вирази може кинути checked виняток, це виняток має бути оголошено в абстрактному методі цільового інтерфейсу. Наприклад, наступне було б помилкою:
 
Runnable sleepingRunner = () -> { System.out.println("…"); Thread.sleep(1000); };
    // Error: Thread.sleep can throw a checkedInterruptedException

Оскільки
Runnable.run
не може кинути виняток, це привласнення є некоректним. Щоб виправити помилку, у вас є два варіанти. Ви можете спіймати виняток у тілі лямбда-вирази. Або ви можете присвоїти лямбду інтерфейсу, один абстрактний метод якого може кинути виняток. Наприклад, метод
call
з інтерфейсу
Callable
може кинути будь виняток. Таким чином, ви можете присвоїти лямбду
Callable <void>
(якщо додати
return null
).
 
 

Посилання на методи

Іноді вже є метод, який здійснює саме ті дії, які ви хотіли б передати в інше місце. Наприклад, припустимо, що ви просто хочете роздрукувати об'єкт події
event
, коли кнопка натиснута. Звичайно, ви могли б викликати
 
button.setOnAction(event -> System.out.println(event));

Було б краще, якби ви могли просто передати метод
println
в метод
setOnAction
. Приблизно так:
 
button.setOnAction(System.out::println);

Вираз
System.out::println
є посиланням на метод, який еквівалентний лямбда-виразу
x -> System.out.println(x)
.
 
В якості іншого прикладу, припустимо, що ви хочете відсортувати рядки незалежно від регістру літер. Ви можете написати такий код:
 
Arrays.sort(strs, String::compareToIgnoreCase)

Як ви можете бачити з цих прикладів оператор :: відокремлює ім'я методу від імені об'єкта або класу. Є три основні варіанти:
 
 
     
  • object :: instanceMethod
  •  
  • Class :: staticMethod
  •  
  • Class :: instanceMethod
  •  
 
У перших двох випадках посилання на метод еквівалентна лямбда-виразу, яке надає параметри методу. Як уже згадувалося,
System.out::println
еквівалентно
x -> System.out.println(x)
. Точно так само,
Math::pow
еквівалентно
(x, y) -> Math.pow(x, y)
. У третьому випадку перший параметр стає цільовим об'єктом методу. Наприклад,
String::compareToIgnoreCase
— це те ж саме, що і
(x, y) -> x.compareToIgnoreCase(y)
.
 
За наявності декількох перевантажених методів з тим же ім'ям компілятор спробує знайти з контексту, який ви маєте на увазі. Наприклад, є два варіанти методу
Math.max
, один для
int
і один для
double
. Який з них буде викликаний, залежить від параметрів методу функціонального інтерфейсу, до якого
Math.max
перетвориться. Так само, як і лямбда-вирази, посилання на методи не живуть в ізоляції. Вони завжди перетворюються в екземпляри функціональних інтерфейсів.
 
Ви можете захопити параметр
this
на засланні на метод. Наприклад,
this::equals
— це те ж, що і
x -> this.equals(x)
. Можна також використовувати
super
. Вираз
super::instanceMethod
використовує
this
в якості мети і викликає версію даного методу суперкласу. Ось штучний приклад, який демонструє механізм:
 
class Speaker {
    public void speak() {
        System.out.println("Hello, world!");
    }
}
   
class Concurrent Speaker extends Speaker {
    public void speak() {
    Thread t = new Thread(super::speak);
        t.start();
    }
}

При запуску потоку викликається його
Runnable
, і
super::speak
виконується, викликаючи
speak
суперкласу. (Зверніть увагу, що у внутрішньому класі ви можете захопити це посилання з класу додатки, як
EnclosingClass.this::method
або
EnclosingClass.super::method
.)
 
 

Посилання на конструктор

Посилання на конструктор такі ж, як посилання на метод, за винятком того, що ім'ям методу є
new
. Наприклад,
Button::new
є посиланням на конструктор класу
Button
. На який саме конструктор? Це залежить від контексту. Припустимо, у вас є список рядків. Потім, ви можете перетворити його в масив кнопок, шляхом виклику конструктора для кожного з рядків, за допомогою наступного виклику:
 
List<String> strs = ...;
Stream<Button> stream = strs.stream().map(Button::new);
List<Button> buttons = stream.collect(Collectors.toList());

Детальна інформація про
stream
,
map
і методах
collect
виходить за рамки цієї статті. На даний момент, важливо те, що метод
map
викликає конструктор
Button(String)
для кожного елемента списку. Є кілька конструкторів
Button
, але компілятор вибирає той, що з параметром строкового типу, тому що з контексту ясно, що конструктор викликається з рядком.
 
Ви можете сформувати посилання на конструктори з типом масивів. Наприклад,
int[]::new
є посиланням на конструктор з одним параметром: завдовжки масиву. Це рівносильно лямбда-виразу
x -> new int[x]
.
 
Посилання на конструктори масиву корисні для подолання обмежень Java. Неможливо створити масив універсального типу T. Вираз
new T[n]
є помилкою, так як воно буде замінено
new Object[n]
. Це є проблемою для авторів бібліотек. Наприклад, припустимо, ми хочемо мати масив кнопок. Інтерфейс
stream
має метод
toArray
, що повертає масив
Object
:
Object[] buttons = stream.toArray();

Але це незадовільно. Користувач хоче масив кнопок, а не об'єктів. Бібліотека потоків вирішує цю проблему за рахунок посилань на конструктори. Передайте
Button[]::new
методу
toArray
:
Button[] buttons = stream.toArray(Button[]::new);

Метод
toArray
викликає цей конструктор для отримання масиву потрібного типу. Потім він заповнює і повертає масив.

Область дії змінної

Часто ви хотіли б мати можливість отримати доступ до змінних з охоплює методу або класу в лямбда-вираженні. Розглянемо наступний приклад:
public static void repeatText(String text, int count) {
     Runnable r = () -> {
         for (int i = 0; i < count; i++) {
             System.out.println(text);
             Thread.yield();
         }
    };
    new Thread®.start();
}

Розглянемо виклик:
repeatText("Hi!", 2000); // Prints Hi 2000 times in a separate thread

Тепер подивимося на змінні
count
і
text
всередині лямбда-вирази. Зверніть увагу, що ці змінні не визначені в лямбда-вираженні. Замість цього, вони є параметрами методу
repeatText
.

Якщо подумати гарненько, то не очевидно, що тут відбувається. Код лямбда-вирази може виконатися набагато пізніше виклику
repeatText
і змінні параметрів вже будуть втрачені. Як же змінні
text
і
count
залишаються доступними?

Щоб зрозуміти, що відбувається, ми повинні уточнити наші уявлення про лямбда-виразах. Лямбда-вираз має три компоненти:

  • Блок коду
  • Параметри
  • Значення для вільних змінних; тобто, змінних, які не є параметрами і не визначені в коді
У нашому прикладі лямбда-вираз має дві вільних змінних,
text
і
count
. Структура даних, що представляє лямбда-вираз, повинна зберігати значення для цих змінних, в нашому випадку, «Hi!» І 2000. Будемо говорити, що ці значення були захоплені лямбда-виразом. (Це деталь реалізації, як це робиться. Наприклад, можна перетворити лямбда-вираз в об'єкт з одним методом, так що значення вільних змінних копіюються в змінні екземпляра цього об'єкта.)

Технічним терміном для блоку коду разом зі значеннями вільних змінних є замикання. Якщо хтось зловтішається, що їхня мова підтримує замикання, будьте впевнені, що Java також їх підтримує. У Java лямбда-вирази є замиканнями. Насправді, внутрішні класи були замиканнями весь цей час. Java 8 надає нам замикання з привабливим синтаксисом.

Як ви бачили, лямбда-вираз може захопити значення змінної в охоплює області. У Java, щоб переконатися, що захопили значення коректно, є важливе обмеження. У лямбда-вираженні можна посилатися тільки на змінні, значення яких не змінюються. Наприклад, наступний код є неправильним:
public static void repeatText(String text, int count) {
    Runnable r = () -> {
        while (count > 0) {
            count--; // Error: Can't mutate captured variable
            System.out.println(text);
            Thread.yield();
       }
    };
    new Thread®.start();
}

Існує причина для цього обмеження. Змінюються змінні в лямбда-вирази не потокобезпечна. Розглянемо послідовність паралельних завдань, кожна з яких оновлює загальний лічильник.
int matchCount = 0;
for (Path p : files)
    new Thread(() -> { if (p has some property) matchCount++; }).start();
    // Illegal to mutate matchCount

Якби цей код був правомірним, це було б не дуже добре. Прирощення
matchCount++
неатомарно, і немає ніякого способу дізнатися, що станеться, якщо декілька потоків виконають цей код одночасно.

Внутрішні класи можуть також захоплювати значення з охоплює області. До Java 8 внутрішні класи могли мати доступ тільки до локальних
final
змінним. Це правило тепер ослаблено для відповідності правилу для лямбда-виразів. Внутрішній клас може отримати доступ до будь ефективно
final
локальної змінної; тобто, до будь-якої змінної, значення якої не змінюється.

Не розраховуйте, що компілятор виявить всі паралельні помилки доступу. Заборона на модифікацію має місце тільки для локальних змінних. Якщо
matchCount
— мінлива примірника чи статична змінна з охоплює класу, то ніякої помилки не буде, хоча результат так само не визначений.

Крім того, абсолютно законно змінювати розділяється об'єкт, хоч це і не дуже надійно. Наприклад,
List<Path> matchedObjs = new ArrayList<>();
for (Path p : files)
    new Thread(() -> { if (p has some property) matchedObjs.add(p); }).start();
    // Legal to mutate matchedObjs, but unsafe

Зверніть увагу, що змінна
matchedObjs
ефективно
final
. (Ефективно
final
змінна є змінною, якої ніколи не присвоюється нове значення після її ініціалізації.) У нашому випадку
matchedObjs
завжди посилається на один і той же об'єкт
ArrayList
. Однак об'єкт змінюється, і це не потокобезпечна. Якщо кілька потоків викличуть метод
add
, результат може бути непередбачуваним.

Існують безпечні механізми підрахунку та збору значень одночасно. Ви можете використовувати потоки для збору значень з певними властивостями. В інших ситуаціях ви можете використовувати потокобезпечна лічильники та колекції.

Як і з внутрішніми класами, є обхідні рішення, яке дозволяє лямбда-виразу оновити лічильник в локальній охоплює області видимості. Використовуйте масив довжиною 1, на зразок цього:
int[] counts = new int[1];
button.setOnAction(event -> counts[0]++);

Звичайно, така код який потокобезпечна. Для зворотного виклику кнопки це не має значення, але в цілому, ви повинні подумати двічі, перш ніж використовувати цей трюк.

Тіло лямбда-вирази має ту ж область видимості, що і вкладений блок. Тут застосовуються ті ж самі правила для конфліктів імен. Не можна оголосити параметр або локальну змінну в лямбда, які мають те ж ім'я, що і локальна змінна.
Path first = Paths.get("/usr/local");
Comparator<String> comp =
    (first, second) -> Integer.compare(first.length(), second.length());
    // Error: Variable first already defined

Усередині методу ви не можете мати дві локальні змінні з тим же ім'ям. Таким чином, ви не можете оголосити такі змінні також і в лямбда-вираженні. При використанні ключового слова
this
в лямбда-вираженні ви посилаєтеся на параметр
this
методу, який створює лямбду. Розглянемо, наприклад, наступний код
public class Application() {
    public void doWork() {
        Runnable r = () -> { ...; System.out.println(this.toString()); ... };
        ...
    }
}

Вираз
this.toString()
викликає метод
toString
об'єкта
Application
, а не примірника
Runnable
. Немає нічого особливого у використанні
this
в лямбда-вираженні. Область видимості лямбда-вирази вкладена всередину методу
doWork
, і
this
має таке ж значення в будь-якій точці цього методу.

Методи за замовчуванням

Багато мови програмування інтегрують функціональні вирази з їх бібліотеками колекцій. Це часто призводить до коду, який коротше і простіше для розуміння, ніж еквівалент, що використовує цикли. Наприклад, розглянемо цикл:
for (int i = 0; i < strList.size(); i++)
    System.out.println(strList.get(i));

Існує кращий спосіб. Автори бібліотеки можуть надати метод
forEach
, який застосовує функцію до кожного елементу. Тоді ви можете просто викликати
strList.forEach(System.out::println);

Це нормально, якщо бібліотека колекцій була розроблена з нуля. Але бібліотека колекцій Java була розроблена багато років тому, і є проблема. Якщо інтерфейс
Collection
отримує нові методи, такі як
forEach
, то кожна програма, яка визначає свій власний клас, який реалізує
Collection
, зламається, поки теж не реалізує цей метод. Це просто неприпустимо в Java.

Проектувальники Java вирішили цю проблему раз і назавжди, дозволивши створювати методи інтерфейсу з конкретною реалізацією (так звані методи за замовчуванням). Ці методи можуть бути безпечно додані до існуючих інтерфейсів. У цьому розділі ми розглянемо методи за замовчуванням в деталях. У Java 8 метод
forEach
був доданий до інтерфейсу
Iterable
, суперінтерфейсу
Collection
, використовуючи механізм, який я опишу тут.

Розглянемо такий інтерфейс:
interface Person {
    long getId();
    default String getFirstName() { return "Jack"; }
}

Інтерфейс має два методи:
getId
, це абстрактний метод, і метод за замовчуванням
getFirstName
. Конкретний клас, який реалізує інтерфейс
Person
, повинен, звичайно, надати реалізацію
getId
, але він може вибрати, залишити реалізацію
getFirstName
або перевизначити її.

Методи за замовчуванням кладуть кінець класичного паттерну надання інтерфейсу і абстрактного класу, який реалізує всі або майже всі з його методів, такі, як
Collection/AbstractCollection
або
WindowListener/WindowAdapter
. Тепер ви можете просто реалізувати методи в інтерфейсі.
Що відбудеться, якщо точно такий же метод визначений як метод за замовчуванням в одному інтерфейсі, а потім знову в якості методу суперкласу або іншого інтерфейсу? Мови типу Скала і C + + мають складні правила вирішення таких неоднозначностей. На щастя, правила в Java набагато простіше. Ось вони:

  1. Батьківські класи виграють. Якщо суперклас надає конкретний метод, методи за замовчуванням з тим же ім'ям і типами параметрів просто ігноруються.
  2. Інтерфейси стикаються. Якщо супер інтерфейс надає метод за замовчуванням, а інший інтерфейс поставляє метод з тим же ім'ям і типами параметрів (за замовчуванням чи ні), то ви повинні вирішити конфлікт шляхом перевизначення цього методу.
Давайте подивимося на друге правило. Розглянемо ще один інтерфейс з методом
getFirstName
:
interface Naming {
    default String getFirstName() { return getClass().getName() + "_" + hashCode(); }
}

Що станеться, якщо ви створите клас, який реалізує обидва?
class Student implements Person, Naming {
    ...
}

Клас успадковує дві суперечливі версії методу
getFirstName
, що надаються інтерфейсами
Person
і
Naming
. Замість вибору того чи іншого методу, компілятор Java повідомляє про помилку і залишає програмісту дозвіл неоднозначності. Просто надайте метод
getFirstName
в класі
Student
. У цьому методі ви можете вибрати один з двох конфліктуючих методів, як показано нижче:
class Student implements Person, Naming {
    public String getFirstName() { returnPerson.super.getFirstName(); }
        ...
}

Тепер припустимо, що
Naming
інтерфейс не містить реалізацію за замовчуванням для
getFirstName
:
interface Naming {
    String getFirstName();
}

Чи може клас
Student
успадкувати метод за замовчуванням з інтерфейсу
Person
? Це могло б бути розумним, але проектувальники Java прийняли рішення на користь однаковості. Це не має значення, як два інтерфейси конфліктують. Якщо хоча б один інтерфейс забезпечує реалізацію, компілятор повідомляє про помилку, і програміст повинен усунути неоднозначність.

Якщо жоден інтерфейс не забезпечує реалізацію за замовчуванням для загального методу, то ми знаходимося в пре-Java 8 ситуації і немає ніякого конфлікту. У класу реалізації є дві можливості: реалізувати метод або залишити його нереалізованим. В останньому випадку клас сам є абстрактним.

Ми щойно обговорили конфлікти імен між двома інтерфейсами. Тепер розглянемо клас, який розширює суперклас і реалізує інтерфейс, наслідуючи той же метод від обох. Наприклад, припустимо, що
Person
є класом і
Student
визначається як:
class Student extends Person implements Naming { ... }

У цьому випадку тільки метод суперкласу має значення, і будь-який метод за замовчуванням з інтерфейсу просто ігнорується. У нашому прикладі
Student
успадковує метод
getFirstName
від
Person
, і немає ніякої різниці, чи забезпечує інтерфейс
Named
реалізацію за замовчуванням для
getFirstName
чи ні. Це правило «клас перемагає» в дії. Правило «клас перемагає» забезпечує сумісність з Java 7. Якщо ви додаєте методи за замовчуванням до інтерфейсу, це не має ніякого впливу на код, який працював до того, як з'явилися методи за замовчуванням. Але майте на увазі: ви не маєте права створювати метод за замовчуванням, який перевизначає один з методів класу
Object
. Наприклад, ви не можете визначити метод за замовчуванням для
toString
або
equals
, хоча це могло б бути зручним для таких інтерфейсів, як
List
. Як наслідок правила про перемогу класів, такий метод ніколи не може виграти у
Object.toString
або
Object.equals
.

Висновок

У цій статті були описані лямбда-вирази в якості єдиного найбільшого поновлення в моделі програмування, коли-небудь колишнього в Java, — більшого, ніж навіть дженерики. А чому ні? Це відкриває програмісту Java можливості, яких йому не вистачало в порівнянні з іншими функціональними мовами програмування. Поряд з іншими фичами, такими, як методи за замовчуванням, лямбда можуть бути використані для написання дійсно гарного коду. Сподіваюся, ця стаття дала уявлення про те, що для нас приготували в Java 8.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.