Быстрый тур по новым, готовым к работе функциям при обновлении с Java 11 до Java 17.

Через три года после Java 11 - на данный момент последней версии с долгосрочной поддержкой (LTS), Java 17 LTS будет выпущена в сентябре 2021 года. Пришло время сделать краткий обзор новых функций, которыми разработчики могут пользоваться после обновления с 11 до 17. Обратите внимание, что было внесено гораздо больше улучшений - в этой статье основное внимание уделяется тем функциям, которые могут напрямую использоваться большинством разработчиков:

  • Switch выражения (JEP 361)

  • Текстовые блоки (JEP 378)

  • Инструмент для упаковки (JEP 392)

  • Сопоставление с образцом для instanceof (JEP 394)

  • Записи (JEP 395)

  • Запечатанные классы (JEP 409)

Switch выражения

Теперь switch может возвращать значение, как и выражение:

// assign the group of the given planet to a variable
String group = switch (planet) {
  case MERCURY, VENUS, EARTH, MARS -> "inner planet";
  case JUPITER, SATURN, URANUS, NEPTUNE -> "outer planet";
};

Если правая часть одного case требует большего количества кода, его можно записать внутри блока, а значение возвращается с помощью yield:

// print the group of the given planet, and some more info,
// and assign the group of the given planet to a variable
String group = switch (planet) {
  case EARTH, MARS -> {
    System.out.println("inner planet");
    System.out.println("made up mostly of rock");
    yield "inner";
  }
  case JUPITER, SATURN -> {
    System.out.println("outer planet");
    System.out.println("ball of gas");
    yield "outer";
  }
};

Однако switch с использованием новых меток со стрелками не требует возврата значения, как и void выражение:

// print the group of the given planet
// without returning anything
switch (planet) {
  case EARTH, MARS -> System.out.println("inner planet");
  case JUPITER, SATURN -> System.out.println("outer planet");
}

По сравнению с традиционным переключателем, новое Switch выражение

  • Использует «->» вместо «:»

  • Позволяет использовать несколько констант для каждого case

  • Не имеет сквозной семантики (т. е. не требует break)

  • Делает переменные, определенные внутри ветви case, локальными для этой ветви

Более того, компилятор гарантирует полноту переключения в том смысле, что выполняется ровно один из случаев, что означает, что либо

  • Все возможные значения перечислены как case (как в приведенном выше перечислении, состоящем из восьми планет), или

  • Должна быть предоставлена ​​ветка «default».

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

Текстовые блоки позволяют писать многострочные строки, содержащие двойные кавычки, без использования \n или \" escape-последовательностей:

String block = """
  Multi-line text
   with indentation
    and "double quotes"!
  """;

Текстовый блок открывается тремя двойными кавычками, """за которыми следует разрыв строки, и закрывается тремя двойными кавычками.

Компилятор Java использует интеллектуальный алгоритм для удаления начального пробела из результирующей строки, чтобы:

  • отступ, необходимый только для лучшей читаемости исходного кода Java, был удален.

  • отступ, относящийся к самой строке, остался нетронутым

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

Multi-line.text
.with.indentation
..and."double.quotes"!

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

String block = """
  Multi-line text
   with indentation
    and "double quotes"!
""";

Результат представлен в следующей строке:

..Multi-line.text
...with.indentation
....and."double.quotes"!

Кроме того, из каждой строки удаляется конечный пробел, чего можно избежать, используя новую escape-последовательность \s.

Разрывы строк внутри текстовых блоков можно экранировать:

String block = """
    No \
    line \
    breaks \
    at \
    all \
    please\
    """;

Результатом является следующая строка без разрывов строк:

No.line.breaks.at.all.please

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

String block = """
    No final line break
    at the end of this string, please""";

Вставка переменных в текстовый блок может выполняться как обычно с помощью статического метода String::format или с помощью нового метода экземпляра String::formatted, который немного короче для записи:

String block = """
    %s marks the spot.
    """.formatted("X");

Инструмент для упаковки

Предположим, у вас есть JAR-файл demo.jar в каталоге lib вместе с дополнительными JAR-файлами зависимостей. Следующая команда:

jpackage --name demo --input lib --main-jar demo.jar --main-class demo.Main

упаковывает это демонстрационное приложение в собственный формат, соответствующий вашей текущей платформе:

  • Linux: deb или rpm

  • Windows: msi или exe

  • macOS: pkg или dmg

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

Кросс-компиляция не поддерживается: если вам нужен пакет для пользователей Windows, вы должны создать его с помощью jpackage на машине Windows.

Создание пакета можно настроить с помощью многих других параметров, которые задокументированы на странице руководства jpackage.

Сопоставление с образцом для Instanceof

Сопоставление с образцом (Pattern matching) для instanceof позволяет исключить шаблонный код для выполнения приведений после сравнения типов:

Object o = "string disguised as object";
if (o instanceof String s) {
  System.out.println(s.toUpperCase());
}

В приведенном выше примере область действия новой переменной s интуитивно ограничена if веткой. Чтобы быть точным, переменная находится в области видимости, в которой гарантировано совпадение шаблона, что также делает следующий код допустимым:

if (o instanceof String s && !s.isEmpty()) {
  System.out.println(s.toUpperCase());
}

А также наоборот:

if (!(o instanceof String s)) {
  throw new RuntimeException("expecting string");
}
// s is in scope here!
System.out.println(s.toUpperCase());

Записи

Записи (Records) сокращают шаблонный код для классов, которые являются простыми носителями данных:

record Point(int x, int y) { }

Эта строка кода в результате приводит к созданию класса записи, в котором автоматически определены:

  • поля для x и y (как private и final)

  • канонический конструктор для всех полей

  • геттеры для всех полей

  • equalshashCode и toString (с учетом всех полей)

// canonical constructor
Point p = new Point(1, 2);
    
// getters - without "get" prefix
p.x();
p.y();
    
// equals / hashCode / toString
p.equals(new Point(1, 2)); // true
p.hashCode();              // depends on values of x and y
p.toString();              // Point[x=1, y=2]

Некоторые из наиболее важных ограничений классов записей заключаются в том, что они:

  • неизменяемы (поскольку их поля являются private и final)

  • неявно final

  • невозможно определить дополнительные поля экземпляра

  • всегда наследует от Record класса

Однако можно:

  • определить дополнительные методы

  • реализовать интерфейсы

  • кастомизировать канонический конструктор и аксессоры

record Point(int x, int y) {

  // explicit canonical constructor
  Point {

    // custom validations
    if (x < 0 || y < 0) 
      throw new IllegalArgumentException("no negative points allowed");

    // custom adjustments (usually counter-intuitive)
    x += 1000;
    y += 1000;

    // assignment to fields happens automatically at the end

  }
  
  // explicit accessor
  public int x() {
    // custom code here...
    return this.x;
  }
}

Кроме того, внутри метода можно определить локальную запись:

public void withLocalRecord() {
  record Point(int x, int y) { };
  Point p = new Point(1, 2);
}

Sealed классы

Sealed (запечатанный) класс явно перечисляет допустимые прямые подклассы. Другие классы не могут наследовать от этого класса:

public sealed class Parent
  permits ChildA, ChildB, ChildC { ... }

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

sealed interface Parent
  permits ChildA, ChildB, ChildC { ... }

Классы или интерфейсы в permits списке должны находиться в одном пакете (или в том же модуле, если родитель находится в названном модуле).

permits список может быть опущена, если подклассы (или интерфейсы) расположены в том же файле:

public sealed class Parent {
  final class Child1 extends Parent {}
  final class Child2 extends Parent {}
  final class Child3 extends Parent {}
}

Каждый подкласс или интерфейс в permits списке должен использовать только один из следующих модификаторов:

  • final (запрещает дальнейшее наследование; только для подклассов, поскольку интерфейсы не могут быть final)

  • sealed (допускает дальнейшее, ограниченное наследование)

  • non-sealed (снова разрешает неограниченное наследование)

Комментарии (8)


  1. PrinceKorwin
    02.09.2021 11:49

    Компилятор Java использует интеллектуальный алгоритм для удаления начального пробела из результирующей строки,

    А если для отступов используется не пробел, а символ табуляции? Сколько пробельных символов будет в результирующей строке? И там будут пробелы или знаки табуляции?

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


    1. aleksandy
      03.09.2021 07:33

      А если для отступов используется не пробел, а символ табуляции? Сколько пробельных символов будет в результирующей строке? И там будут пробелы или знаки табуляции?

      На этот и другие вопросы можно найти ответы в спецификаци языка.

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


    1. iamgal
      13.09.2021 14:18
      +1

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

      А вообще такие строки очень удобная штука. Пользовался таким в питоне и прямо кайфовал

      Вот бы ещё и плейсходеры в строки завезли по типу

      f”{self.date} - {self.time}\n”


  1. WASD1
    10.09.2021 16:16
    +1

    Здравствуйте.
    Записи (records) выглядит хорошим нововведением, делающим заголовки для меня (не-джависта) намного более читаемыми.

    Но есть вопрос (он не-джависта): а зачем к каждому полю объекта в Джаве генерить геттер-сеттер и обращаться через них?
    Случай, когда геттеры-сеттеры удобны, чтобы менять нижележащую логику понятен (был string address -> после рефакторинга стало TAddress address).
    Но ведь удобство это "редкие случаи рефакторинга", а неудобства, это "бойлер-плейт код при создании каждого класса".
    Более того: чем лучше ты знаешь свою предметную область - тем лучше ты можешь пресказать достаточно ли тут просто переменной (которую, например, можно сделать public и прямо читать-писать) или вероятность рефакторинга относительно высокая и лучше обращаться косвенно, через геттеры-сеттеры.


    1. DSolodukhin
      14.09.2021 19:02

      Потому что такова спецификация Java Bean.


      1. WASD1
        16.09.2021 22:37

        спасибо за ответ.
        Но я вот прочитал JavaBeans - и написано, что это не интерфейс, а скорее соглашения (фактически код-стайл придуманный корпорацией Sun Microsystems).
        Если бы это был (поддерживаемый языком \ платформой) интерфейс - вопросов нет.

        А так колучается довольно сомнительный, но продвигаемый на уровне стандарта код-стайл.
        Зачем ему следовать, поясните пожалуйста?

        ПС
        Разумность следования общим шаблонам я, разумеется, понимаю - обьясните зачем следовать JavaBeans на уровне здравого смысла.



        1. DSolodukhin
          17.09.2021 11:45

          Ну это не совсем код-стайл, это скорее «контракт». И нужно это для того, чтобы разные части кода, написанные разными людьми, находящиеся в разных библиотеках или фреймворках, могли взаимодействовать между собой. В современной Java это менее актуально, но если бы вы писали на JSF, например, вы бы поняли, как это работает.


  1. Nick252
    13.09.2021 14:18
    -1

    Здорово наблюдать, как некоторые возможности Java из Котлина заимствует.