JavaRush /Java блог /Архив info.javarush /Особенности Java 8 – максимальное руководство (часть 2)
0xFF
9 уровень
Донецк

Особенности Java 8 – максимальное руководство (часть 2)

Статья из группы Архив info.javarush
Вторая часть перевода статьи 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 преобразует поток Tasks в поток Integers используя метод 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:
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ