null

Java 17. Что появилось в новой LTS версии по сравнению с 11-й?

Введение

Java 11 была выпушена в сентябре 2018 года. Спустя три года, 14 сентября 2021, свет увидел 17-ую версию языка.

За это время были разработаны и добавлены новые возможности. О некоторых из них мы поговорим в этой статье.

По мимо этого, Oracle изменила модель лицензирования. Язык Java 17 выпущен под лицензией NFTC (Oracle No-Fee Terms and Conditions). Таким образом, в отличии от 11-ой версии языка, вновь разрешено использовать Oracle JDK бесплатно для коммерческого использования.

И так, приступим к рассмотрению новых возможностей Java 17.

Текстовые блоки (text blocks)

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

Предположим, что нам необходимо определить в коде JSON строку (сложный текст) и затем вывести ее (например, в консоль).

Исходная строка:

{
  "name": "Капитан Джек Воробей",
  "age": 45,
  "address": "г. Сочи, ул. Пиратская, д. 7, кв. 777"
}

До появления текстовых блоков обычно мы определяли подобные строки следующим образом:

String text = "{\n" +
                  "  \"name\": \"Капитан Джек Воробей\",\n" +
                  "  \"age\": 45,\n" +
                  "  \"address\": \"г. Сочи, ул. Пиратская, д. 7, кв. 777\"\n" +
                  "}";
    System.out.println(text);

С использование текстового блока:

String text = """
            {
              "name": "Капитан Джек Воробей",
              "age": 45,
              "address": "г. Сочи, ул. Пиратская, д. 7, кв. 777"
            }
            """;
    System.out.println(text);

Как говорится почувствуйте разницу.

Стало намного легче писать и читать код.

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

String text = """ "Однострочный текст, не внутри блока" """;

Такой же текстовый блок считается пустым:

String text = """
            """;

и аналогичен коду:

String text = "";

 

Выражения в операторе выбора switch (switch expressions)

Выражения в операторе switch позволяют возвращать значения из оператора и использовать их далее, назначая переменным и т. д.

Рассмотрим нововведение на примере кода, который в зависимости от значения Enum, которое ему передается, выполняет некоторые действия.

Начнем с классической записи оператора switch.

switch (month) {
        case DECEMBER, JANUARY, FEBRUARY:
           {
            System.out.println("Зима");
             break;
           }
        case MARCH, APRIL, MAY:
           {
            System.out.println("Весна");
             break;
           }
        default:
            System.out.println("Не зима и не весна");
    }

Обратите внимание, что в каждом блоке case, присутствует ключевое слово break. Если мы его опустим в каком-либо из блоков, то после выполнения действий в таком блоке, будем осуществлен переход к следующему низ лежащему блоку и выполнен его код (такое поведение кода называется fall-through, что можно перевести как «проваливаться вниз»).

Давайте перепишем код выше с применением Switch expressions:

switch (month) {
        case DECEMBER, JANUARY, FEBRUARY -> System.out.println("Зима");
        case MARCH, APRIL, MAY -> System.out.println("Весна");
        default -> System.out.println("Не зима и не весна");
    }

Вновь результат на лицо. Код стал более компактным и читаемым. Ключевое слово break больше не нужно, так как поведение такого кода по умолчанию не является fall-through.

А что, если мы хотим не просто выполнить действие в зависимости от выбора, а вернуть из оператора switch значение и затем использовать его далее в коде?

Посмотрите, как это можно сделать:

    String text = switch (month) {
        case DECEMBER, JANUARY, FEBRUARY -> "Зима";
        case MARCH, APRIL, MAY -> "Весна";
        default -> "Не зима и не весна";
    };
    System.out.println(text);

Также switch можно использовать в качестве аргумента для метода:

  System.out.println(
        switch (month) {
        case DECEMBER, JANUARY, FEBRUARY -> "Зима";
        case MARCH, APRIL, MAY -> "Весна";
        default -> "Не зима и не весна";
        });

Что если нам необходимо не просто возвратить значение, но при это еще выполнить одно или более действий? На помощь приходит ключевое слово yield.

Рассмотрим пример:

String text = switch (month) {
        case DECEMBER, JANUARY, FEBRUARY -> {
            System.out.println("выбранный месяц: " + month);
            yield "Зима";
        }
        case MARCH, APRIL, MAY -> "Весна";
        default -> "Не зима и не весна";
    };
    System.out.println(text);

Ключевое слово yield можно применить и при классическом написании оператора switch. В таком случае необходимость в break отпадает:

   System.out.println(switch (month) {
        case DECEMBER, JANUARY, FEBRUARY:
            yield "Зима";
        case MARCH, APRIL, MAY:
            yield "Весна";
        default:
            yield "Не зима и не весна";
    });

 

Записи (Records)

У нас есть отдельная статья про Java records.

Вы можете найти ее по этому адресу: статья о Java Records

Приятного прочтения :)

Запечатанные или закрытые классы (Sealed classes)

Sealed классы были введены в язык для того, чтобы разработчики могли добиться гибкости в таком аспекте программирования, как наследование.

В дополнение к классам были добавлены и sealed интерфейсы.

Рассмотрим их применение на примере.

Предположим что у нас есть публичный абстрактный класс, обозначающий животное - Animal и мы его расположили в пакете com.sampleapp.first.

public abstract class Animal {}

В том же пакете мы создали еще два класса:

public final class Bird extends Animal {}
public final class Fish extends Animal {}

Как видно эти классы – наследники родительского класса Animal. Также они финальные – то есть закрыты для последующего наследования другими классами.

В другом же пакете, к примеру, com.sampleapp.second, мы в одном из файлов создали метод hereIsExtensionalProblem():

private static void hereIsExtensionalProblem(){

Bird bird = new Bird();
Fish fish = new Fish();

Animal animal = bird;

class Insect extends Animal {};

}

Как видно нет сложности в том, чтобы внутри пакета создать и использовать наши публичные классы из другого пакета. Допустимо и присвоение Bird или Fish к переменной типа Animal. Возвращаясь немного назад, хочется сказать, что, описывая классы Bird и Fish как финальные, мы на самом деле хотели, чтобы наследование Animal ограничивалось только этими классами. Однако, как видно из примера, нам доступно создание и других классов наследников – в нашем случае Insect. Как можно решить эту проблему – ограничить наследование только определенными нужными нам классами?

Одним из вариантов будет изменение видимости родительского класса -абстрактного класса Animal.

Убрав модификатор public у класса Animal, мы получим класс с видимостью package-private (доступен для наследования только внутри своего пакета).

Таким образом наш вышеуказанный код более не скомпилируется. Следующие два действия стали невозможными:

Animal animal = bird;

class Insect extends Animal {};

Запрет последнего действия – это именно то, что мы и добивались, однако вместе с этим мы лишились возможности использовать родительский класс.

Вы совершенно верно догадались, что эту и подобные проблемы как раз и решают sealed классы. Если обобщить, то их главное предназначение – это позволить родительскому суперклассу быть широкодоступным (видимым за пределами своего пакета) и в то же время не быть сколь угодно наследуемым.

Перепишем наш код, чтобы в нем теперь применялись sealed классы:

public abstract sealed class AnimalSealed permits Bird, Fish, InsectSealed{}

public non-sealed class Bird extends AnimalSealed {}

public final class Fish extends AnimalSealed {}

public sealed class InsectSealed extends AnimalSealed permits Fly, Bug {}

…..

Строку public abstract sealed class AnimalSealed permits Bird, Fish, InsectSealed{} можно прочитать так: запечатанный sealed суперкласс AnimalSealed разрешает (permits) иметь в качестве своих наследниковтолько классы Bird, Fish и InsectSeald и ни какие другие.

Обратите внимание, что классы, наследуемые от sealed класса, обязательно должны сопровождаться одним из ключевых слов, обозначающих их собственную дальнейшую судьбу в цепочке наследования: non-sealed, sealed и final. Ключевое слово sealed говорит о том, что такой наследник сам является запечатанным и разрешает дальнейшее наследование только определенному списку классов. Обозначая наследника как final, мы вообще запрещаем его дальнейшее наследование. Наконец, характеризуя подкласс как non-sealed, мы даем ему неограниченную свободу, как бы распечатывая его, – теперь он не ограничен списком разрешенных классов и в принципе доступен для наследования.

Перепишем метод hereIsExtensionalProblem так чтобы он стал hereIsNoMoreExtensionalProblem:

private static void hereIsNoMoreExtensionalProblem(){

Bird bird = new Bird();
Fish fish = new Fish();

AnimalSealed animal = bird; //Родительский класс доступен к использованию в другом пакете

class Mammal extends AnimalSealed {}; //Не скомпилируется, так как класса Mammal нет в списке разрешенных к наследованию у класса AnimalSealed

class Eagle extends Bird {}; //Все отлично, так как Bird помечен как non-sealed

class Fly extends InsectSealed {}; //Нет проблем, так как Fly есть в списке разрешенных у InsectSealed

}

Использование sealed класса накладывает три важных ограничения на его разрешенных наследников:

- Все разрешенные наследники должны принадлежать к тому же модулю, что и запечатанный родительский класс.

- Каждый разрешенный наследник должен явно наследоваться от запечатанного класса-родителя.

- Каждый разрешенный наследник должен сопровождаться одним из трех модификаторов: final, sealed или non-sealed.

Немного слов о sealed интерфейсах.

Запечатанные интерфейсы имеют схожий с запечатанными классами синтаксис:

sealed interface Flying permits Bird, FlyingSquirrel {}

Такое обозначение можно прочитать как: интерфейс Flying могут реализовывать только классы Bird и FlyingSquirrel и никакие другие классы.

Использование соответствие шаблону совеместно с оператором instanceof (Pattern matching for instanceof)

При составлении программы нередко бывает необходимо проверять объекты на принадлежность к определенному типу и затем присваивать их переменным этого типа.

К примеру:

private static void oldStyle() {
    Object o = new PhoneClass(Color.BLUE, 32);
    if (o instanceof PhoneClass) {
        PhoneClass phone = (PhoneClass) o;
        System.out.println("У этого телефона " + phone.getRamSize() + " Гб ОЗУ.");
    }
}

Используя «соответствие шаблону», мы можем переписать код выше следующим образом:

private static void patternMatching() {
     Object o = new PhoneClass (Color.BLUE, 32);
     if (o instanceof PhoneClass phone) {
        System.out.println("У этого телефона " + phone.getRamSize() + " Гб ОЗУ.");
     }
}

Как видно, теперь можно использовать переменную в проверке instanceof, и лишняя строка для создания новой переменной и приведения объекта больше не нужна. Удобно.

Содержательные NullPointerExceptions (NPE)

Рассмотрим код, который приводит к выбросу NullPointerException:

public static void main(String[] args) {
    HashMap<String, PhoneClass> phones = new HashMap<>();
    phones.put("phone1", new PhoneClass (Color.BLUE, 32));
    phones.put("phone2", new PhoneClass (Color.WHITE, 64));
    phones.put("phone3", null);
    var color = ((PhoneClass) phones.get("phone3")).getColor();
}

В Java 11 вывод с ошибкой будет примерно таким:

Exception in thread "main" java.lang.NullPointerException
        at …...main(ClassName.java:6)

В итоге, только номер строки, где возникла NPE. Что именно привело к ошибке, необходимо выяснять через отладку.

В Java 17 вывод для точно такого же кода будет иным:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "…PhoneClass.getColor()" because the return value of "java.util.HashMap.get(Object)" is null   at ….main(ClassName.java:6)

Другое дело. Вывод стал намного содержательнее. Стало легче локализовать место возникновения ошибки NPE.

Вывод чисел в удобной для человека форме

В класс NumberFormat был добавлен фабричный метод для того, чтобы форматировать числа в компактной, удобной для прочтения человеком форме в соответствии со стандартом Unicode.

Результат применения форматирования со стилем SHORT:

NumberFormat fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.SHORT);
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));

вывод:

1K
100K
1M

Результат применения форматирования со стилем LONG:

fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));

вывод:

1 thousand
100 thousand
1 million

 

Поддержка суточных интервалов

Был добавлен новый шаблон B для форматирования DateTime, который указывает суточный интервал (day period) в соответствии со стандартом Unicode.

Пример (для English Locale):

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("B");
System.out.println(dtf.format(LocalTime.of(8, 0)));
System.out.println(dtf.format(LocalTime.of(13, 0)));
System.out.println(dtf.format(LocalTime.of(20, 0)));
System.out.println(dtf.format(LocalTime.of(23, 0)));
System.out.println(dtf.format(LocalTime.of(0, 0)));

вывод:

in the morning
in the afternoon
in the evening
at night
midnight

 

Stream to List

До Java 17 для того, чтобы преобразовать Stream в List,

необходимо было вызывать метод collect в связке с Collectors.toList():

private static void beforeJava17() {
    Stream<String> stringStream = Stream.of("a", "b", "c");
    List<String> stringList =  stringStream.collect(Collectors.toList());
    for(String s : stringList) {
        System.out.println(s);
    }
}

В Java 17 всё несколько удобнее:

private static void withJava17() {
    Stream<String> stringStream = Stream.of("a", "b", "c");
    List<String> stringList =  stringStream.toList();
    for(String s : stringList) {
        System.out.println(s);
    }
}

 

Конец статьи

======================================================

Вдохновением для создания этой статьи была статья «What’s New Between Java 11 and Java 17?» автора Gunter Rotsaert.