Введение
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.