Java 8. Новые возможности
Java SE 8 представляет самые глубокие изменения в языке Java и библиотеках с момента возникновения. Хорошей новостью является то, что эти изменения позволят Вам писать программы более легко. Java 8 предлагает новый интерфейс прикладного программирования — API (Streams API), который поддерживает множество параллельных операций обработки данных и помогает вам думать о них в представлениях похожих на запросы к базе данных. Изменение программирования в Java на функциональный стиль поможет Вам писать код, который является более кратким и выразительным, и (во многих случаях) готовым к использованию в параллельной среде. В этом курсе, вы узнаете, как, казалось бы, незначительные изменения, которые вводят лямбда-выражения делают это возможным.
После изучения курса вы сможете:
- Применять лямбда-выражения (lambda expressions).
- Использовать методы интерфейсов по умолчанию (default interface methods).
- Включать в код ссылки на методы (method references) и повторяемые аннотации (repeatable annotations).
- Встраивать новые конкурентные функции лямбда-выражений.
- Работать с новыми возможностями API для работы с датами: Java Date и Time API.
- Использовать среду Nashorn JavaScript.
Содержание курса
Модуль 1: Введение в лямбда-выражения.
- Цель использования анонимного внутреннего класса.
- Недостатки анонимных внутренних классов.
- Компоненты лямбда-выражения.
- Функциональный интерфейс.
- Создание программ, использующих лямбда-выражения.
Модуль 2: Рефакторинг кода для использования лямбда-выражений.
- Причины добавления лямбда-выражений в язык Java.
- Рефакторинг кода для уменьшения избыточности.
- Рефакторинг кода при использовании внутренних классов.
- Рефакторинг кода при использовании лямбда-выражений.
- Список преимуществ лямбда-выражений.
Модуль 3: Фильтрация коллекций с помощью лямбда-выражений.
- Итерации по коллекции с помощью forEach.
- Итерации по коллекции с помощью лямбда-выражений.
- Описание интерфейса Stream.
- Фильтрация коллекции с использованием лямбда-выражения.
- Вызов существующего метода с использованием ссылки на метод.
- Сцепление нескольких методов вместе.
- Сравнение функционального и императивного программирования.
- Определение конвейеров в терминах лямбда-выражений и коллекций.
Модуль 4: Использование встроенных интерфейсов в лямбда-выражениях.
- Список встроенных интерфейсов, включенных в java.util.function.
- Использование интерфейса Predicate.
- Обработка объекта с помощью интерфейса Consumer.
- Обработка одного объекта и возврат другого с помощью интерфейса Function.
- Создание нового объекта с помощью интерфейса Supplier.
- Использование примитивных версий базовых интерфейсов.
- Использование бинарных версий базовых интерфейсов.
Модуль 5: Операции с коллекциями в лямбда-выражениях.
- Извлечение данных из объекта с использованием отображения (map).
- Обнаружение данных с использованием методов поиска.
- Описание типов потоковых операций.
- Определение класса Optional.
- Выполнение расчетов с использованием методов.
- Описание отложенной обработки.
- Сортировка в потоке.
- Сохранение результатов в коллекцию с помощью метода collect.
Модуль 6: Параллельные потоки.
- Обзор ключевых характеристик потоков.
- Сравнение старого стиля циклических операции с потоками.
- Описание параллельного выполнения потокового процесса.
- Список основных условий использования параллельных потоков.
- Определение редукции (reduction).
- Ассоциативная функция при редукции.
- Вычисление значения при редукции.
- Описание процесса декомпзиции и объединения при завершении.
Модуль 7: Примеры применения лямбда-выражений.
- Изменение списка с использованием removeIf.
- Обновление списка с использованием ReplaceAll.
- Обновление map (отображение) с помощью computeIfAbsent, computeIfPresent и merge.
- Отправка ключей и значений из map в stream.
- Чтение файла в поток.
- Чтение текстового файла в ArrayList.
- Список, проход и поиск по структуре каталогов при помощи потока.
- Сведение потока с помощью flatMap.
Модуль 8: Расширения методов.
- Построение качественных библиотек.
- Использование статических методов в интерфейсах.
- Использование методов по умолчанию.
- Правила наследования для методов по умолчанию.
Модуль 9: Использование DateTime API.
- Список целей Date/Time API (JSR-310).
- Создание и управление событиями на основе даты.
- Создание и управление событиями на основе времени .
- Комбинирование даты и времени в одном объекте.
Модуль 10: JavaScript на Java с использованием Nashorn.
- Создание и выполнение сценариев shell.
- Написание приложений JavaScript.
- Написание JavaFX приложений с помощью JavaScript.
Разработчики java приложений, использующие Java SE 8.
Новые возможности в Java 8
В этом руководстве мы кратко рассмотрим некоторые из наиболее интересных новых функций Java 8.
Мы поговорим об интерфейсе по умолчанию и статических методах, справочнике по методам и необязательных.
Мы уже рассмотрели некоторые возможности версии Java 8 — потоковый API , лямбда-выражения и функциональные интерфейсы — поскольку это обширные темы, заслуживающие отдельного рассмотрения.
2. Интерфейс по умолчанию и статические методы
До Java 8 интерфейсы могли иметь только общедоступные абстрактные методы. Было невозможно добавить новую функциональность к существующему интерфейсу, не заставляя все реализующие классы создавать реализацию новых методов, а также невозможно было создать методы интерфейса с реализацией.
Начиная с Java 8, интерфейсы могут иметь статические методы и методы по умолчанию , которые, несмотря на то, что они объявлены в интерфейсе, имеют определенное поведение.
2.1. Статический метод
Рассмотрим этот метод интерфейса (назовем этот интерфейс Vehicle ):
static String producer() return "N&F Vehicles"; >
Статический метод производителя () доступен только через и внутри интерфейса. Он не может быть переопределен реализующим классом.
Для вызова вне интерфейса следует использовать стандартный подход для вызова статического метода:
String producer = Vehicle.producer();
2.2. Метод по умолчанию
Методы по умолчанию объявляются с использованием нового ключевого слова по умолчанию . Они доступны через экземпляр реализующего класса и могут быть переопределены.
Давайте добавим в наш интерфейс Vehicle метод по умолчанию , который также будет вызывать статический метод этого интерфейса:
default String getOverview() return "ATV made by " + producer(); >
Предположим, что этот интерфейс реализован классом VehicleImpl .
Для выполнения метода по умолчанию необходимо создать экземпляр этого класса:
Vehicle vehicle = new VehicleImpl(); String overview = vehicle.getOverview();
3. Ссылки на методы
Ссылку на метод можно использовать как более короткую и удобочитаемую альтернативу лямбда-выражению, которое вызывает только существующий метод. Существует четыре варианта ссылок на методы.
3.1. Ссылка на статический метод
Ссылка на статический метод содержит синтаксис ContainingClass::methodName .
Попробуем подсчитать все пустые строки в List с помощью Stream API:
boolean isReal = list.stream().anyMatch(u -> User.isRealUser(u));
Давайте подробнее рассмотрим лямбда-выражение в методе anyMatch() . Он просто вызывает статический метод isRealUser(User user) класса User .
Таким образом, его можно заменить ссылкой на статический метод:
boolean isReal = list.stream().anyMatch(User::isRealUser);
Этот тип кода выглядит намного информативнее.
3.2. Ссылка на метод экземпляра
Ссылка на метод экземпляра содержит синтаксис , содержащийInstance::methodName . «
Следующий код вызывает метод isLegalName(String string) типа User , который проверяет входной параметр:
User user = new User(); boolean isLegalName = list.stream().anyMatch(user::isLegalName);
3.3. Ссылка на метод экземпляра объекта определенного типа
Этот ссылочный метод использует синтаксис ContainingType::methodName . «
Давайте посмотрим на пример:
long count = list.stream().filter(String::isEmpty).count();
3.4. Ссылка на конструктор
Ссылка на конструктор имеет синтаксис ClassName::new . «
Поскольку конструктор в Java — это особый метод, к нему тоже можно применить ссылку на метод с помощью new в качестве имени метода:
StreamUser> stream = list.stream().map(User::new);
4. Необязательно
До Java 8 разработчикам приходилось тщательно проверять значения, на которые они ссылались, из-за возможности создания исключения NullPointerException (NPE) . Все эти проверки требовали довольно раздражающего и подверженного ошибкам шаблонного кода.
Класс Optional Java 8 может помочь справиться с ситуациями, когда существует вероятность получения NPE . Он работает как контейнер для объекта типа T . Он может вернуть значение этого объекта, если это значение не равно null . Когда значение внутри этого контейнера равно null , это позволяет выполнять некоторые предопределенные действия вместо создания NPE .
4.1. Создание необязательного
Экземпляр класса Optional может быть создан с помощью его статических методов.
Давайте посмотрим, как вернуть пустой option :
OptionalString> optional = Optional.empty();
Затем мы возвращаем необязательный элемент, содержащий ненулевое значение:
String str = "value"; OptionalString> optional = Optional.of(str);
Наконец, вот как вернуть необязательный параметр с определенным значением или пустой необязательный параметр, если параметр имеет значение null :
OptionalString> optional = Optional.ofNullable(getString());
4.2. Необязательное использование
Допустим, мы ожидаем получить List , и в случае null мы хотим заменить его новым экземпляром ArrayList .
С кодом до Java 8 нам нужно сделать что-то вроде этого:
ListString> list = getList(); ListString> listOpt = list != null ? list : new ArrayList>();
В Java 8 та же функциональность может быть достигнута с помощью гораздо более короткого кода:
ListString> listOpt = getList().orElseGet(() -> new ArrayList>());
Существует еще больше шаблонного кода, когда нам нужно добраться до какого-то поля объекта по-старому.
Предположим, у нас есть объект типа User , у которого есть поле типа Address с полем street типа String , и нам нужно вернуть значение поля street , если оно существует, или значение по умолчанию, если street равно null :
User user = getUser(); if (user != null) Address address = user.getAddress(); if (address != null) String street = address.getStreet(); if (street != null) return street; > > > return "not specified";
Это можно упростить с помощью опционального :
OptionalUser> user = Optional.ofNullable(getUser()); String result = user .map(User::getAddress) .map(Address::getStreet) .orElse("not specified");
В этом примере мы использовали метод map() для преобразования результатов вызова getAdress() в Optional и getStreet() в Optional . Если бы какой-либо из этих методов вернул null , метод map() вернул бы пустое значениеOptional .
Теперь представьте, что наши геттеры возвращают Optional .
В этом случае мы должны использовать метод flatMap() вместо map() :
OptionalOptionalUser> optionalUser = Optional.ofNullable(getOptionalUser()); String result = optionalUser .flatMap(OptionalUser::getAddress) .flatMap(OptionalAddress::getStreet) .orElse("not specified");
Еще один вариант использования Optional — изменение NPE с другим исключением.
Итак, как мы делали ранее, давайте попробуем сделать это в стиле до Java 8:
String value = null; String result = ""; try result = value.toUpperCase(); > catch (NullPointerException exception) throw new CustomException(); >
И ответ будет более читабельным и простым, если мы используем Optional :
String value = null; OptionalString> valueOpt = Optional.ofNullable(value); String result = valueOpt.orElseThrow(CustomException::new).toUpperCase();
Обратите внимание, как и для чего использовать Optional в нашем приложении — это серьезное и спорное дизайнерское решение, и объяснение всех его плюсов и минусов выходит за рамки этой статьи. Но есть много интересных статей, посвященных этой проблеме. Этот и этот могут быть очень полезными, чтобы копнуть глубже.
5. Вывод
В этой статье мы кратко обсудили некоторые интересные новые функции в Java 8.
Конечно, во многих пакетах и классах Java 8 JDK есть много других дополнений и улучшений.
Но информация, проиллюстрированная в этой статье, является хорошей отправной точкой для изучения некоторых из этих новых функций.
Наконец, весь исходный код статьи доступен на GitHub.
Java 8 — что нового. Часть 1 — интерфейсы.
То, что долго готовилось и должно было случиться, — случилось. Java 8 уже доступна, традиционно — для всех платформ. Попробую кратко обрисовать новое, но сконцентрирую внимание больше на эволюции Java, чем на подчёркивании каких-то «революционных» изменений.
Для начала — немного относительно «свежих» и очевидных фактов, объясняющих почему эволюция Java-платформы важна для IT в целом:
- несмотря на любые домыслы, Java остаётся платформой №1;
- количество разработчиков оценивается на уровне 9 миллионов;
- число загрузок Java (рантайм и JDK) — больше 1 миллиарда в год;
- количество Java-устройств в мире оценивается на уровне 3 миллиардов;
- 97% рабочих компьютеров из корпоративного сектора исполняют Java-приложения;
- Java прочто удерживает титул самой востребованной платформы в сегменте анализа данных.
Проверял я работу Java 8 SDK на двух машинах — одна под управлением Windows 8.1, вторая — Xubuntu 13.10, причём совсем слабенькая, она еле дышит под той же Windows 8.1. Выбор машин — умышленный, как и приложений. Java слишком обширна, чтобы ограничиваться только серверными приложениями, над платформой надстроен великолепный технологический стек для повседневного программистского, причём выходящего далеко за границы Java (например, среда разрабтки PyCharm для Python), и игнорировать эту составляющую никак нельзя.
Но детали — это не главное.
Итак, что же серьёзное случилось в Java 8 и как разработчики языка умудрились «вписать» это серьёзное в развитие платформы так, чтобы ничего не «сломать» во всём её прошлом?
Первое существенное изменение — расширение понятия «интерфейс». «Интерфейс» в Java всегда был специфическим абсолютно и безоговорочно абстрактным классом (и, соответственно, довольно самостоятельным типом). То есть, классом, который без дополнительной и явно выполненной программистом реализации «всего» принципиально не может быть «овеществлён» (instantiated). Именно эту специфику и подчеркнули отдельным ключевым словом языка interface. И это фундаментальная особенность системы типов Java, на которой выстроенна вся развитая иерархия типов.
Попробую объяснить проще — «интерфейс» в Java мог содержать только константы и описания методов (функций). Без всякой реализации. «Овеществить» такое (создать объект такого типа), очевидно, без явной реализации всего, что существует только на уровне описаний, невозможно. И нужно всё это для описания «набора правил поведения» будущих объектов классов, реализующих интерфейс. Как программист реализует следование «правилам поведения» — его личное дело, интерфейс же гарантирует соблюдение объектом этих правил. Поэтому можно считать интерфейс неким соглашением (контрактом) с внешним по отношению к реализующим его объектам, миром.
С помощью интерфейсов в Java всегда компенсировалась (и очень разумно) «врождённая недостаточность» множественного наследования, которого язык принципиально не поддерживает. В общем, интерфейсы — фундаментальная и одна из главнейших концепций Java.
Java 8 внесла существенную корректировку в эту концепцию. начнём с того, что теперь «интерфейс» может быть как бы частично абстрактным, потому что в Java 8 появились «методы интерфейсов, определённые по умолчанию» (interface default methods), а также статические методы.
Default methods — конструкция, которая, по идее, нужна была давно. Без неё научились обходиться и сформировали разные приёмы проектирования и реализации, но если есть возможность избежать лишнего — лучше его избежать (собственно, это и особенно привлекательно в модификациях интерфейсов Java 8 — все они явно подсказаны практикой и накопленным опытом). Предпосылкой необходимости default methods стал очевидный вопрос: если «интерфейс» — это де-факто контракт, то что произойдёт с его реализацией и со всеми «подписантами» в случае изменения контракта? Программные системы ведь эволюционируют, и уровень проектирования «до самых мелочей» возможет только в очень специфических случаях. Этот вопрос сам по себе неприятен на уровне работы с исходными кодами, но ещё же есть бинарное представление программ и крайне неприятная, но нужная в реальности «бинарная совместимость» (не всё можно перекомпилировать и пересобрать по разным причинам — или нет исходных текстов, или просто не позволяют ресурсы). Методы интерфейсов, определённые по умолчанию (default methods) — один из вариантов ответа на этот вопрос в Java 8. Default методы интерфейсов теперь содержат реализации. При этом соблюдается следующая логика:
- очевидно, что раз реализация default метода интерфейса существует до момента «овеществления» объекта, реализующего интерфейс, функциональность default-методов ограничена доступом к объектам «внешнего мира» — мы не можем оперировать несуществующим;
- все default-методы интерфейса по умолчанию являются публично доступными (public);
- при расширении интерфейса возможны три варианта — не обращать внимание на default-методы вообще (и их реализации будут использованы «как есть»), объявить не-default абстрактный метод с той же сигнатурой, что у default-метода из расширяемого интерфейса (в этом случае в новом расширенном интерфейсе метод станет абстрактным), явно переопределить (override) метод вместе с его реализацией;
- только если класс, реализующий интерфейс, реализует и все методы интерфейса, возможна его компиляция и последующее «овеществление» с использованием его собственных реализаций методов (включая default-методы), в противном случае Java-компилятор пытается отыскать default-реализации нереализованных методов в интерфейсе (интерфейсах), а в случае возникновения «бриллиантовой проблемы» (о ней — чуть позже два слова буквально), формируется ошибка компиляции;
- default-методы не нарушают бинарной совместимости кода, реализующего интерфейсы, то есть, их можно добавлять к интерфейсам, расширяя их для новых разработок, при этом старые библиотеки остаются полностью работоспособными (что подтверждается работоспособностью всех Java-приложений дремучих времён в Java 8, где уже default-методов интерфейсов море).
Упомянутую «бриллиантовую проблему» знают многие программисты, имеющие дело с языками, допускающими множественное наследование — если есть абстрактный класс A с одним единственным абстрактным методом M, этот класс наследуют и по-разному реализуют метод М два класса B и С, и есть четвёртый класс — D, наследующий классы B и C и не переопределяющий реализацию метода M, то какая из конкретных реализаций M должна использоваться овеществлениями класса D — B.M или C.M? Ответа на этот вопрос нет, это грубейшая семантическая ошибка на уровне проектирования, потому и «бриллиантовую проблему» жёстко называют «deadly diamond of death».
Кроме default-методов, обновлённые интерфейсы Java 8 позволяют описывать реализации статических (static) методов, являющихся общими для всего класса, а не для овеществлений отдельных объектов класса. Это, можно сказать, тактическое решение, упрощающее де-факто стандартную реализацию паттерна «фабрика объектов», который требовал в общем случае отдельного утилитарного класса со статическими методами генерации объектов. Появление статических методов в интерфейсах позволяет во многих случаях избежать потребности в таком дополнительном классе. Разработчики языка сочли это весомым аргументом.
В следующей записи будет тоже про интерфейсы, но уже настолько важное, что требует отдельного времени и места.
В целом впечатления от Java 8 более чем положительные. Конечно, ожидалось нечто большее (в первую очередь модульность), но, понимая сложность платформы и её критическую значимость для IT, с желаниями больших изменений можно повременить, куда важнее стабильная работа и сохранение всего, что сделано в мире Java за долгие годы существования платформы.
В качестве приятного бонуса — Java 8 при исполнении больших пользовательских приложений (из проверенных мной Netbeans 8, Eclipse, PyCharm и Intellij Idea) в Linux стала существенно «быстрее» и «легковеснее». Это очень заметно и радует.
Новое в Java 8
Добро пожаловать в ведение по Java 8. Этот материал шаг за шагом познакомит вас со всеми новыми фичами языка. Вы научитесь использовать методы интерфейсов по умолчанию (default interface methods), лямбда-выражения (lambda expressions), ссылки на методы (method references) и повторяемые аннотации (repeatable annotations). Все это будет сопровождаться короткими и простыми примерами кода. В конце статьи вы познакомитесь с наиболее свежими изменениями в API, касающихся потоков, функциональных интерфейсов, расширений для ассоциативных массивов, а также с изменениями в API работы с датами.
Методы интерфейсов по умолчанию
Java 8 позволяет вам добавлять неабстрактные реализации методов в интерфейс, используя ключевое слово default . Эта фича также известна, как методы расширения. Вот наш первый пример:
interface Formula < double calculate(int a); default double sqrt(int a) < return Math.sqrt(a); >>
Кроме абстрактного метода calculate интерфейс Formula также определяет метод по умолчанию sqrt . Классы, имплементирующие этот интерфейс, должны переопределить только абстрактный метод calculate . Метод по умолчанию sqrt будет доступен без переопределения.
Formula formula = new Formula() < @Override public double calculate(int a) < return sqrt(a * 100); >>; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0
formula реализован как анонимный объект. Этот код довольно избыточен: целых 6 строчек для такого простого вычисления как sqrt(a * 100) . В следующем разделе мы увидим, что в Java 8 есть куда более изящный способ реализации объектов с одним методом.
Лямбда-выражения
Давайте начнем с простого примера: сортировка массива строк в предыдущих версиях языка.
List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator() < @Override public int compare(String a, String b) < return b.compareTo(a); >>);
Статический метод Collections.sort принимает список и компаратор, который используется для сортировки списка. Наверняка вам часто приходилось создавать анонимные компараторы для того чтобы передать их в метод.
Java 8 предоставляет гораздо более короткий синтаксис — лямбда-выражения, чтобы вам не приходилось тратить время на создание анонимных объектов:
Collections.sort(names, (String a, String b) -> < return b.compareTo(a); >);
Как видите, код гораздо короче и куда более читаем. И его можно сделать еще короче:
Collections.sort(names, (String a, String b) -> b.compareTo(a));
Для однострочных методов вы можете опустить скобки <> и ключевое слово return . Так еще короче:
Collections.sort(names, (a, b) -> b.compareTo(a));
Компилятору известны типы параметров, поэтому их можно тоже опустить. Давайте посмотрим, как еще могут использовать лямбда-выражения.
Функциональные интерфейсы
Как лямбда-выражения соответствуют системе типов языка Java? Каждой лямбде соответствует тип, представленный интерфейсом. Так называемый функциональный интерфейс должен содержать ровно один абстрактный метод. Каждое лямбда-выражение этого типа будет сопоставлено объявленному методу. Также, поскольку методы по умолчанию не являются абстрактными, вы можете добавлять в функциональный интерфейс сколько угодно таких методов.
Мы можем использовать какие угодно интерфейсы для лямбда-выражений, содержащие ровно один абстрактный метод. Для того, чтобы гарантировать, что ваш интерфейс отвечает этому требованию, используется аннотация @FunctionalInterface . Компилятор осведомлен об этой аннотации, и выдаст ошибку компиляции, если вы добавите второй абстрактный метод в функциональный интерфейс.
@FunctionalInterface interface Converter
Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123
Имейте в виду, что этот код останется корректным даже если убрать аннотацию @FunctionalInterface .
Ссылки на методы и конструкторы
Предыдущий пример можно упростить, если использовать статические ссылки на методы:
Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123
Java 8 позволяет вам передавать ссылки на методы или конструкторы. Для этого нужно использовать ключевое слово :: . Предыдущий пример иллюстрирует передачу ссылки на статический метод. Однако мы также можем ссылаться на экземплярный метод:
class Something < String startsWith(String s) < return String.valueOf(s.charAt(0)); >>
Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J"
Давайте посмотрим, как передавать ссылки на конструкторы. Сперва определим бин с несколькими конструкторами:
class Person < String firstName; String lastName; Person() <>Person(String firstName, String lastName) < this.firstName = firstName; this.lastName = lastName; >>
Затем определим интерфейс фабрики, которая будет использоваться для создания новых персон:
interface PersonFactory
Теперь вместо реализации интерфейса мы соединяем все вместе при помощи ссылки на конструктор:
PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");
Мы создаем ссылку на конструктор с помощью Person::new . Компилятор автоматически выбирает подходящий конструктор, сигнатура которого совпадает с сигнатурой PersonFactory.create .
Области действия лямбд
Доступ к переменным внешней области действия из лямбда-выражения очень схож к доступу из анонимных объектов. Вы можете ссылаться на переменные, объявленные как final , на экземплярные поля класса и статические переменные.
final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3
Но в отличии от анонимных объектов, переменная num не обязательно должна быть объявлена как final . Такой код тоже сработает:
int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3
Однако переменная num должна все равно оставаться неизменяемой. Следующий код не скомпилируется:
int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
Запись в переменную num в пределах лямбда-выражения также запрещена.
Доступ к полям и статическим переменным
В отличии от локальных переменных, мы можем записывать значения в экземплярные поля класса и статические переменные внутри лямбда-выражений. Это поведение хорошо знакомо по анонимным объектам.
class Lambda4 < static int outerStaticNum; int outerNum; void testScopes() < ConverterstringConverter1 = (from) -> < outerNum = 23; return String.valueOf(from); >; Converter stringConverter2 = (from) -> < outerStaticNum = 72; return String.valueOf(from); >; > >
Доступ к методам интерфейсов по умолчанию
Помните пример с формулой из первого раздела? В интерфейсе Formula определен метод по умолчанию sqrt , который доступен из каждой имплементации интерфейса, включая анонимные объекты. Однако это не сработает в лямбда-выражениях.
Внутри лямбда-выражений запрещено обращаться к методам по умолчанию. Следующий код не скомпилируется:
Formula formula = (a) -> sqrt( a * 100);
Встроенные функциональные интерфейсы
JDK 1.8 содержит множество встроенных функциональных интерфейсов. Некоторые из них хорошо известны по предыдущим версиям языка, например, Comparator или Runnable . Все эти интерфейсы были поддержаны в лямбдах добавлением аннотации @FunctionalInterface .
Однако в Java 8 также появилось много новых функциональных интерфейсов, которые призваны облегчить вам жизнь. Некоторые интерфейсы хорошо известны по библиотеке Google Guava. Даже если вы незнакомы с этой библиотекой, вам стоить взглянуть, как эти интерфейсы были дополнены некоторыми полезными методами расширений.
Предикаты
Предикаты — это функции, принимающие один аргумент, и возвращающие значение типа boolean. Интерфейс содержит различные методы по умолчанию, позволяющие строить сложные условия ( and , or , negate ).
Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
Функции
Функции принимают один аргумент и возвращают некоторый результат. Методы по умолчанию могут использоваться для построения цепочек вызовов ( compose , andThen ).
Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
Поставщики
Поставщики (suppliers) предоставляют результат заданного типа. В отличии от функций, поставщики не принимают аргументов.
Supplier personSupplier = Person::new; personSupplier.get(); // new Person
Потребители
Потребители (consumers) представляют собой операции, которые производятся на одним входным аргументом.
Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
Компараторы
Компараторы хорошо известны по предыдущим версиям Java. Java 8 добавляет в интерфейс различные методы по умолчанию.
Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
Опциональные значения
Опциональные значения (optionals) не являются функциональными интерфейсами, однако являются удобным средством предотвращения NullPointerException. Это важная концепция, которая понадобится нам в следующем разделе, поэтому давайте взглянем, как работают опциональные значения.
Опциональные значение — это по сути контейнер для значения, которое может быть равно null. Например, вам нужен метод, который возвращает какое-то значение, но иногда он должен возвращать пустое значение. Вместо того, чтобы возвращать null, в Java 8 вы можете вернуть опциональное значение.
Optional optional = Optional.of("bam"); optional.isPresent(); // true optional.get(); // "bam" optional.orElse("fallback"); // "bam" optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
Потоки
Тип java.util.Stream представляет собой последовательность элементов, над которой можно производить различные операции. Операции над потоками бывают или промежуточными (intermediate) или конечными (terminal). Конечные операции возвращают результат определенного типа, а промежуточные операции возвращают тот же поток. Таким образом вы можете строить цепочки из несколько операций над одним и тем же потоком. Поток создаются на основе источников, например типов, реализующих java.util.Collection , такие как списки или множества (ассоциативные массивы не поддерживаются). Операции над потоками могут выполняться как последовательно, так и параллельно.
Сначала давайте посмотрим, как работать с потоком последовательно. Сперва создадим источник в виде списка строк:
List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
В Java 8 вы можете быстро создавать потоки, используя вызовы Collection.stream() или Collection.parallelStream() . Следующие разделы объясняют наиболее распространенные операции над потоками.
Filter
Операция Filter принимает предикат, который фильтрует все элементы потока. Эта операция является промежуточной, т.е. позволяет нам вызвать другую операцию (например, forEach ) над результатом. ForEach принимает функцию, которая вызывается для каждого элемента в (уже отфильтрованном) поток. ForEach является конечной операцией. Она не возращает никакого значения, поэтому дальнейший вызов потоковых операций невозможен.
stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Sorted
Операция Sorted является промежуточной операцией, которая возвращает отсортированное представление потока. Элементы сортируются в обычном порядке, если вы не предоставили свой компаратор:
stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2"
Помните, что sorted создает всего лишь отсортированное представление и не влияет на порядок элементов в исходной коллекции. Порядок строк в stringCollection остается нетронутым:
System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Map
Промежуточная операция map преобразовывает каждый элемент в другой объект при помощи переданной функции. Следующий пример преобразовывает каждую строку в строку в верхнем регистре. Однако вы так же можете использовать map для преобразования каждого объекта в объект другого типа. Тип результирующего потока зависит от типа функции, которую вы передаете при вызове map .
stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Match
Для проверки, удовлетворяет ли поток заданному предикату, используются различные операции сопоставления (match). Все операции сопоставления являются конечными и возвращают результат типа boolean.
boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
Count
Операция Count является конечной операцией и возвращает количество элементов в потоке. Типом возвращаемого значения является long .
long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Reduce
Эта конечная операция производит свертку элементов потока по заданной функции. Результатом является опциональное значение.
Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
Параллельные потоки
Как уже упоминалось выше, потоки могут быть последовательными и параллельными. Операции над последовательными потоками выполняются в одном потоке процессора, над параллельными — используя несколько потоков процессора.
Следующие пример демонстрирует, как можно легко увеличить скорость работы, используя параллельные потоки.
Сперва создадим большой список из уникальных элементов:
int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i
Теперь измерим время сортировки этого списка.
Последовательная сортировка
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
Параллельная сортировка
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms
Как вы можете видеть, оба куска кода практически идентичны, однако параллельная сортировка почти в два раза быстрее. Все, что вам нужно сделать, это заменить вызов stream() на parallelStream() .
Ассоциативные массивы
Как уже упоминалось, ассоциативные массивы (maps) не поддерживают потоки. Вместо этого ассоциативные массивы теперь поддерживают различные полезные методы, которые решают часто встречаемые задачи.
Map map = new HashMap<>(); for (int i = 0; i < 10; i++) < map.putIfAbsent(i, "val" + i); >map.forEach((id, val) -> System.out.println(val));
Этот код в особых комментариях не нуждается: putIfAbsent позволяет нам не писать дополнительные проверки на null; forEach принимает потребителя, который производит операцию над каждым элементом массива.
Этот код показывает как использовать для вычислений код при помощи различных функций:
map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33
Затем мы узнаем, как удалить объект по ключу, только если этот объект ассоциирован с ключом:
map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null
Еще один полезный метод:
map.getOrDefault(42, "not found"); // not found
Объединить записи двух массивов? Легко:
map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
В случае отсутствия ключа Merge создает новую пару ключ-значение. В противном случае — вызывает функцию объединения для существующего значения.
API для работы с датами
Java 8 содержит совершенно новый API для работы с датами и временем, расположенный в пакете java.time . Новый API сравним с библиотекой Joda-Time, однако имеются отличия. Следующие разделы рассказывают о наиболее важных частях нового API.
Clock
Тип Clock предоставляет доступ к текущей дате и времени. Этот тип знает о часовых поясах и может использоваться вместо вызова System.currentTimeMillis() для возвращения миллисекунд. Такая точная дата также может быть представлена классом Instant . Объекты этого класса могут быть использованы для создания объектов устаревшего типа
java.util.Date .
Clock clock = Clock.systemDefaultZone(); long millis = clock.millis(); Instant instant = clock.instant(); Date legacyDate = Date.from(instant); // legacy java.util.Date
Часовые пояса
Часовые пояса представлены типом ZoneId . Доступ к ним можно получить при помощи статических фабричных методов. Часовые пояса содержат смещения, которые важны для конвертации дат и времени в местные.
System.out.println(ZoneId.getAvailableZoneIds()); // prints all available timezone ids ZoneId zone1 = ZoneId.of("Europe/Berlin"); ZoneId zone2 = ZoneId.of("Brazil/East"); System.out.println(zone1.getRules()); System.out.println(zone2.getRules()); // ZoneRules[currentStandardOffset=+01:00] // ZoneRules[currentStandardOffset=-03:00]
LocalTime
Тип LocalTime представляет собой время с учетом часового пояса, например, 10pm или 17:30:15. В следующем примере создаются два местных времени для часовых поясов, определенных выше. Затем оба времени сравниваются, и вычисляется разница между ними в часах и минутах.
LocalTime now1 = LocalTime.now(zone1); LocalTime now2 = LocalTime.now(zone2); System.out.println(now1.isBefore(now2)); // false long hoursBetween = ChronoUnit.HOURS.between(now1, now2); long minutesBetween = ChronoUnit.MINUTES.between(now1, now2); System.out.println(hoursBetween); // -3 System.out.println(minutesBetween); // -239
Тип LocalTime содержит различные фабричные методы, которые упрощают создание новых экземпляров, а также парсинг строк.
LocalTime late = LocalTime.of(23, 59, 59); System.out.println(late); // 23:59:59 DateTimeFormatter germanFormatter = DateTimeFormatter .ofLocalizedTime(FormatStyle.SHORT) .withLocale(Locale.GERMAN); LocalTime leetTime = LocalTime.parse("13:37", germanFormatter); System.out.println(leetTime); // 13:37
LocalDate
Тип LocalDate представляет конкретную дату, например, 2014-03-11. Объекты LocalDate неизменяемы и являются аналогом LocalTime . Пример демонстрирует вычисление новой даты путем сложения или вычитания дней, месяцев или годов. Помните, что каждая операция возвращает новый экземпляр.
LocalDate today = LocalDate.now(); LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS); LocalDate yesterday = tomorrow.minusDays(2); LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4); DayOfWeek dayOfWeek = independenceDay.getDayOfWeek(); System.out.println(dayOfWeek); // FRIDAY
Создание экземпляра LocalDate путем парсинга строки:
DateTimeFormatter germanFormatter = DateTimeFormatter .ofLocalizedDate(FormatStyle.MEDIUM) .withLocale(Locale.GERMAN); LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter); System.out.println(xmas); // 2014-12-24
LocalDateTime
Тип LocalDateTime представляет собой комбинацию даты и времени. Объекты LocalDateTime неизменяемы и работают аналогично LocalTime и LocalDate . Мы можем использовать различные методы для извлечения конкретных значений из даты-времени:
LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59); DayOfWeek dayOfWeek = sylvester.getDayOfWeek(); System.out.println(dayOfWeek); // WEDNESDAY Month month = sylvester.getMonth(); System.out.println(month); // DECEMBER long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY); System.out.println(minuteOfDay); // 1439
Путем добавления информации о часовом поясе мы можем получить Instant .
Instant instant = sylvester .atZone(ZoneId.systemDefault()) .toInstant(); Date legacyDate = Date.from(instant); System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014
Форматирование даты-времени работает так же, как и форматирование даты или времени. Мы можем использовать библиотечные или свои собственные шаблоны.
DateTimeFormatter formatter = DateTimeFormatter .ofPattern("MMM dd, yyyy - HH:mm"); LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter); String string = formatter.format(parsed); System.out.println(string); // Nov 03, 2014 - 07:13
В отличии от java.text.NumberFormat , новый DateTimeFormatter является неизменяемым и потокобезопасным.
Подробно о синтаксисе шаблонов можно почитать здесь.
Аннотации
Аннотации в Java 8 являются повторяемыми. Давайте сразу посмотрим пример, чтобы понять, что это такое.
Сперва мы определим аннотацию-обертку, которая содержит массив аннотаций:
@interface Hints < Hint[] value(); >@Repeatable(Hints.class) @interface Hint
Java 8 позволяет нам использовать множество аннотаций одного типа путем указания аннотации @Repeatable .
Вариант 1: использовать аннотацию-контейнер (старый способ)
@Hints() class Person <>
Вариант 2: использовать повторяемую аннотацию (новый способ)
@Hint("hint1") @Hint("hint2") class Person <>
При использовании варианта 2 компилятор автоматически подставляет аннотацию @Hints . Это важно при чтении информации об аннотациях через рефлексию.
Hint hint = Person.class.getAnnotation(Hint.class); System.out.println(hint); // null Hints hints1 = Person.class.getAnnotation(Hints.class); System.out.println(hints1.value().length); // 2 Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class); System.out.println(hints2.length); // 2
Хотя мы никогда не объявляли аннотацию @Hints в классе Person , она доступна нам при вызове getAnnotation(Hints.class) . Однако более удобным является метод getAnnotationsByType , который напрямую предоставляет доступ ко всем аннотациям @Hint .
Более того, аннотации в Java 8 можно использовать еще на двух элементах:
@Target() @interface MyAnnotation <>
Вот и все
На этом введение в программирование на Java 8 завершено. Вам остается самостоятельно изучить другие новинки JDK 1.8, например, Arrays.parallelSort , StampedLock , CompletableFuture и другие.
Полный исходный код статьи доступен на GitHub.
