В Java 15 появились sealed-классы и sealed-интерфейсы, с помощью которых стало возможным ограничивать иерархию классов и интерфейсов на уровне синтаксиса языка. Теперь возможные иерархии определяются декларативно. Этот функционал пока представлен в режиме превью (preview).
Также в Java 15 есть изменения в записях (Records), появившихся в Java 14. А сопоставление с образцом (pattern matching) для instanceof вошло в Java 15 как второе превью без изменений. Текстовые блоки (text block) из Java 13 включены в Java 15 как стандартная языковая конструкция. Изменений в них по сравнению с Java 14 нет.
В этой статье я расскажу обо всех новых и обновленных языковых конструкциях Java 15, о том, как они вам могут пригодиться, и как их использовать в IntelliJ IDEA. Давайте начнем.
Sealed-классы и интерфейсы
Определяя класс как sealed, вы можете явно указать, каким классам разрешено его расширять. Это, с одной стороны, позволяет использовать класс повторно через наследование, а с другой ограничить допустимых наследников. Но зачем вам ограничивать иерархии наследования?
Необходимость ограниченных иерархий
Представьте, что вы разрабатываете приложение для садовников. Садовнику, в зависимости от вида растения, требуется выполнять различные действия. Давайте смоделируем иерархию растений следующим образом (я намеренно не привожу полный текст классов):
class Plant {}class Herb extends Plant {}class Shrub extends Plant {}class Climber extends Plant{}class Cucumber extends Climber {}
Ниже приведен пример того, как класс Gardener
(садовник) может использовать эту иерархию классов:
public class Gardener { int process(Plant plant) { if (plant instanceof Cucumber) { return harvestCucumber(); } else if (plant instanceof Climber) { return sowClimber(); } else if (plant instanceof Herb) { return sellHerb(); } else if (plant instanceof Shrub) { return pruneShrub(); } else { System.out.println("Unreachable CODE. Unknown Plant type"); return 0; } } private int pruneShrub() { .. } private int sellHerb() { .. } private int sowClimber() { .. } private int harvestCucumber() { .. }}
Проблема здесь в том, что разработчику необходимо предусмотреть ветку else для контроля ситуации, когда другой разработчик добавит класс в эту иерархию. Sealed-классы помогают наложить необходимые ограничения на уровне языка.
Определение защищенных иерархий с помощью sealed-классов
Объявить sealed-класс можно с помощью модификатора
sealed
. Для указания классов, которые могут его
расширять напрямую, используется ключевое слово
permits
. Подклассы могут быть final
,
non-sealed
или sealed
.
Gif'ка ниже показывает, как изменить объявление обычного класса на sealed-класс и модифицировать его наследников:
Вот измененный код:
sealed public class Plant permits Herb, Shrub, Climber {}final class Herb extends Plant {}non-sealed class Shrub extends Plant {}sealed class Climber extends Plant permits Cucumber{}final class Cucumber extends Climber {}
Позволяя расширять класс только определенному перечню классов, вы можете отделить доступность (accessibility) от расширяемости (extensibility). Можно сделать sealed-класс доступным для других пакетов и модулей и контролировать, кто может его расширять. В прошлом, чтобы предотвратить расширение классов, разработчики создавали package-private классы. Однако это также ограничивало к ним доступ. Для sealed-классов это уже не так.
Перечень permitted-подклассов доступен через рефлексию
(reflection) с помощью метода
Class.permittedSubclasses()
. Можно получить всю
sealed-иерархию в рантайме.
Давайте быстро проверим вашу конфигурацию IntelliJ IDEA, чтобы убедиться, что вы сможете запустить код примеров.
Конфигурация IntelliJ IDEA
Возможности Java 15 поддерживаются в IntelliJ IDEA с версии 2020.2, выпущенной в июле 2020 года. Для настройки использования Java 15 выберите в свойствах проекта и модулей в параметре "Project SDK" значение "15", а в "Project language level" "15 (Preview) Sealed types, records, patterns, local enums and interfaces".
Также вы можете скачать Java 15 непосредственно из IntelliJ IDEA. Для этого в левой части окна "Project Structure" в разделе "Platform Settings" выберите "SDKs", затем нажмите вверху значок "+" и выберите "Download JDK". Укажите поставщика (Vendor), версию (Version) и каталог для загрузки JDK.
Возвращаемся к обработке подтипов Plant в классе Gardener
При создании sealed-иерархии вы знаете полный список наследников
и вам не нужно обрабатывать какие-то общие случаи. Ветка
else
в методе process()
класса
Gardener
никогда не будет выполнена. Однако нам
все-равно нужно оставить else
из-за
return
.
Сопоставление с образцом, добавленное в Java 14 для instanceof,
может появиться в будущих версиях Java и в выражениях
switch
. С помощью улучшенного switch
можно работать с полным списком наследников. Это позволит исключить
написание любого "обобщенного кода" для обработки ситуаций, когда
передается непредусмотренный подтип Plant
:
// Этот код не работает в Java 15.// Он будет работать в будущих версиях Java // после реализации type-test-pattern в switch int processInAFutureJavaVersion(Plant plant) { return switch (plant) { case Cucumber c -> c.harvestCucumber(); case Climber cl -> cl.sowClimber(); case Herb h -> h.sellHerb(); case Shrub s -> s.pruneShrub(); }}
Пакеты и модули
Sealed-классы и их реализации должны находиться в одном модуле. Если базовый sealed-класс определен в именованном модуле, то все его реализации должны быть определены там же. Но они могут быть в разных пакетах.
Для sealed-класса, определенного в безымянном модуле, все его реализации должны быть в одном пакете.
Правила для базовых классов и наследников классов
Классы, расширяющие sealed-класс, должны быть объявлены как
final
, non-sealed
или
sealed
. Модификатор final
запрещает
дальнейшее расширение, non-sealed
позволяет другим
классам расширять его, а sealed-подкласс должен следовать тем же
правилам, что и родительский базовый класс необходимо явно указать
список классов, которые могут его расширять.
Sealed-класс также может быть абстрактным. Его наследники могут быть как абстрактными, так и конкретными классами.
Давайте изменим набор классов, используемый в предыдущем
разделе, и определим класс Plant
как абстрактный с
абстрактным методом grow()
. Поскольку производный
класс Herb
является final-классом, то в нем должна
быть реализация метода grow()
. Non-sealed класс
Shrub
объявлен абстрактным и может не реализовывать
метод grow()
. Sealed-класс Climber
реализует абстрактный метод grow()
:
Вот измененный код:
sealed abstract public class Plant permits Herb, Shrub, Climber { abstract void grow();}final class Herb extends Plant { @Override void grow() { }}non-sealed abstract class Shrub extends Plant {}sealed class Climber extends Plant permits Cucumber{ @Override void grow() { }}final class Cucumber extends Climber {}
Если вы определяете sealed-класс и его наследников в одном файле
исходного кода, то можно опустить модификатор permits
и имена подклассов, указанных в объявлении sealed-класса. В этом
случае компилятор способен самостоятельно вывести иерархию.
Sealed-интерфейсы
Sealed-интерфейс позволяет явно указать интерфейсы, которые могут его расширять, и классы (включая записи), которые могут его реализовать. Для интерфейсов применяются правила, аналогичные sealed-классам.
Однако поскольку вы не можете объявить интерфейс с помощью
модификатора final
(иначе это противоречило бы его
назначению, так как интерфейсы должны быть реализованы) интерфейс
может быть объявлен только с использованием модификаторов
sealed
или non-sealed
. В разделе permits
перечисляются классы, которые непосредственно могут реализовать
sealed-интерфейс, и интерфейсы, которые могут его расширять.
Реализующий класс может быть final, sealed или non-sealed.
Поскольку записи, появившиеся в Java 14, неявно являются final, то
они не нуждаются в каких-либо дополнительных модификаторах:
sealed public interface Move permits Athlete, Person, Jump, Kick {}final class Athlete implements Move {}record Person(String name, int age) implements Move {}non-sealed interface Jump extends Move {}sealed interface Kick extends Move permits Karate {}final class Karate implements Kick {}
Давайте перейдем к следующему нововведению Java 15 локальные записи (record).
Записи (records)
Записи (records) предназначены для компактной записи объектов-значений (value object). Первое превью записей появилось в Java 14, а в Java 15 второе превью с некоторыми изменениями.
Если вы не знакомы с записями или хотите узнать об их поддержке в IntelliJ IDEA, то обратитесь к статье Java 14 и IntelliJ IDEA. В IntelliJ IDEA есть множество функций, которые помогут вам создавать и использовать записи.
В этом посте я расскажу об изменениях в Java 15 по сравнению с Java 14.
Java 15 позволяет определять локальные записи внутри метода. В
следующем примере метод getTopPerformingStocks()
ищет
акции (stock), которые имеют наибольшую стоимость на указанную
дату, и возвращает их названия.
List<String> getTopPerformingStocks(List<Stock> allStocks, LocalDate date) { // TopStock - локальная запись (Record) record TopStock(Stock stock, double stockValue) {} return allStocks.stream() .map(s -> new TopStock(s, getStockValue(s, date))) .sorted((s1, s2) -> Double.compare(s1.stockValue(), s2.stockValue())) .limit(2) .map(s -> s.stock.getName()) .collect(Collectors.toList());}
Локальные интерфейсы и перечисления
Java 15 также позволяет объявлять локальные перечисления и интерфейсы. Внутри метода можно инкапсулировать локальные для него данные или бизнес-логику.
public void createLocalInterface() { interface LocalInterface { void aMethod(); } // Код, использующий LocalInterface}public void createLocalEnum() { enum Color {RED, YELLOW, BLUE} // Код, использующий enum Color}
Однако в этих случаях нельзя использовать контекстные
переменные. Например, для создания значений перечисления
FOO
и BAR
нельзя использовать параметры
метода:
void test(int input) { enum Data { FOO(input), BAR(input*2); // Ошибка. Нельзя обращаться к input private final int i; Data(int i) { this.i = i; } }}
Сопоставление с образцом (pattern matching) для instanceof
Многие Java-разработчики используют оператор
instanceof
для сравнения типов. Если результат
сравнения будет true, то далее следует явное приведение к типу, с
которым сравнивали. Этот паттерн применяется довольно часто и
выглядит следующим образом: сравнение - ifTrue -
приведениеКТипу.
В Java 14 оператор instanceof
стал проще за счет
поддержки в нем сопоставления с образцом. Дополнительные переменные
и явное приведение больше не нужны, что делает ваш код безопаснее и
лаконичнее.
Это уже второе превью сопоставления с образцом для
instanceof
(изменений по сравнению с Java 14 нет).
Подробнее узнать о поддержке этой функциональности в IntelliJ IDEA вы можете в статье Java 14 и IntelliJ IDEA. Там также есть несколько интересных примеров того рефакторинга кода с помощью этой возможности и других инспекций из IntelliJ IDEA, таких как объединение вложенных if и извлечение или инлайнинг переменных.
Текстовые блоки
Многострочные строки и текстовые блоки были добавлены в Java 15 как стандартная языковая конструкция без каких-либо изменений по сравнению с Java 14.
Подробнее о поддержке текстовых блоков в IntelliJ IDEA вы можете узнать в статье Java 14 и IntelliJ IDEA.
Превью
Sealed-классы и интерфейсы появились в Java 15 в качестве превью. С новым релизным циклом в шесть месяцев новые языковые конструкции выпускаются в режиме превью. И в дальнейшем они могут появиться повторно в более поздних версиях в качестве второго или третьего превью с изменениями или без них. Как только они станут достаточно стабильными, они могут быть добавлены в стандарт языка.
Превью версии являются полноценными, но могут измениться, что, по сути, означает готовность функциональности к использованию разработчиками, но ее детали могут поменяться в будущих релизах Java в зависимости от отзывов. В отличие от API, языковые конструкции не могут в будущем быть объявлены устаревшими (deprecated). Если вы хотите высказать свое мнение о каких-либо превью возможностях, то можете сделать это в списке рассылки JDK (требуется бесплатная регистрация).
По указанной выше причине IntelliJ IDEA поддерживает превью
возможности Java только для текущего JDK. Реализация превью
возможностей может измениться от версии к версии, пока они не будут
удалены или добавлены в качестве стандарта. Код, использующий
превью возможности из более ранней версии Java SE, может не
компилироваться или не запускаться в новой версии. Например, Switch
Expressions в Java 12 использовали break для возврата значения из
ветки, а позже это было изменено на yield
. Поддержка
использования break
для возврата значения из Switch
Expressions уже отсутствует в IntelliJ IDEA.
Резюме
IntelliJ IDEA стремится не только к поддержке новых функций Java, но и к разработке для них новых проверок и инспекций.
IntelliJ
IDEA 2020.2 поддерживает все новые языковые конструкции
Java 15. Попробуйте уже сегодня sealed-классы и интерфейсы, а также
записи (record), сопоставление с образцом (pattern matching) для
instanceof
и текстовые блоки (text block). Скачать
IntelliJ IDEA вы можете по этой
ссылке.
Мы всегда рады обратной связи от наших пользователей. Не забудьте оставить отзыв о поддержке этих возможностей в IntelliJ IDEA.
А прямо сейчас в OTUS открыт набор на новый поток курса Java Developer. Professional. Приглашаю всех желающих на demo day курса, в рамках которого можно будет подробно ознакомиться с программой и процессом обучения, а также задать вопросы экспертам OTUS.