Быстрый тур по новым, готовым к работе функциям при обновлении с 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)
канонический конструктор для всех полей
геттеры для всех полей
equals
,hashCode
и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)
WASD1
10.09.2021 16:16+1Здравствуйте.
Записи (records) выглядит хорошим нововведением, делающим заголовки для меня (не-джависта) намного более читаемыми.
Но есть вопрос (он не-джависта): а зачем к каждому полю объекта в Джаве генерить геттер-сеттер и обращаться через них?
Случай, когда геттеры-сеттеры удобны, чтобы менять нижележащую логику понятен (был string address -> после рефакторинга стало TAddress address).
Но ведь удобство это "редкие случаи рефакторинга", а неудобства, это "бойлер-плейт код при создании каждого класса".
Более того: чем лучше ты знаешь свою предметную область - тем лучше ты можешь пресказать достаточно ли тут просто переменной (которую, например, можно сделать public и прямо читать-писать) или вероятность рефакторинга относительно высокая и лучше обращаться косвенно, через геттеры-сеттеры.DSolodukhin
14.09.2021 19:02Потому что такова спецификация Java Bean.
WASD1
16.09.2021 22:37спасибо за ответ.
Но я вот прочитал JavaBeans - и написано, что это не интерфейс, а скорее соглашения (фактически код-стайл придуманный корпорацией Sun Microsystems).
Если бы это был (поддерживаемый языком \ платформой) интерфейс - вопросов нет.
А так колучается довольно сомнительный, но продвигаемый на уровне стандарта код-стайл.
Зачем ему следовать, поясните пожалуйста?
ПС
Разумность следования общим шаблонам я, разумеется, понимаю - обьясните зачем следовать JavaBeans на уровне здравого смысла.DSolodukhin
17.09.2021 11:45Ну это не совсем код-стайл, это скорее «контракт». И нужно это для того, чтобы разные части кода, написанные разными людьми, находящиеся в разных библиотеках или фреймворках, могли взаимодействовать между собой. В современной Java это менее актуально, но если бы вы писали на JSF, например, вы бы поняли, как это работает.
PrinceKorwin
А если для отступов используется не пробел, а символ табуляции? Сколько пробельных символов будет в результирующей строке? И там будут пробелы или знаки табуляции?
А вообще стремное решение по мне. Ожидаю очередной виток религиозных воин на тему отступов. Тут же получается в итоге разный результат в рантайме в зависимости от того как у кого настроены отступы в IDE.
aleksandy
На этот и другие вопросы можно найти ответы в спецификаци языка.
Вкратце, не стоит отождествлять пробел и пробельный символ.
iamgal
Насколько я понимаю среды разработки не заменяют пробелы на табы ибо они внутри строки.
А вообще такие строки очень удобная штука. Пользовался таким в питоне и прямо кайфовал
Вот бы ещё и плейсходеры в строки завезли по типу
f”{self.date} - {self.time}\n”