Функции Java 15: скрытые и запечатанные классы, сопоставление шаблонов и текстовые блоки

Java 15  —  это еще один функциональный релиз, таким будет и 16. Следующий LTS (релиз с долгосрочной поддержкой) запланирован как Java 17, и выйдет он в сентябре 2021 года.

Этот релиз включает новое превью запечатанных (sealed) классов, а также вторую предварительную версию записей (records) и сопоставления шаблонов (pattern matching) для instanceof. И, конечно же, готовые к использованию текстовые блоки (text blocks) и скрытые (hidden) классы.

Запечатанные классы

Наконец одна из проблем Java, возникшая в версии 1.0 еще 25 лет назад, теперь решается.

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

Эта проблема гораздо более актуальна для проектов, в которых использование модификатора области по умолчанию (package-private) не рекомендовано официальным руководством. Создание класса (который должен быть унаследован в какой-то момент) как публичного, так и защищенного, иногда обходится слишком дорого.

Java 15 решает эту проблему, предоставляя детализированный контроль наследования с использованием модификатора области sealed для классов и интерфейсов.

Если нужно сделать суперкласс широко доступным, но не произвольно расширяемым, sealed  —  идеальный вариант.

Пример из документации JDK:

package com.example.geometry;

public sealed class Shape
    permits Circle, Rectangle, Square {...}

Если в исходном файле есть вложенные классы или просто более одного, вы можете опустить permits  —  компилятор Java будет выводить разрешенные подклассы, находящиеся в одном файле.

Это означает, что в приведенном выше примере можно не писать permits, поскольку Shape, Circle, Rectangle и Square находятся в одном файле.

package com.example.geometry;

sealed class Shape {...}
... class Circle    extends Shape {...}
... class Rectangle extends Shape {...}
... class Square    extends Shape {...}

Возможно, это редкий случай, однако если вы хотите сэкономить несколько байтов, не указывая permits в примере выше, то это допустимо.

Разрешенные классы могут быть final, sealed и non-sealed.

package com.example.geometry;

public sealed class Shape
    permits Circle, Rectangle, Square {...}

public final class Circle extends Shape {...}

public sealed class Rectangle extends Shape 
    permits TransparentRectangle, FilledRectangle {...}
public final class TransparentRectangle extends Rectangle {...}
public final class FilledRectangle extends Rectangle {...}

public non-sealed class Square extends Shape {...}

Указание final предотвратит дальнейшее расширение этой части иерархии классов.

sealed допускает расширение этой части иерархии за рамки первоначально определенного суперкласса, но только разрешенными им классами.

И наконец, non-sealed открывает иерархию классов для расширения неизвестными подклассами. Однако, начиная с дочернего незапечатанного класса, суперкласс будет запечатан и недоступен для неизвестных расширений. 

Вероятно, non-sealed —  это первое ключевое слово с дефисом в Java, по неизвестным причинам вносящее несогласованность в язык. В Java уже есть ключевые слова, состоящие из двух слов, такие как instanceof, поэтому было бы логичнее использовать nonsealed.

В Java Reflection API также добавлена поддержка запечатанных классов  —  два новых метода, оба довольно понятны:

java.lang.constant.ClassDesc[] 
getPermittedSubclasses();

boolean isSealed()

Запечатанным классам и интерфейсам стоит уделить внимание, потому что в одном из будущих релизов Java они пригодятся для сопоставления шаблонов в выражениях switch.

Скрытые классы

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

Подробную информацию об этой функции можно прочитать в открытой документации JDK. Ниже ее основная суть.

Стандартные API, определяющие класс ClassLoader::defineClass и Lookup::defineClass, не обращают внимание на то, как были сгенерированы байт-коды класса  —  динамически (во время выполнения) или статически (во время компиляции). Они всегда определяют видимый класс, который будет использоваться каждый раз, когда другой класс в той же иерархии загрузчика попытается связать класс с этим именем.

Таким образом, если стандартный API может определять скрытые классы, недоступные для обнаружения и с ограниченным жизненным циклом, то фреймворки как внутри, так и вне JDK, генерирующие классы динамически, могут вместо этого определять скрытые классы с меньшей видимостью и издержками. Это повысит эффективность всех реализаций языка, построенных на JVM.

Добавление этой функции также позволяет отказаться от использования нестандартного API sun.misc.Unsafe::defineAnonymousClass. Скрытые классы не будут поддерживать все функции defineAnonymousClass, но это и не является целью. Главное  —  отказаться от этого API и полностью удалить его в будущих выпусках.

Мы не будем углубляться в скрытые классы, поскольку они больше пригодятся разработчикам фреймворков, работающим над внутренними низкоуровневыми задачами. Всю остальную информацию о них можно найти в официальном JEP.

Сопоставление шаблонов для instanceof (второе превью)

Первоначальное превью этой функции было представлено в Java 14. Второе превью в Java 15 не включает никаких новых изменений  —  все то же, что было и в Java 14. Главная цель  —  получить больше отзывов от сообщества.

Все мы знакомы с идиомой instanceof-and-cast, она широко используется во всех крупных базах кода:

if (obj instanceof String) {
    String s = (String) obj;
    s.contains("Java 13");
} else {
    ...
}

Сопоставление шаблонов для instanceof дает более чистый способ выполнения этой проверки, а оператор instanceof теперь поддерживает тестовый шаблон типа вместо простого типа:

if (obj instanceof String s) {
    s.contains("Java 15 with Pattern Matching");
} else {
    ...
}

Больше нет необходимости в явном приведении типов в блоке true, что не только сокращает код, но и делает его более читабельным.

Записи (вторая предварительная версия)

Записи были представлены в качестве превью функции в Java 14. Повторное превью содержит изменения, основанные на отзывах от сообщества. Главная цель этой функции состоит в том, чтобы упростить создание неизменяемых, управляемых данными конструкций в Java.

Записи  —  это новый вид класса, объявление которого состоит из имени, заголовка и тела. В заголовке перечислены компоненты записи, которые представлены переменными.

record Point(int x, int y) { }

Все переменные, в данном случае x и y, автоматически будут иметь публичный метод доступа с тем же именем и типом возвращаемого значения, что и переменная.

В тело записи можно добавить произвольные методы, которые работают над ее состоянием .

Reflection API был обновлен для работы с записями:

// получение всех переменных из заголовка
RecordComponent[] getRecordComponents();

// задание класса, объявленного как record
boolean isRecord();

Записи  —  это чистый способ создания класса данных в Java с помощью нескольких строк кода. Они имеют указание final, а значит не могут участвовать в наследовании.

Текстовые блоки

Первое превью появилось еще в Java 12, и теперь текстовые блоки стали постоянной функцией! Они упрощают написание и, что более важно, повышают читабельность многострочного текста внутри Java. Появление этой функции ожидалось уже давно, поскольку другие языки, работающие на виртуальных машинах Java, такие как Kotlin и Scala, уже давно ее поддерживают.

Основная проблема, которую решают текстовые блоки  —  это написание кода других языков внутри кода Java. До этого требовалось использовать шаблон типа /n для разрывов строк в конце каждой строки, что ухудшало читабельность и повышало риск возникновения ошибок.

Текстовые блоки предоставляют лучший способ написания многострочного текста в Java  —  с помощью тройных кавычек в роли разделителей. Они могут использоваться везде, где и обычная строка.

// Пример на SQL

// Использование "одномерных" строковых литералов
String query = 
    "SELECT \"EMP_ID\", \"LAST_NAME\" FROM \"EMPLOYEE_TB\"\n" +
    "WHERE \"CITY\" = 'INDIANAPOLIS'\n" +
    "ORDER BY \"EMP_ID\", \"LAST_NAME\";\n";

// Использование "двумерного" блока текста
String query = """
               SELECT "EMP_ID", "LAST_NAME" FROM "EMPLOYEE_TB"
               WHERE "CITY" = 'INDIANAPOLIS'
               ORDER BY "EMP_ID", "LAST_NAME";
               """;

Обратите внимание, что текстовые блоки нельзя использовать в одной строке: открывающие кавычки должны сопровождаться терминатором строки, иначе код не будет компилироваться.

Java скомпилирует текстовый блок в обычную строку так, что даже не будет ясно, чем именно этот фрагмент являлся изначально.

Java развивается на невероятной скорости, выпуская новые релизы каждый шесть месяцев  — 15 вышел только недавно, а уже интересно, что готовится в 16 и LTS 17. Отличное время, чтобы перейти на Java!

Прочитать более подробную информацию о релизе и попробовать JDK 15 самостоятельно можно на официальном сайте.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Oskar: Java 15 Features: Sealed and Hidden Classes, Pattern Matching And Text Blocks