Что нового в Java 14?

Ну что, прошло еще шесть месяцев, и мы имеем еще один релиз Java, в котором довольно много новых и интересных фич. Поэтому пришло время для статьи, в которой я попытаюсь перечислить все нововведения в JDK 14.
Давайте начнем с наиболее важных из них, которые вносят изменения в синтаксис языка Java.
Записи (Records)
Все мы знаем, что Java является объектно-ориентированным языком: вы создаете классы для хранения данных и используете инкапсуляцию для управления доступом и изменения этих данных. Использование объектов делает манипулирование сложными типами данных простым и понятным. Это одна из причин, почему Java так популярна как платформа.

Недостатком (до сих пор) является то, что для создание типа данных требуется написать много стереотипного кода, даже для самых простых случаев. Давайте посмотрим на код, необходимый для описания двумерной точки:

public class Point {
    private final double x;
    private final double y;
 
    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }
 
    public double getX() {
        return x;
    }
 
    public double getY() {
        return y;
    }
}
А, если добавить в этот код еще и equals, hashCode, toString, то класс заметно увеличится в размерах.

JDK 14 представляет Записи в качестве ознакомительной функции. Функция для ознакомления — это новая концепция, которая позволяет разработчикам платформы Java включать новую языковую возможность, не делая ее частью стандарта Java SE. Подобный подход позволяет разработчикикам опробовать эти функции и получить обратную связь, позволяющую вносить изменения (или даже удалять функцию), при необходимости, до того, как она станет языковым стандартом. Чтобы использовать ознакомительную функцию, необходимо указать в командной строке флаг --enable-preview для компиляции и запуска. Для компиляции вы также должны указать флаг -sourceflag.

Запись — это гораздо более простой способ представления класса данных. Если мы возьмем наш пример Point, код можно сократить до одной строки:

public record Point(double x, double y) { }
Данный способ записи не нарушает читабельность кода, но при этом мы сразу же понимаем, что теперь у нас есть класс, который содержит два значения типа double с именами x и y, к которым мы можем получить доступ, используя стандартные имена методов доступа getX и getY.

Давайте рассмотрим некоторые детали Записей.
Начнем с того, что Записи — это новый вид типа, который представляет собой ограниченную форму класса так же, как и перечисление. Запись имеет имя и описание состояния, которое определяет компоненты записи. В приведенном выше примере Point описание состояния — это x и y. Записи предназначены для упрощения, поэтому они не могут расширять какой-либо другой класс или определять дополнительные переменные экземпляра. Всё состояние в записи является final, поэтому методы доступа (сеттеры) не предоставляются. Если они вам необходимы, то используйте привычную запись класса.

В то же время, у Записей действительно есть некоторая гибкость.
Часто, помимо простого присваивания значений, конструктор должен обеспечивать дополнительное поведение. В таком случае мы можем предоставить альтернативную реализацию конструктора:

record Range(int min, int max) {
    public Range {
        if (min > max)
            throw new IllegalArgumentException(“Max must be >= min”);
    }
}
Обратите внимание, что конструктор все еще в сокращенном виде, поскольку явное указание параметров является избыточным. Любой из членов, который автоматически извлекается из описания состояния, также может быть объявлен. Таким образом, например, вы можете предоставить альтернативную реализацию toString() или hashCode().
Сопоставление с образцом (pattern matching): instanceof
В некоторых ситуациях вам не известен заранее точный тип объекта. Для этой ситуации в Java есть оператор instanceof, который можно использовать для определения различных типов. Недостатком этого оператора является то, что вы должны использовать явное приведение типов:

if (o instanceof String) {
    String s = (String)o;
    System.out.println(s.length);
}
В JDK 14 оператор instanceof был расширен, что позволило указывать имя переменной рядом с проверяемым типом. Далее эту переменную можно использовать без явного приведения:

if (o instanceof String s) {
    System.out.println(s.length);
}
Область видимости такой переменной ограничена:

if (o instanceof String s) }
    System.out.println(s.length);
} else {
    // s is out of scope here
}
Область видимости также может быть ограничена в условном выражении, поэтому мы можем сделать что-то вроде этого:

if (o instanceof String s && s.length() > 4) ...
Это имеет смысл, поскольку метод length() будет вызываться только в том случае, если o является строкой. То же самое не работает с логической операцией `или`:

if (o instanceof String s || s.length() > 4) ...
В этом случае s. length() необходимо оценивать независимо от того, является ли o строкой. Логически это не работает и может привести к ошибке компиляции.

Использование логическое «Не» может привести к некоторым интересным эффектам:

if (!(o instanceof String s && s.length() > 3)
    return;
 
System.out.println(s.length()); // s is in scope here
Я видел несколько отрицательных отзывов касательно определения области видимости переменных, но, учитывая, что все области видимости являются полностью логические, я могу предположить, что это работает очень хорошо.

Уже есть планы для второй ознакомительной версии этой функции, которая расширит возможности для использования ее с Записями и предоставит простой способ реализации шаблона для deconstruction pattern. Более подробную информацию об этом можно найти в JEP 375.
Helpful NullPointerException
Каждый, кто написал более нескольких строк Java-кода, в какой-то момент получал исключение NullPointerException. Если не инициализировать ссылку на объект (или по ошибке явно установить её в null), а затем попытаться использовать ее, то будет вызвано это исключение.

В простых случаях найти причину проблемы достаточно просто. Если мы попробуем запустить код:

public class NullTest {
    List<String> list;
 
    public NullTest() {
        list.add("foo");
    }
}
то получим сгенерированную ошибку:
Поскольку в строке 16 мы ссылаемся на список, то становится очевидным, что список и является причиной появившегося исключения. Данную проблему можно решить очень быстро.

Однако, если мы используем последовательные ссылки (chained references) в строке, подобной этой:

a.b.c.d = 12;
то когда мы запустим этот код, то увидм такую ошибку:
Проблема в том, что мы не можем из этого вывода определить, является ли исключение результатом равенства `а` значению null, или же равенства `b` или `c` значению null. Нам нужно либо использовать отладчик из нашей IDE, либо изменить код, чтобы разделить ссылки на разные строки, но ни один из этих вариантов не является идеальным.

В JDK 14, если мы запустим тот же самый код, то увидим что-то вроде этого:
В полученном выводе сразу же видно, что проблема в `a.b` и мы можем приступить к ее исправлению. Я уверен, что это облегчит жизнь многим Java-разработчикам.
Новые API
Теперь давайте обратим наше внимание на изменения в библиотеках классов.
java.io
PrintStream получил два новых метода, write (byte[] buf) и writeBytes (byte[] buf). Они фактически делают то же самое, что и write (buf, 0, buf. length). Причина наличия двух разных методов заключается в том, что write определен для выброса IOException (но, как ни странно, никогда этого не делает), в то время как writeBytes — нет. Поэтому, выбор метода зависит от того, хотите ли вы окружить его вызов блоком try-catch или нет.

Также, появился новый тип аннотации — Serial, который используется при проверках компилятора во время сериализации. В частности, аннотации этого типа должны применяться к методам и полям классов, объявленных как Serializable. (В некотором смысле он похож на аннотацию Override).
java.lang
Класс Class имеет два метода для Записий: isRecord() и getRecordComponents(). Метод getRecordComponents() возвращает массив объектов RecordComponent. RecordComponent — это новый класс в пакете java.lang.reflect с одиннадцатью методами для извлечения такой информации, как детали аннотаций и дженерики.

Запись (Record) — это простой новый класс, который переопределяет методы Object: equals, hashCode и toString.

NullPointerException теперь переопределяет метод getMessage из Throwable как часть функции helpful NullPointerExceptions.

Класс StrictMath получил шесть новых методов, которые дополняют существующие методы, используемые при обнаружении ошибок переполнения. Новые методы — decrementExact, incrementExact и negateExact (все с двумя перегруженными версиями для параметров int и long).
java.lang.annotation
Для перечисления ElementType добавлена новая константа для Записей (Records) RECORD_TYPE.
java.lang.invoke
Класс MethodHandles. Lookup получил два новых метода:
  • hasFullPrivilegeAccess, который возвращает true, если искомый метод имеет доступ как PRIVATE, так и MODULE.
  • previousLookupClass, который сообщает класс поиска (lookup) в другом модуле, из которого этот объект поиска был ранее телепортирован, или null. Я не слышал о телепортации в контексте Java раньше (только в «Доктор Кто» и «Майнкрафт»). Поиск может привести к телепортации через модули.
java.lang.runtime
Это новый пакет в JDK 14, который имеет единственный класс ObjectMethods. Это низкоуровневая часть функционала Записей, имеющая единственный метод bootstrap, который генерирует методы Object equals, hashCode и toString.
java.util.text
Класс CompactNumberFormat получил новый конструктор, который добавляет аргумент для правил множественных чисел (pluralRules). Представляет собой строку, устанавливающую правила для множественных чисел, которые связывают ключевое слово Count, такое как «один» с фактическим целым числом. Его синтаксис определен в синтаксисе консорциума Unicode для множественных правил (Unicode Consortium’s Plural rules syntax).
java.util
Класс HashSet имеет один новый метод toArray, который возвращает массив, тип компонента которого во время выполнения (runtime) представляет собой Object, содержащий все элементы в этой коллекции.
java.util.concurrent.locks
Класс LockSupport имеет один новый метод setCurrentBlocker. LockSupport предоставляет возможность приостановить (park) и возобновить (unpack) поток (который не испытывает тех же проблем, что и устаревшие методы Thread. suspend и Thread. resume). Теперь можно установить объект, который будет возвращен getBlocker. Это может быть полезно при вызове метода приостановки (park) без аргументов из объекта, не имеющего общий доступ (non-public).
javax.lang.model.element
Перечисление ElementKind имеет три новые константы для Записей (records) и сопоставления с шаблоном (pattern matching) для функции instanceof, а именно BINDING_VARIABLE, RECORD и RECORD_COMPONENT.
javax.lang.model.util
Этот пакет предоставляет утилиты для помощи в обработке элементов и типов программ. С добавлением Записей (records) для поддержки этой функции был добавлен новый набор абстрактных и конкретных классов. (Примерами являются AbstractTypeVisitor14, ElementScanner14 и TypeKindVisitor14).
org.xml.sax
Один новый метод был добавлен в интерфейс ContentHandler синтаксического анализатора SAX XML. Метод объявления получает уведомление о декларации XML. В случае реализации по умолчанию нечего не происходит.
JEP 370: API доступа к внешней памяти
Вводится как эксперементальный модуль для проведения тестирования широким сообществом Java и получения обратной связи, результат которой может быть интегрирован прежде, чем он станет частью стандарта Java SE. Он предназначен в качестве допустимой альтернативы как sun.misc.Unsafe, так и java.io.MappedByteBuffer.

API доступа к внешней памяти вводит три основные абстракции:
      • MemorySegment: предоставление доступа к непрерывной области памяти с заданными границами.
      • MemoryAddress: предоставление смещения (offset) в MemorySegment (представляет собой указатель).
      • MemoryLayout: предоставляет способ описания схемы сегмента памяти, который значительно упрощает доступ к MemorySegment с помощью дескриптора var. Используя это, нет необходимости вычислять смещение, основываясь на том, как используется память. Например, массив int или long будет смещаться по-разному, но будет обрабатываться прозрачно с помощью MemoryLayout.
      Изменения JVM
      Я пристально изучил все 609 страниц спецификации JVM, но не смог найти каких-либо ярко выделенных отличий. Будет интересно взглянуть на байт-коды, сгенерированные для Записей (records), поскольку для этого не требуется никакой специальной поддержки на уровне JVM. То же самое относится и к функции helpful NullPointerException.

      JDK 14 включает в себя некоторые предложения по улучшению JDK (JEP — JDK Enhancement Proposals), которые изменяют нефункциональные части JVM:
      • JEP 345: распределение памяти с помощью NUMA (Non-Uniform Memory Architecture) для G1. Это повышает производительность на больших машинах, которые используют неоднородную архитектуру памяти (NUMA).
      • JEP 363. Удаление сборщика мусора типа CMS (Concurrent Mark Sweep). Начиная с JDK 9, G1 был сборщиком по умолчанию и большинством (но не всеми) считается превосходным сборщиком CMS. Учитывая ресурсы, необходимые для поддержки двух похожих профилей сборщиков, Oracle решила отказаться от CMS (также в JDK 9), и теперь он удален. Если вы ищете высокопроизводительную альтернативу с низкой задержкой, почему бы не попробовать C4 в Zing JVM?
      • JEP 349: потоковая передача событий для регистратора событий, встроенного в виртуальную машину Java (Java Flight Recorder Event Streaming). Это позволит увеличить объем мониторинга в виртуальной машине Java в режиме реального времени, позволяя инструментам асинхронно подписываться на события в регистраторе виртуальной машины Java.
      • JEP 364: ZGC в macOS и JEP 365: ZGC в Windows. ZGC — экспериментальный сборщик с малой задержкой, который изначально поддерживался только в Linux. Теперь это распространено и на операционные системы MacOS и Windows.
      • JEP 366. Не рекомендуется использовать комбинацию ParallelScavenge и SerialOld GC. Oracle утверждает, что очень немногие используют эту комбинацию, при этом затраты на обслуживание значительны. Можно ожидать, что в ближайшем будущем эта комбинация уже не будет использоваться и канет в лету.
      Другие особенности
      Существует ряд JEP, связанных с различными частями OpenJDK:
      • JEP 343: Packaging Tool (инкубатор). Это простой пакетный инструмент, основанный на инструменте JavaFX javapackager, который был удален из Oracle JDK в JDK 11. Много чего можно сказать об этом, поэтому я напишу более подробно в отдельном сообщении в блоге.
      • JEP 352: Non-Volatile Mapped Byte Buffers. Это добавляет новый режим маппинга файлов, специфичный для JDK, так что API FileChannel можно использовать для создания экземпляров MappedByteBuffer, которые ссылаются на энергонезависимую память. Новый модуль, jdk.nio.mapmode, был добавлен, чтобы позволить MapMode быть установленным в READ_ONLY_SYNC или WRITE_ONLY_SYNC.
      • JEP 361: Switch Expressions Standard. Выражения выбора switch были первой ознакомительной функцией, добавленной в OpenJDK в JDK 12. В JDK 13 полученная обратная связь привела к изменению синтаксиса, заменив значения `break` на значения `yield`. В JDK 14 выражения выбора switch больше не являются ознакомительной функцией и включены в стандарт Java SE.
      • JEP 362: устаревшие порты Solaris и SPARC. Поскольку Oracle больше не разрабатывает ни операционную систему Solaris, ни архитектуру микросхемы SPARC, они не хотят продолжать обслуживание этих портов. Они могут быть подхвачены другими в сообществе Java.
      • JEP 367: удаление инструментов и API Pack 200. Еще один функционал, который устарел в JDK 11 и теперь удален из JDK 14. Основное использование этого формата сжатия было для файлов JAR, используемых апплетами. Учитывая, что плагин для браузера был удален из Oracle JDK 11, кажется разумным удаление этого функционала.
      • JEP 368: текстовые блоки. Они были включены в JDK 13 в качестве ознакомительной функции, и они продолжают вторую итерацию (все еще в режиме ознакомления) в JDK 14. Текстовые блоки обеспечивают поддержку многострочных строковых литералов. Добавленные изменения — две новых escape-последовательности. Первая подавляет включение символа новой строки, помещая \ в конце строки. Вторая — \s, которая представляет собой один пробел. Это может быть полезно для предотвращения удаления пробелов с конца строки в текстовом блоке.

      Как видите, у JDK 14 появилось много новых полезных функций, которые могут облегчить жизнь разработчикам.
      Оцените статью, если она вам понравилась!