Java 14: Record, более лаконичный instanceof, упаковщик jpackage, switch-лямбды и текстовые блоки

UPD. Сегодня состоится долгожданный релиз Java 14 — и пусть она не LTS, — новых фич в ней достаточно. Java 14 релизнется в течение нескольких часов —, но знакомиться с ней можно уже сейчас.

usu0aephljxik4pjtyk6baaapyc.jpeg

В Java 14 достаточно изменений, как на уровне написания кода, так и на уровне API, GC и многих других подкапотных штук. Можно с некоторой уверенностью сказать, что если Вы знаете о каких-то суперфишках Kotlin или Python — не переживайте, с большой долей вероятности они скоро появятся в джаве. Во всяком случае, сегодняшний релиз содержит некоторые из них. Но — обо всём по порядку.

В Java 14 нас ожидают следующие нововведения:

  • JEP 305. Присвоение ссылки объекту, проверяемому через instanceof.
  • JEP 343. Упаковщик jpackage (Инкубатор).
  • JEP 345. Распределение памяти с учётом NUMA для G1.
  • JEP 349. Потоковая передача событий через JFR.
  • JEP 352. Неизменяемые Mapped Byte Buffers.
  • JEP 358. Подсказки при NullPointerException.
  • JEP 359. Record.
  • JEP 361. Switch-лямбды.
  • JEP 362. Порты Solaris и SPARC теперь Deprecated.
  • JEP 363. Удаление сборщика мусора Concurent Mark Sweep, который ранее был помечен, как Deprecated.
  • JEP 364. Портирование ZGC на macOS.
  • JEP 365. Портирование ZGC на Windows.
  • JEP 366. Комбинация ParallelScavenge + SerialOld GC теперь Deprecated.
  • JEP 367. Удаление инструментов и API Pack200 (которые были помечены как Deprecated ещё в Java 11).
  • JEP 368. Текстовые блоки.
  • JEP 370. API доступа к внешней памяти (Инкубатор).


Поговорим о каждом улучшении, на некоторых остановимся подробнее.

JEP 305. Присвоение ссылки объекту, проверяемому через instanceof.


7apmo-a8gbn7oj4tqu-9-iv6vcq.jpeg

Рассмотрим ситуацию проверки типа объекта через instanceof. Если мы хотим явно кастомизировать объект в необходимый тип без риска поймать ClassCastException, нам необходимо сначала убедиться, что объект — нужного нам типа.

    private static void showNameIfToy(Object o) {
        if (o instanceof Toy) {  //проверяем, действительно ли объект - класса Toy
            Toy t = (Toy) o;  //присваиваем ему ссылку
            System.out.println("Toy's name: " + t.getName());  //что-то делаем с объектом
        }
    }


В данном примере, который встречается, кстати, сплошь и рядом, мы а) убеждаемся, что перед нами объект нужного типа, б) присваиваем ему ссылку, в) что-то с ним делаем, исходя из нашей логики. В Oracle, похоже, разглядели некоторую пышность кода в данной конкретной конструкции, и решили сократить её ровно на одну ступень. Теперь можно писать так:

    private static void showNameIfToy(Object o) {
        if (o instanceof Toy t) {  //проверяем объект и сразу присваиваем ему ссылку
            System.out.println("Toy's name: " + t.getName());  //сразу можем приступать к дальнейшим действиям
        }
    }


Насколько всё это действительно удобно и полезно, оставляю судить тебе, читатель. Да, действительно, эта конструкция в данном виде встречается, наверное, в 99% случаев, связанных с instanceof, и, возможно, упрощение приживётся (а может, и нет). Время покажет.

JEP 343. Упаковщик jpackage (Инкубатор).


qyhk2diseyyhq_rysnopvdiq1gk.jpeg

Мы, разработчики, ребята стреляные, нас установкой джарника не испугаешь, а как быть простому пользователю, который не хочет знать при установке JVM, что она установлена ещё на 3 миллиардах машин, не хочет знать про кросс-платформенность Java, а просто хочет ткнуть 2 раза .exe-файл, если у него винда, или просто перетащить .app-приложение в папку «Приложения», если у него мак, и не париться? Как создать все условия для программистов, чтобы они упаковывали свои приложения в привычные конечному потребителю «экзешники»?

Похоже, в Oracle осознали проблему и решили написать упаковщик, который будет сразу паковать приложения в подходящий для платформы формат:

  • Linux: deb и rpm
  • macOS: pkg и dmg
  • Windows: msi и exe


Выглядит это примерно так:

$ jpackage --name myapp --input lib --main-jar main.jar --type exe


--name myapp — будущее имя приложения
--input lib — источник архива jar
--main-jar main.jar — имя архива, содержащего основной класс
--type exe — тип, в который будет упаковано приложение.

После упаковки, Вы сможете дважды ткнуть на myapp.exe (если у Вас Windows) и установить его как обычное windows-приложение. Графический интерфейс представлен JavaFX.

Попробуем собрать такое приложение, используя платформу Windows.

Итак, возьмём проект отсюда:
https://github.com/promoscow/bouncer
При отправке GET-запроса мы получаем сообщение: «Bounce successfull» и timestamp.

Собираем джарник. Поскольку у нас gradle, он в папке build/libs.

bfbzeg3ansxdehnqoudd76-fcbw.jpeg

Переходим в папку build, вводим необходимый минимум команд:

jpackage --name bouncer --input libs --main-jar bouncer.jar --type exe


Получаем, действительно, exe-шник.

56-knmpxzinlb7nwtm3owmkn2pa.jpeg

Тыкаем два раза в экзешник, происходит обычная установка приложения.
Ого!

a3ckjkvq0hs7flr4towxmah19h4.jpeg

Приложение установилось и ждёт своего часа.
В Program Files в папке bouncer можно запустить bouncer.exe.

2ky_vc98fu1a7msjhss9v_m59dk.jpeg

JEP 345. Распределение памяти с учётом NUMA для G1.


Существует такая проблема — NUMA, Non-Uniform Memory Access, Неодинаковый доступ к памяти. Иными словами, доступ к удалённым сокетам в многосекционных машинах может занимать значительное время, особенно это касается сборщика мусора G1. В Java 14 попытались решить эту проблему следующим образом:

Куча G1 организована как набор областей фиксированного размера. Регион обычно представляет собой набор физических страниц, хотя при использовании больших страниц (через -XX:+UseLargePages) несколько регионов могут составлять одну физическую страницу.

Если добавить при инициализации JVM параметр +XX:+UseNUMA, то регионы будут равномерно распределены по общему количеству доступных узлов NUMA.

В Oracle обещают, что хоть данный подход не совсем гибкий, в дальнейшем они его улучшат.

JEP 349. Потоковая передача событий через JFR.


Есть такая штука в Java, как JFR — Java Flight Recording — фиксация событий «на лету», которая позволяет мониторить около 500 разновидностей событий во время работы приложения. Проблема в том, что большинство из них можно посмотреть только в логах. Улучшение, предлагаемое Oracle, заключается в том, чтобы реализовать обработчик, например, лямбда-функцию, которая будет вызываться в ответ на событие.

Вот как это выглядит:

try (var rs = new RecordingStream()) {
  rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
  rs.onEvent("jdk.CPULoad", event -> System.out.println(event.getFloat("machineTotal")));
  rs.start();
}


Это событие каждую секунду выводит в консоль загрузку процессора.

8bm_bpzyy8qacbkj7grka4oyr3c.jpeg

JEP 358. Подсказки при NullPointerException.


Ещё одно явное удобство, призванное упростить поиск ошибки в коде. Представим себе конструкцию — планета, на планете много стран, в каждой стране много городов.

public class Planet {

    private List countries;
    //...
}

public class Country {

    private List cities;
    //...
}

public class City {

    private String name;
    //...
}


Мы решили вывести на экран хэшкоды всех городов, находящихся на планете:

planet.getCountries().forEach(c -> c.getCities().forEach(city -> city.hashCode()));


Но не подумали об обязательной инициализации полей. И в какой-то момент получили NullPointerException:

Exception in thread "main" java.lang.NullPointerException
	at ru.xpendence.jep_358_nullpointerexception.Main.main(Main.java:19)


Какое поле у нас null? planet? country? city??? Мы не знаем. Мы ставим на нужную строчку брейкпоинт и, вздохнув, идём дебажить.

В Java 14 NullPointerException более информативен:

Exception in thread "main" java.lang.NullPointerException: Cannot assign field "cities" because "countries" is null
     at Main.main(Main.java:21)
     ...


И сразу всё понятно. countries is null.

JEP 359. Record.


us7zaleyxjn5obz6_8gaufbpdgu.jpeg

Не зря в Oracle сменили релизный цикл на полугодовой. Тектонические сдвиги в IT-отрасли заставляют бодрее шевелиться даже таких лидеров. И если, к примеру, Kotlin и Python при своём появлении испытывали влияние Java (про Python так, во всяком случае, сказано в Википедии), то теперь Java оглядывается на своих, так сказать, последователей. Про Python будет ниже, но следующую фичу Oracle точно посмотрели в Kotlin. Речь о data-классах, которые в Java теперь называются record.

Что такое data-класс? Напишем обычный POJO:

public class Station {

    private String name;
    private Coordinates coordinates;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Coordinates getCoordinates() {
        return coordinates;
    }

    public void setCoordinates(Coordinates coordinates) {
        this.coordinates = coordinates;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PlainStation that = (PlainStation) o;
        return Objects.equals(name, that.name) &&
                Objects.equals(coordinates, that.coordinates);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, coordinates);
    }

    @Override
    public String toString() {
        return "PlainStation{" +
                "name='" + name + '\'' +
                ", coordinates=" + coordinates +
                '}';
    }
}


Тут у нас всё подряд — геттеры, сеттеры, equals, hashcode, toString… Чтобы как-то избавиться от этого безобразия, хорошие люди придумали Lombok.

В Jetbrains проблему решили в своё время более радикальным способом — придумав для Kotlin data-классы. Такой класс со всеми стандартными методами выглядит так:

data class Station(val name: String, val coordinates: Coordinates)


И всё. В Oracle, похоже, посмотрели на данную конструкцию и сделали точно такую же, только record:

public record RecordStation(String name, List coordinates) {}


Он содержит в себе стандартные геттеры, сеттеры, equals, hashcode и toString.
Класс такого типа уже можно создать в IDEA 2020.1.

В чём отличия от POJO?


  • Record не может быть унаследован от какого-нибудь класса.
  • У Record не может быть других полей объекта, кроме тех, которые объявлены в конструкторе при описании класса (да, это конструктор по умолчанию, кстати). Статичные — можно.
  • Поля неявно являются финальными. Объекты неявно являются финальными. Со всеми вытекающими, вроде невозможности быть абстрактными.


Получается, что это такой иммутабельный data-класс, не предназначенный для каких-то сложных логических действий с ним, а предназначенный для передачи данных, и всё.

JEP 361. Switch-лямбды.


tdbvsa6l6cmnjqbaxukwjggnrtg.jpeg

Похоже, в Oracle плотно взялись за switch. До текущего релиза это была довольно громоздкая конструкция, и выглядела она так:

    public static void translateDayOfWeekOld(String dayOfWeek) {
        switch (dayOfWeek) {
            case "MONDAY":
                System.out.println("Понедельник");
                break;
            case "TUESDAY":
                System.out.println("Вторник");
                break;
            case "WEDNESDAY":
                System.out.println("Среда");
                break;
            case "THURSDAY":
                System.out.println("Четверг");
                break;
            case "FRIDAY":
                System.out.println("Пятница");
                break;
            case "SATURDAY":
                System.out.println("Суббота");
                break;
            case "SUNDAY":
                System.out.println("Воскресенье");
                break;
            default:
                System.out.println("Day of week not found, try again with today day of week");
                String displayName = LocalDate.now().getDayOfWeek().name();
                translateDayOfWeek(displayName);
        }
    }


Обработка одного условия занимает минимум три строчки — case, действие, break. Теперь, с расширенной функциональностью switch, мы можем сократить вышеописанную конструкцию до такого:

    public static void translateDayOfWeek(String dayOfWeek) {
        switch (dayOfWeek) {
            case "MONDAY" -> System.out.println("Понедельник");
            case "TUESDAY" -> System.out.println("Вторник");
            case "WEDNESDAY" -> System.out.println("Среда");
            case "THURSDAY" -> System.out.println("Четверг");
            case "FRIDAY" -> System.out.println("Пятница");
            case "SATURDAY" -> System.out.println("Суббота");
            case "SUNDAY" -> System.out.println("Воскресенье");
            default -> {
                System.out.println("Day of week not found, try again with today day of week");
                String displayName = LocalDate.now().getDayOfWeek().name();
                translateDayOfWeek(displayName);
            }
        }
    }


Согласитесь, довольно компактно, модно и с лямбдами. Стоит ли это того, решать Вам.

Кстати, написанный по старым правилам switch IDEA 2020.1 заботливо предложит переписать на новый лад.

6gvc-klmcnwryt4hnukssulzaqe.jpeg

JEP 362. Порты Solaris и SPARC теперь Deprecated.


Тут всё просто. В Oracle решили, что не стоит тратить ресурсы на поддержку портов Solaris и SPARC, а освободившихся сотрудников переключить на разработку новых фич.

JEP 363. Удаление сборщика мусора Concurent Mark Sweep, который ранее был помечен, как Deprecated.


Об удалении сборщика мусора CMS говорили ещё два года назад, в 9-м релизе. За это время Oracle выпустили два сборщика мусора — ZGC и Shenandoah. В то же время, ни один из заслуживающих доверия Oracle контрибьюторов не уделил внимания поддержке CMS.

В общем, доктор сказал в морг — значит, в морг.

JEP 364, 365. Портирование ZGC на macOS и на Windows.


Обычно мы разворачиваем приложения на платформах Linux, но для локальной разработки мы часто используем Windows и Mac. Для этих нужд в Java 14 портировали сборщик мусора ZGC на эти две платформы.

JEP 366. Комбинация ParallelScavenge + SerialOld GC теперь Deprecated.


Существует крайне редко используемая конфигурация сборщика мусора — когда молодой ParallelScavenge комбинируют с SerialOld. Зачем это делают — непонятно, поскольку ParallelScavenge является параллельным, а SerialOld — наоборот, нет. Включение этой комбинации требует особых танцев с бубном и требует немалой крови разработчиков. «Oracle заботится о вас», поэтому помечает эту конфигурацию как устаревшую и надеется отправить её в скором времени к сборщику мусора CMS.

Возрадуемся, братья.

JEP 367. Удаление инструментов и API Pack200 (которые были помечены как Deprecated ещё в Java 11).


Пришёл черёд pack200, unpack200 и API pack200 отправиться на заслуженный отдых. И причина в моральном устаревании этого упаковщика. Когда-то давно, когда интернет был модемным и скорость у него была 56k (бумеры помнят), JDK приходилось выкачивать часами. Был придуман упаковщик, который лучше сжимал JDK и уменьшал время скачивания. Также, им можно было сжимать аплеты и клиентские приложения. Но время шло, и при текущих скоростях упаковщик не актуален.

Следующие пакеты будут удалены:

java.util.jar.Pack200
java.util.jar.Pack200.Packer
java.util.jar.Pack200.Unpacker

а также, модуль jdk.pack.

JEP 368. Текстовые блоки.


mrg9_vzcquzspnav3ld9yl0ofcm.jpeg

В своё время, Java оказала (в числе полутора десятков других языков) влияние на Python. Поскольку Python стремительно набирает популярность и уверенно толкается с другими лидерами IT-чартов Java и C, нет ничего зазорного в том, чтобы что-то подсмотреть и у него. В своё время в Java 9 появился JShell, очень похожий на питоний Jupiter. И вот, настало время текстовых блоков.

Когда нам нужно записать строку, мы пишем строку:

String s = "Строка";


Когда нам нужно записать отформатированную строку, мы пишем что-то такое:

String oldHtml = "\n\t\n\t\t

Hi all!

\n\t \n";


Текст абсолютно нечитаемый. И вот, в Java 14 проблема решена. Теперь, используя тройные кавычки, можно писать любой текст:

        String html = """
                      
                          
                              

Hi all!

""";


Намного удобнее и читабельнее. Мы можем просто скопировать в код любой текст и не возиться с табами и переносами. Красота! Если же нам просто нужно в таком тексте перенести текст на другую строку, не делая при этом переноса, мы можем воспользоваться новым литералом — обратным слэшем. Этот символ вообщает, что следующий за ним перенос — не перенос. Пример:

        String text = """
                Богами вам ещё даны \
                Златые дни, златые ночи, \
                И томных дев устремлены  \
                На вас внимательные очи. \
                Играйте, пойте, о друзья! \
                Утратьте вечер скоротечный; \
                И вашей радости беспечной \
                Сквозь слёзы улыбнуся я.
                """;


Вывод:

Богами вам ещё даны Златые дни, златые ночи, И томных дев устремлены На вас внимательные очи. Играйте, пойте, о друзья! Утратьте вечер скоротечный; И вашей радости беспечной Сквозь слёзы улыбнуся я.

JEP 370. API доступа к внешней памяти (Инкубатор).


Бывает такое, что приложение обращается к внешней памяти наподобие Ignite, mapDB, memcached и др. Существующие API для доступа к ней вполне рабочие, но Oracle захотелось чего-то глобального. Так появились абстракции MemorySegment, MemoryAddress и MemoryLayout. Пока фича находится в инкубаторе, и все желающие могут по-прежнему довольствоваться ByteBuffer, Unsafe и JNI.

Заключение.


Не знаю, как тебе, читатель, а мне нравится полугодовой релизный цикл, на который перешла Oracle начиная с Java 9. Теперь Oracle не ставит задач выпускать абсолютно стабильные релизы, но искушённому джависту не составит труда быть в курсе стабильности той или иной фичи, наблюдая за их развитием и тестируя что-то из инкубатора. Язык стал более живым, изменяемым, в нём стали появляться очень смелые нововведения и заимствования из других языков. Да, кто-то не поспевает за мельканием релизов, но профессия у нас такая, что надо поспевать, поэтому, хотим мы этого или нет — встречаем Java 14.

Как обычно, прилагаю проект на гитхабе с примерами кода: [тыцнуть сюда]

© Habrahabr.ru