Вторая часть перевода статьи Java 8 Features – The ULTIMATE Guide. Первая часть тут (ссылка может поменяться).
![Особенности Java 8 – максимальное руководство (часть 2) - 1]()

5. Новые возможности в библиотеках Java 8
В Java 8 добавили много новых классов и расширили существующие в целях оказания более эффективной поддержки современного параллелизма, функционального программирования, даты/времени и многого другого.5.1. Класс Optional
Знаменитый NullPointerException на сегодняшний день является самой популярной причиной сбоев приложений Java. Давным давно прекрасный проект Google Guava представилOptional
как решение NullPointerException
, тем самым препятствуя загрязнению кода проверками на null, как следствие поощряя написание более чистый код. Вдохновленные Google Guava класс Optional
сейчас часть Java 8.
Optional
всего лишь контейнер: он может содержать значение или некоторый тип Т
или просто быть null. Он предоставляет много полезных методов так что явные проверки на null теперь не имеют оправданий. Обратитесь к официальной документации для более детальной информации.
Давайте посмотрим на два небольших примера использования Optional
: с использованием null и без него.
Optional<String> fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) );
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
Метод isPresent()
возвращает true если экземпляр Optional
содержит не null значение и false в противном случае. Метод orElseGet()
содержит запасной механизм результата, если Optional
содержит null, принимая функции для генерации значения по умолчанию. Метод map() преобразует текущее значение Optional
и возвращает новый экземпляр Optional
. Метод orElse()
похож на orElseGet()
, но вместо функции он принимает значение по умолчанию. Вот вывод данной программы:
Full Name is set? false
Full Name: [none]
Hey Stranger!
Давайте кратко рассмотрим другой пример:
Optional<String> firstName = Optional.of( "Tom" );
System.out.println( "First Name is set? " + firstName.isPresent() );
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) );
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();
Результат будет таким:
First Name is set? true
First Name: Tom
Hey Tom!
Для более детальной информации обратитесь к официальной документации.
5.2. Потоки
Недавно добавленный Stream API (java.util.stream
) представляет реальное программирование в функциональном стиле в Java. Это, безусловно, наиболее полное дополнение к библиотеке Java, которое позволяет разработчикам Java быть значительно более эффективными, а также дает возможность им создать эффективный, чистый и краткий код.
Stream API значительно упрощает обработку коллекций (но не ограничиваясь только ими, как мы увидим позже). Возьмем для примера простой класс Task
.
public class Streams {
private enum Status {
OPEN, CLOSED
};
private static final class Task {
private final Status status;
private final Integer points;
Task( final Status status, final Integer points ) {
this.status = status;
this.points = points;
}
public Integer getPoints() {
return points;
}
public Status getStatus() {
return status;
}
@Override
public String toString() {
return String.format( "[%s, %d]", status, points );
}
}
}
Task имеет некоторое представление о очках (или псевдо-сложностях) и может быть либо OPEN либо CLOSE. Давайте введем небольшую коллекцию задач, чтобы поиграть с ними.
final Collection<Task> tasks = Arrays.asList(
new Task( Status.OPEN, 5 ),
new Task( Status.OPEN, 13 ),
new Task( Status.CLOSED, 8 )
);
Первый вопрос, который мы намерены выяснить, это сколько очков содержат задачи со статусом OPEN сейчас? До Java 8 обычным решением для этого было бы использование итератора foreach
. Но в Java 8 ответ в потоках: последовательность элементов, которые поддерживают последовательные и параллельные агрегатные операции.
// Подсчет общего количества очков всех активных задач с использованием sum()
final long totalPointsOfOpenTasks = tasks
.stream()
.filter( task -> task.getStatus() == Status.OPEN )
.mapToInt( Task::getPoints )
.sum();
System.out.println( "Total points: " + totalPointsOfOpenTasks );
И вывод на консоль будет выглядеть как:
Total points: 18
Рассмотрим что здесь происходит. Во-первых, коллекция задач конвертируется в потоковое представление. Затем операция filter
отфильтровывает все задачи со статусом CLOSED. На следующем шаге операция mapToInt
преобразует поток Task
s в поток Integer
s используя метод Task::getPoints
для каждого экземпляра Task
. И наконец, все очки суммируются с использованием метода sum
, который предоставляет конечный результат.
Прежде чем перейти к следующим примерам, есть некоторые замечания о потоках, которые нужно иметь в виду (более детально здесь). Операции stream
делятся на промежуточные и конечные операции.
Промежуточные операции возвращают новый поток. Они всегда ленивые, при выполнении промежуточных операций, таких как filter
, они не выполняют фильтрацию на самом деле, вместо этого создается новый поток, который по завершению формирования, содержит элементы исходного потока, которые соответствуют заданному предикату.
Конечные операции, такие как forEach
и sum
, могут пройти через поток для получения результата или побочного эффекта. После того как конечная операция выполняется, поток считается использованным и не может быть больше использован. Практически во всех случаях конечные операции стремятся завершить свое прохождение по базовому источнику данных.
Другая ценная возможность потоков – это поддержка параллельных процессов из коробки. Давайте посмотрим на этот пример, который находит сумму очков всех задач.
// Calculate total points of all tasks
final double totalPoints = tasks
.stream()
.parallel()
.map( task -> task.getPoints() ) // or map( Task::getPoints )
.reduce( 0, Integer::sum );
System.out.println( "Total points (all tasks): " + totalPoints );
Это очень похоже на первый пример, за исключением того, что мы пытаемся обработать все задачи параллельно и рассчитать конечный результат используя метод reduce
.
Вот вывод в консоль:
Total points (all tasks): 26.0
Часто возникает необходимость в группировке элементов по определенному критерию. Пример демонстрирует как потоки могут помочь с этим.
// Группировка задач по их статусу
final Map<Status, List<Task>> map = tasks
.stream()
.collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );
Вывод в консоль будет следующим:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
Чтобы закончить с примерами для задач, давайте вычислим общий процент (или вес) каждой задачи в коллекции основанный на общем количестве очков:
// Подсчет веса каждой задачи (как процент от общего количества очков)
final Collection<String> result = tasks
.stream() // Stream<String>
.mapToInt( Task::getPoints ) // IntStream
.asLongStream() // LongStream
.mapToDouble( points -> points / totalPoints ) // DoubleStream
.boxed() // Stream<Double>
.mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
.mapToObj( percentage -> percentage + "%" ) // Stream<String>
.collect( Collectors.toList() ); // List<String>
System.out.println( result );
Вывод в консоль будет таким:
[19%, 50%, 30%]
И наконец, как мы отмечали ранее, Stream API предназначено не только для коллекций Java. Типичная операция ввода-вывода, такая как чтение текстовых файлов построчно, является очень хорошим кандидатом для использования потоковой обработки. Вот небольшой пример для подтверждения этого.
final Path path = new File( filename ).toPath();
try( Stream<String> lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}
Метод onConsole
, который вызывается в потоке, возвращает эквивалентный поток с дополнительным закрытым обработчиком. Закрытый обработчик вызывается когда метод close()
вызывается в потоке.
Stream API вместе с лямбдами и ссылочными методами в совокупности с методами по умолчанию и статическими методами в Java 8 являются ответами современным парадигмам в разработке программного обеспечения. Для более детальной информации обращайтесь к официальной документации.
5.3. API для даты/времени (JSR 310)
Java 8 дает новый взгляд на управление датой и временем предоставляя новый API для даты и времени (JSR 310). Манипуляции с датой и временем являются одной из худших болевых точек для Java разработчиков. Стандартныйjava.util.Date
следующий за java.util.Calendar
в общем не улучшил ситуацию (возможно, сделал ее даже более запутанной).
Вот как родился Joda-Time: прекрасная альтернатива API даты/времени для Java. Новый API для даты/времени в Java 8 (JSR 310) находится под сильным влиянием Joda-Time и взял лучшее из нее. Новый пакет java.time
содержит все классы для даты, времени, даты/времени, часовых поясов, продолжительностей и манипуляций со временем. В дизайне API неизменяемость была учтена очень серьезно: изменения не допускаются (жесткий урок извлеченный из java.util.Calendar
). Если требуется модификация – будет возвращен новый экземпляр соответствующего класса.
Давайте посмотрим на основные классы и примеры их использования. Первый класс Clock
, который предоставляет доступ к текущему мгновению, дате и времени, используя часовой пояс. Clock
может быть использован вместо System.currentTimeMillis()
и TimeZone.getDefault()
.
// Получить системное время как смещение UTC
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );
Пример вывод на консоль:
2014-04-12T15:19:29.282Z
1397315969360
Другие новые классы, которые мы будем рассматривать – это LocaleDate
и LocalTime
. LocaleDate
содержит только часть даты без часового пояса в календарной системе ISO-8601. Соответственно, LocalTime
содержит только часть времениcode>.
// получить местную дату и время время
final LocalDate date = LocalDate.now();
final LocalDate dateFromClock = LocalDate.now( clock );
System.out.println( date );
System.out.println( dateFromClock );
// получить местную дату и время время
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );
System.out.println( time );
System.out.println( timeFromClock );
Пример вывода на консоль:
2014-04-12
2014-04-12
11:25:54.568
15:25:54.568
LocalDateTime
объединяет вместе LocaleDate
и LocalTime
и содержит дату и время, но без часового пояса в календарной системе ISO-8601. Простой пример приведен ниже.
// Get the local date/time
final LocalDateTime datetime = LocalDateTime.now();
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );
System.out.println( datetime );
System.out.println( datetimeFromClock );
Пример вывода на консоль:
2014-04-12T11:37:52.309
2014-04-12T15:37:52.309
В случае, если вам нужна дата/время для конкретного часового пояса, вам поможет ZonedDateTime
. Он содержит дату и время в календарной системе ISO-8601. Вот несколько примеров для разных часовых поясов.
// Получение даты/времени для временной зоны
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) );
System.out.println( zonedDatetime );
System.out.println( zonedDatetimeFromClock );
System.out.println( zonedDatetimeFromZone );
Пример вывода на консоль:
2014-04-12T11:47:01.017-04:00[America/New_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los_Angeles]
И, наконец, давайте взглянем на класс Duration
: промежуток времени в секундах и наносекундах. Это делает вычисление между двумя датами очень простым. Давайте посмотрим как это сделать:
// Получаем разницу между двумя датами
final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 );
final Duration duration = Duration.between( from, to );
System.out.println( "Duration in days: " + duration.toDays() );
System.out.println( "Duration in hours: " + duration.toHours() );
В приведенном выше примере вычисляется продолжительность (в днях и часах) между двумя датами, 16 апреля 2014 и 16 апреля 2015. Вот пример вывода на консоль:
Duration in days: 365
Duration in hours: 8783
Общее впечатление о новой дате/времени в Java 8 очень, очень положительное. Частично потому, что изменения основаны на проверенном в боях фундаменте (Joda-Time), частично потому, что в этот раз этот вопрос пересматривался серьезно и голоса разработчиков были услышаны. Для получения деталей обратитесь к официальной документации.
5.4. Движок Nashorn JavaScript
Java 8 поставляется с новым движком Nashorn JavaScript, который позволяет вести разработку и запуск определенных видов приложений JavaScript на JVM. Движок Nashorn JavaScript это просто еще одна реализация javax.script.ScriptEngine, которая выполняет тот же набор правил для разрешения взаимодействия Java и JavaScript. Вот небольшой пример.
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );
Пример вывода на консоль:
jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
5.5. Base64
Наконец, поддержка кодирования Base64 нашла свое место в стандартной библиотеке Java с появлением релиза Java 8. Это очень просто в использовании, пример демонстрирует это.
package com.javacodegeeks.java8.base64;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class Base64s {
public static void main(String[] args) {
final String text = "Base64 finally in Java 8!";
final String encoded = Base64
.getEncoder()
.encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
System.out.println( encoded );
final String decoded = new String(
Base64.getDecoder().decode( encoded ),
StandardCharsets.UTF_8 );
System.out.println( decoded );
}
}
Вывод в консоль программы показывает как кодированный так и декодированный текст:
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!
Есть также классы URL-дружественные кодеры/декодеры, а также MIME-дружественные кодеры/декодеры (Base64.getUrlEncoder()
/ Base64.getUrlDecoder()
, Base64.getMimeEncoder()
/ Base64.getMimeDecoder()
).
5.6. Параллельные массивы
Релиз Java 8 добавляет много новых методов для параллельной обработки массивов. Возможно, наиболее важным из них являетсяparallelSort()
, который может значительно ускорить сортировку на многоядерных машинах. Небольшой пример ниже демонстрирует семейство новых методов (parallelXxx
) в действии.
package com.javacodegeeks.java8.parallel.arrays;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class ParallelArrays {
public static void main( String[] args ) {
long[] arrayOfLong = new long [ 20000 ];
Arrays.parallelSetAll( arrayOfLong,
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
}
}
Этот небольшой фрагмент кода использует метод parallelSetAll()
для заполнения массива 20000 случайными значениями. После этого применяется parallelSort()
. Программа выводит первые 10 элементов до и после сортировки, чтобы показать, что массив действительно отсортирован. Пример вывода программы может выглядеть следующим образом (Обратите внимание, что элементы массива заданы случайным образом).
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793
5.7. Параллелизм
Новые методы были добавлены к классуjava.util.concurrent.ConcurrentHashMap
для поддержки агрегатных операций на основе недавно добавленных объектов потоков и лямбда выражений. Также новые методы были добавлены в класс java.util.concurrent.ForkJoinPool
для поддержки общего пула (посмотрите также наш бесплатный курс по параллелизму Java).
Новый класс java.util.concurrent.locks.StampedLock
был добавлен для обеспечения блокировки на основе возможностей с тремя режимами доступа для управления чтением/записью (он может быть рассмотрен как лучшая альтернатива для не очень хорошего java.util.concurrent.locks.ReadWriteLock
).
Новые классы, которые были добавлены в пакет java.util.concurrent.atomic
:
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
6. Новые функции в среде выполнения Java (JVM)
ОбластьPermGen
упразднена и была заменена на Metaspace (JEP 122). JVM опции -XX:PermSize
и -XX:MaxPermSize
были заменены на -XX:MetaSpaceSize
и -XX:MaxMetaspaceSize
соответственно.
7. Заключение
Будущее здесь: Java 8 продвинуло свою платформу вперед предоставив возможности, которые позволяют разработчикам быть более продуктивными. Еще слишком рано для перевода производственных систем на Java 8, но в ближайшие несколько месяцев эта адаптация должна медленно начинать расти. Тем не менее настало время для начала подготовки вашей кодовой базы для совместимости с Java 8 и быть готовым включить изменения Java 8, когда она окажется достаточно безопасной и стабильной. В качестве подтверждения принятия сообществом Java 8, недавно Pivotal был выпущен Spring Framework с production поддержкой Java 8. Вы можете внести свой вклад о захватывающих новых возможностях в Java 8 в комментариях.8. Источники
Некоторые дополнительные ресурсы, которые глубоко обсуждают различные аспекты функций Java 8:- What’s New in JDK 8
- The Java Tutorials
- WildFly 8, JDK 8, NetBeans 8, Java EE 7
- Java 8 Tutorial
- JDK 8 Command-line Static Dependency Checker
- The Illuminating Javadoc of JDK 8
- The Dark Side of Java 8
- Installing Java™ 8 Support in Eclipse Kepler SR2
- Java 8
- Oracle Nashorn. A Next-Generation JavaScript Engine for the JVM
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ