JavaRush /Java блог /Random UA /Особливості Java 8 – максимальний посібник (частина 2)
0xFF
9 рівень
Донецк

Особливості Java 8 – максимальний посібник (частина 2)

Стаття з групи Random UA
Друга частина перекладу статті 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:
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ