Зміни в мові java 8

Лямбда-вирази і зміни в класах інтерфейсів роблять Java 8 новою мовою

Версія Java ™ 8 включає в себе нові важливі мовні особливості, які надають розробникові більш легкі способи для конструювання програм. Т. н. лямбда-вирази (lamba expression) визначають новий синтаксис для блоків вбудованого коду, який забезпечує таку ж гнучкість, як анонімні внутрішні класи, але з істотно зменшеною кількістю шаблонів (boilerplate). Зміни в області інтерфейсів дозволяють додавати їх до існуючих інтерфейсів без шкоди для сумісності з наявним кодом. Дізнайтеся про те, як ці зміни працюють спільно, а також прочитайте супутню статтю Java 8 concurrency basics. в якій описується використання лямбда-виразів з т. зв. потоками Java 8 (stream).

Денис Сосноскі. консультант, Sosnoski Software Solutions, Inc.

Денис Сосноскі (Dennis Sosnoski) - засновник і провідний фахівець консалтингової компанії за технологіями Java - Sosnoski Software Solutions, Inc. що спеціалізується в навчанні і консультуванні з проблем XML і Web-сервісів. Він має більш ніж 30-річний досвід роботи в професійному проектуванні ПО, спеціалізуючись на серверних XML і Java-технологіях. Денис є основним розробником інтегрованої системи з відкритим програмним кодом JiBX XML Data Binding. побудованої на базі технології класів Java і пов'язаної системи Web-сервісів JibxSoap. також як і системи Web-сервісів Apache Axis2. Він також був одним їх експертів при розробці специфікацій JAX-WS 2.0.

Найбільше зміна в Java 8 - це додавання підтримки т. Н. лямбда-виразів. Лямбда-вирази являють собою блоки коду, які може передавати по посиланню. Вони подібні до замикань (closure) в деяких інших мовах програмування: код, який реалізує функцію, опціонально приймає один або більше вхідних параметрів і опціонально повертає значення результату. Замикання визначені в контексті і мають доступ (в разі лямбда-виразів - доступ тільки з читання) до значень з цього контексту.

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

У цій статті ви побачите, як використовувати лямбда-вирази в різних ситуаціях, і дізнаєтеся про пов'язаних з ними розширеннях визначень interface мови Java. У супутньої статті Java 8 concurrency basics з циклу JVM concurrency наведені додаткові приклади роботи з лямбда-виразами, включаючи їх спільне використання з т. Зв. потоками Java 8 (stream).

Поняття про лямбда-виразах

Лямбда-вираз завжди є реалізацією того, що в термінології Java 8 носить назву функціональний інтерфейс. класу interface. визначального єдиний абстрактний метод. Обмеження у вигляді використання не більше одного абстрактного методу важливо, оскільки синтаксис лямбда-вирази не задіє імені методу. Замість цього вираз використовує т. Н. качину типізацію (duck typing - відповідність типів параметра і значення, що повертається, як це робиться в багатьох динамічних мовами), щоб гарантувати, що надане лямбда-вираз сумісно з очікуваним методом інтерфейсу.

Лістинг 1. Порівняння лямбда-вирази і анонімного внутрішнього класу

У лістингу 1 лямбда-вираз використовується в якості заміни для звичайного анонімного внутрішнього класу. Цей різновид звичайного внутрішнього класу вельми широко поширена на практиці, тому лямбда-вирази забезпечують негайний виграш програмістам на Java 8 (В даному випадку для виконання роботи в порівнянні і внутрішній клас, і лямбда-вираз використовує метод, реалізований в класі Name. Якщо код методу compareTo () був вбудований в лямбда-вираз, то вираз буде менш лаконічним).

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

  • Function: Приймає єдиний параметр, повертає результат на основі значення параметра.
  • Predicate: Приймає єдиний параметр, повертає логічний результат на основі значення параметра.
  • BiFunction: Приймає два параметра, повертає результат на основі значення параметра.
  • Supplier: Не приймає параметрів, повертає результат.
  • Consumer: Приймає єдиний параметр, не повертає результату (void)
Лістинг 2. Поєднання предикатів

У лістингу 2 код визначає дві конструкції виду Predicate. одна з яких відповідає імені Sally. а друга відповідає прізвища Queue. виклик методу pred1.or (pred2) створює об'єднаний предикат, визначений за допомогою застосування цих двох предикатів по порядку, з поверненням значення true. якщо будь-який з цих двох предикатів має значення true (з достроковим виходом, як у випадку логічного оператора || в Java). Метод List.removeIf () застосовує цей об'єднаний предикат для видалення відповідних імен зі списку.

В Java 8 визначено багато корисних комбінацій інтерфейсів java.util.function однак ці комбінації не є узгодженими. Всі варіації предикатів (DoublePredicate. IntPredicate. LongPredicate і Predicate ) Визначають одні і ті ж методи для поєднання і модифікації: and (). negate (). or (). Однак варіації примітивів для Function не визначають методів поєднання і модифікації. Якщо у вас є досвід роботи з мовами функціонального програмування, то ви можете порахувати ці відмінності і пропуски надлишковими.

Зміни в класах interface

В Java 8 змінилася структура класів interface (таких як Comparator. Використаний в лістингу 1), частково для спрощення вживання лямбда-виразів. До версії Java 8 інтерфейси дозволяли визначати тільки константи і абстрактні методи, які ви потім повинні були реалізувати. В Java 8 додана можливість визначати в інтерфейсах static -Методи і default -Методи. Static-методи в інтерфейсі - це по суті те ж саме, що static-методи в абстрактному класі. Default-методи більше схожі на методи інтерфейсу в старому стилі, але мають надану реалізацію, яка використовується в тому випадку, якщо розробник не перекриває відповідний default-метод.

Одна важлива особливість default-методів полягає в тому, що вони можуть бути додані до існуючого класу interface без шкоди для сумісності з іншим кодом, який використовує цей інтерфейс (за умови, що ваш існуючий код не використовує те ж саме ім'я методу в інших цілях) . Це дуже потужна можливість; проектувальники Java8 використовували її, щоб "підлаштувати" підтримку лямбда-виразів до багатьох вже існуючих Java-бібліотек. У лістингу 3 показаний відповідний приклад - у формі третього способу сортування імен, доданого до коду в лістингу 1.

Лістинг 3. Зв'язування в ланцюжок компараторов на основі виразів key-extractor

Код в лістингу 3 спочатку демонструє застосування нового static-методу Comparator.comparing () для створення компаратора на основі визначається розробником лямбда-вирази key-extraction (в технічному сенсі лямбда-вираз key-extraction є екземпляром інтерфейсу java.util.function.Function. де тип результуючого компаратора сумісний з присвоєння з T. а тип витягнутого ключа R реалізує інтерфейс Comparable). Цей код також демонструє об'єднання компараторов за допомогою нового default-методу Comparator.thenComparing (). який в лістингу 3 повертає новий компаратор, який здійснює сортування спочатку на прізвище, а потім по імені.

У вас може скластися враження про можливість вбудовування наступної comparator-конструкції:

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

Перша форма додає тип лямбда-параметра до лямбда-виразом: (Name name1) -> name1.lastName. З цією допомогою компілятор здатний зрозуміти іншу частину того, що йому необхідно зробити. Друга форма повідомляє компілятору типи T і R для функціонального інтерфейсу (який в даному випадку реалізований за допомогою лямбда-вирази), передані в метод comparing ().

Можливість простого конструювання компараторов і зв'язування їх в ланцюжок - це дуже корисна особливість Java 8, однак вона реалізується ціною додаткового підвищення рівня складності. У мові Java 7 інтерфейс Comparator визначає два методи (метод compare () і всюдисущий метод equals (). Який гарантовано визначено для кожного об'єкта). Версія Java 8 визначає 18 методів (два вихідних методу, плюс 9 нових static-методів і 7 нових default-методів). Ви побачите, що ця модель масштабної інфляції інтерфейсів для роботи з лямбда-виразами повторюється в значній частині стандартної Java-бібліотеки.

Використання існуючих методів як лямбда-виразів

Лістинг 4. Використання існуючих методів як лямбда-виразів

Код в лістингу 4 робить те ж саме, що код в лістингу 3. але з використанням існуючих методів. Ви можете скористатися наявними в Java 8 синтаксисом посилання на метод ClassName :: methodName. щоб задіяти будь-який метод таким же чином, як якщо б він був лямбда-виразом. Результат буде в точності таким же, як у випадку визначення лямбда-вирази, яке викликає цей метод. Ви можете використовувати посилання на метод для static-методів, для instance-методів будь-якого певного об'єкта або в якості вхідного типу для лямбда-виразів (як в лістингу 4. де методи getFirstName () і getLastName () - це instance-методи для типу порівнюваних Name) і для конструкторів.

Щоб подолати обмеження типу "effectively final", можна використовувати певні обхідні маневри. Наприклад, щоб використовувати тільки поточні значення певних змінних в лямбда-виразі, ви можете додати новий метод, який приймає значення в якості параметрів і повертає лямбда-вираз (в формі відповідного посилання на інтерфейс) із захопленими (captured) значеннями. Якщо ви хочете, щоб лямбда-вираз змінило значення з навколишнього контексту, ви можете обернути відповідне значення в вигляді допускає зміни власника (holder).

Лямбда-вирази - що відбувається за сценою

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

Замість використання для лямбда-виразів окремого файлу класу версія Java 8 спирається на байткод-інструкцію invokedynamic. додану у версії Java 7. Інструкція invokedynamic орієнтована на bootstrap-метод, який, в свою чергу, створює реалізацію лямбда-вирази при першому виклику цього методу. Після цього здійснюється безпосередній виклик повернутої реалізації. Це дозволяє уникнути накладних витрат у вигляді простору, споживаного окремим файлом для кожного класу, і значної частини накладних витрат на етапі виконання, обумовлених завантаженням класу. Точно таким же чином реалізація лямбда-функції відкладається до моменту bootstrap. Bootstrap-код, в даний час генерується Java 8, збирає на етапі виконання новий клас для лямбда-вирази, проте в майбутніх реалізаціях цілком можуть бути використані і інші підходи.

Версія Java 8 включає в себе засоби оптимізації, завдяки яким реалізація лямбда-виразів за допомогою інструкції invokedynamic добре працює в практичних умовах. Більшість інших JVM-мов, в тому числі Scala (2.10.x), використовує для замикань згенеровані компілятором внутрішні класи. Майбутні версії цих мов, ймовірно, перейдуть до підходу на основі інструкції invokedynamic з метою використання засобів оптимізації, пропонованих версією Java 8 (із наступними версіями).

Обмеження лямбда-виразів

Як говорилося на початку статті, лямбда-вирази завжди є реалізаціями якогось певного функціонального інтерфейсу. Лямбда-вирази можна передавати тільки як посилання на інтерфейс; як і в разі інших реалізацій інтерфейсу, лямбда-вираз можна використовувати тільки в якості певного інтерфейсу, для чого воно і було створено. Код в лістингу 5 демонструє обмеження пари ідентичних (за винятком імен) функціональних інтерфейсів. Компілятор Java 8 приймає метод String :: length як лямбда-реалізацію обох цих інтерфейсів. Однак після того як лямбда-вираз визначено в якості примірника першого інтерфейсу, воно вже не може бути використано в якості примірника другого інтерфейсу.

Лістинг 5. Обмеження лямбда-виразів

У коді, показаному в лістингу 5. немає нічого дивного для будь-якої людини, мислячого в термінах Java-інтерфейсів, оскільки саме таким чином завжди працювали Java- інтерфейси (за винятком одного моменту - посилання на методи, які з'явилися у версії Java 8). Однак розробники, які працювали з мовами функціонального програмування, такими як Scala, можуть розцінити це обмеження інтерфейсів що не дуже зрозуміле на інтуїтивному рівні.

У мовах функціонального програмування для визначення змінних використовуються не інтерфейси, а типи функцій. Звичайним явищем для таких мов є робота з т. Зв. функціями вищого порядку - це функції, які передають функції в якості параметрів або повертають функції в якості значень. Результатом є набагато більш гнучкий стиль програмування, ніж в разі лямбда-виразів, включаючи можливість використання функцій в якості стандартних блоків для складання інших функцій. Версія Java 8 не визначає типи функцій, тому ви не зможете складати лямбда-виразів таким чином. Ви зможете складати інтерфейси (див. Лістинг 3), але тільки з кодом, написаним для роботи з певними задіяними інтерфейсами. В одному тільки новий пакет java.util.function 43 інтерфейсу налаштовані спеціально для використання з лямбда-виразами. З урахуванням сотень уже існуючих інтерфейсів можна сказати, що перелік способів, за допомогою яких можна складати інтерфейси, завжди буде строго обмеженим.

Вибір на користь використання інтерфейсів замість додавання в Java типів функцій був навмисним. Це усуває необхідність у внесенні значних змін в Java-бібліотеки, а також дозволяє використовувати лямбда-вирази з існуючими бібліотеками. Зворотний бік цього підходу полягає в тому, що він обмежує Java 8 так званим "інтерфейсним програмуванням" або функціонально-подібним програмуванням - замість справжнього функціонального програмування. Однак наявність великої кількості інших мов, доступних на платформі JVM (в тому числі функціональних), суттєво послаблює це обмеження.

висновок

Лямбда-вирази являють собою досить значне розширення мови Java. Як і інше настільки ж значне розширення - посилання на методи - вони швидко стануть необхідним інструментом для всіх Java-розробників - у міру перенесення їх додатків на платформу Java 8. Лямбда-вирази особливо корисні в поєднанні з т. Зв. потоками Java 8 (stream). У статті JVM concurrency: Java 8 concurrency basics показано спільне використання лямбда-виразів і потоків з метою спрощення паралельного програмування і підвищення продуктивності додатка.

Отримати продукти і технології

  • Приєднуйтесь до спільноти developerWorks. Як звернутись іншими користувачами developerWorks і знайомтеся з орієнтованими на розробників форумами, блогами, групами і вікі-ресурсами.