Java 15 и IntelliJ IDEA

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В 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.

  • ЗАПИСАТЬСЯ НА DEMO DAY

Источник: https://habr.com/ru/company/otus/blog/561480/


Интересные статьи

Интересные статьи

Задача интеграции сервисов и различных систем является чуть ли не одной из основных проблем современного IT. На сегодняшний день самым популярным архитектурным стилем для...
Нет более быстрого способа замедлить сайт (такой вот каламбур), чем использовать на нём кучу JavaScript-кода. При использовании JavaScript приходится расплачиваться за это производительностью про...
Возможность интеграции с «1С» — это ключевое преимущество «1С-Битрикс» для всех, кто профессионально занимается продажами в интернете, особенно для масштабных интернет-магазинов.
SonarQube — отличный инструмент для внедрения статического анализа кода в процесс разработки ПО. Он поддерживает как используемую у нас в компании Java, так и большое количество других языков п...
Эту статью следовало бы назвать «генерирование комбинаций шестерен гитары китайского токарного станка для подбора шага подачи с помощью скрипта на JavaScript», но это звучит не так интригующе. ...