Код
Java
#статьи #База знаний

Switch-выражения, класс record и запечатанные классы на практике: что нового в Java 17

Показываем главные фичи новой версии Java на реальных примерах. Это LTS-выпуск с поддержкой до 2029 года.

tesla / youtube

14 сентября 2021 года вышла семнадцатая версия Java. Это LTS-релиз (long-term support), поэтому его будут поддерживать до 2029 года. Предыдущим LTS была Java 11, которая вышла ещё в 2018 году.

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

Switch-выражения из Java 14

Чтобы получить значение из switch-выражения, раньше приходилось создавать отдельную переменную и постоянно использовать break;. Вот как это выглядело:

String season;
switch (month) {
    case JANUARY:
    case FEBRUARY:
        season = "winter";
        break;
    case MARCH:
    case APRIL:
    case MAY:
        season = "spring";
        break;
    case JUNE:
    case JULY:
    case AUGUST:
        season = "summer";
        break;
    case SEPTEMBER:
    case OCTOBER:
    case NOVEMBER:
        season = "autumn";
        break;
    case DECEMBER:
        season = "winter";
        break;
    default:
        throw new IllegalArgumentException();
    	}

В Java 14 появился новый формат записи, который помогает получать результат выбора и записывать выражение компактнее. Если перечислены все возможные варианты, ветка default теперь не нужна:

String season = switch (month) {
    case JANUARY, FEBRUARY -> "winter"; //несколько вариантов
    case MARCH, APRIL, MAY -> "spring";
    case JUNE, JULY, AUGUST -> "summer";
    case SEPTEMBER, OCTOBER, NOVEMBER -> "autumn";
    case DECEMBER -> "winter"; //oдин вариант
}

Но веткой default можно задать сообщение об ошибке:

String season = switch (month) {
    case "JAN", "FEB" -> "winter";
    default -> throw new IllegalArgumentException("no such case:" + month);
}

Если в значение (case) нужно записать выражение, его заключают в фигурные скобки {} и для возврата значения используют ключевое слово yield:

String season = switch (month) {
    case JANUARY, FEBRUARY -> "winter";
    case MARCH, APRIL, MAY -> "spring";
    case JUNE, JULY, AUGUST -> "summer";
    case SEPTEMBER, OCTOBER, NOVEMBER -> {
        System.out.println("winter is coming!");
        yield "autumn";
    }
    case DECEMBER -> "winter";
}

Switch-выражениям не обязательно возвращать определённое значение:

switch (order) {
    case NATURAL -> Arrays.sort(strings, Comparator.naturalOrder());
    case REVERSE -> Arrays.sort(strings, Comparator.reverseOrder());
}

Подробнее о switch-выражениях пишут на сайте OpenJDK — JEP 361: Switch Expressions.

Текстовые блоки из Java 15

Если раньше нужно было использовать литерал в несколько строк, его собирали через конкатенацию:

String query = "SELECT Students.name as \"Name\", SUM(Courses.duration) as \"Duration\"\n"
   + "FROM Students\n"
   + "JOIN Subscriptions ON Students.id = Subscriptions.student_id\n"
   + "JOIN Courses ON Subscriptions.course_id = Courses.id\n"
   + "GROUP BY Students.id\n"
   + "ORDER BY Students.name";

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

String query = """
   SELECT Students.name as "Name", SUM(Courses.duration) as "Duration"
   FROM Students
   JOIN Subscriptions ON Students.id = Subscriptions.student_id
   JOIN Courses ON Subscriptions.course_id = Courses.id
   GROUP BY Students.id
   ORDER BY Students.name""";
System.out.println(query);

Вот как этот код отобразится при выводе в консоль (примечание: слева не будет пробелов или отступов):

Скриншот: Мария Помазкина / Skillbox Media

Размер отступа зависит от отступа в предыдущей строке:

String query = """
   SELECT Students.name as "Name", SUM(Courses.duration) as "Duration"
       FROM Students
       JOIN Subscriptions ON Students.id = Subscriptions.student_id
       JOIN Courses ON Subscriptions.course_id = Courses.id
       GROUP BY Students.id
       ORDER BY Students.name""";
System.out.println(query);
Скриншот: Мария Помазкина / Skillbox Media

Левая граница строки общая для всего блока, правую определяет последний символ каждой строки:

Скриншот: Мария Помазкина / Skillboxa Media

Если знак """ поставить не за последним элементом, а разместить на новой строке, то после каждой строки появится перенос:

Java 15 и выше:

String query = """
   line1
   line2
   line3
   """;

Ранние версии Java:

String query = "line1\nline2\nline3\n";

Чтобы разместить всё в одну строку, нужно экранировать переносы — добавлять в конце строк символ \:

Java 15 и выше:

String query = """
   line1\
   line2\
   line3""";

Ранние версии Java:

String query = "line1line2line3";

Писать многострочные тексты стало намного проще — код приятно читается.

Подробнее о текстовых блоках читайте на сайте OpenJDK — JEP 378: Text Blocks.

Новые варианты применения instanceof из Java 16

Чтобы проверить, к какому классу относится объект, используют оператор instanceof. Если нужно проверить объект и привести его к нужному виду, раньше объявляли переменную, присваивали ей тип, а затем проверяли объект:

Object string = "this is string!";

if(string instanceof String){
   String realString = (String) string;
   System.out.println(realString);
}

Начиная с Java 16 присвоение не требуется. Значение переменной можно задать прямо в выражении:

if(string instanceof String realString){
   System.out.println(realString);
}

Если условие проверки не выполняется, оператор instanceof не ограничивается фигурными скобками внутри условия if, а проверяет код дальше:

Object object = 23;

if (!(object instanceof Number number)) {
   throw new IllegalArgumentException("this is not a Number!");
}

System.out.prin

То есть проверка !(object instanceof Number number) выдаёт результат false, и после выхода из if мы можем использовать number для реализации своей логики.

Подробности: JEP 394: Pattern Matching for instanceof.

Класс record из Java 16

Класс record — одна из самых упоминаемых фич новой версии Java. Она позволяет быстро писать иммутабельные POJO-классы, с ней не нужно повторять одинаковые методы: геттеры, toString(), equals() и hashCode().

Напишем старым методом класс Student c двумя полями — именем и названием факультета:

public class Student {
   private final String name;
   private final CourseType courseType;

   public Student(String name, CourseType courseType) {
       this.name = name;
       this.courseType = courseType;
   }

   public String getName() {
       return name;
   }

   public CourseType getCourseType() {
       return courseType;
   }

   @Override
   public boolean equals(Object o) {
       if (this == o) {
           return true;
       }
       if (o == null || getClass() != o.getClass()) {
           return false;
       }
       Student student = (Student) o;
       return name.equals(student.name) && courseType == student.courseType;
   }

   @Override
   public int hashCode() {
       return Objects.hash(name, courseType);
   }

   @Override
   public String toString() {
       return "Student{" +
              "name='" + name + '\'' +
              ", courseType=" + courseType +
              '}';
   }
}

У класса всего два поля, но много бойлерплейтного кода.

Чтобы проверить, как работает класс, сделаем два объекта, выведем их в консоль и сравним:

var student = new Student("Alex", CourseType.MATH);
var studentSame = new Student("Alex", CourseType.MATH);

System.out.println(student);
System.out.println(studentSame.equals(student));
System.out.println(studentSame.hashCode() == student.hashCode());

Так код будет выглядеть в консоли:

Скриншот: Мария Помазкина / Skillbox Media

До того как в Java появился класс record, нам помогал плагин и библиотека Lombok — она позволяет указать аннотациями, какие методы нужно сгенерировать для класса на этапе компиляции. С ней класс может выглядеть так:

@Data
@RequiredArgsConstructor
public class Student {
    private final String name;
    private final CourseType courseType;
}

@Data генерирует геттеры, сеттеры, equals(), hashCode() и toString(). @RequiredArgsConstructor создаёт конструктор с итоговыми параметрами класса и добавляет аргументы в порядке объявления. Такая запись намного компактнее, но это даётся ценой лишней зависимости и плагина для среды разработки.

Если запустить тестовый код, видно, что формат преобразования в строку отличается, но в остальном всё работает одинаково:

Скриншот: Мария Помазкина / Skillbox Media

Класс record избавляет от бойлерплейтного кода. Для этого не нужны внешние плагины, достаточно встроенных возможностей языка. Чтобы создать класс, нужно указать только два поля — конструктор, геттеры, equals(), hashCode() и toString() уже включены в класс.

Вот как класс выглядит в новой записи. Сэкономили сорок строк:

public record Student(String name, CourseType courseType) {}

Запустим тестовый код и увидим тот же результат:

Скриншот: Мария Помазкина / Skillbox Media

У геттеров больше нет приставки get., к методу мы обращаемся по имени переменной:

student.name(); //Alex
student.courseType(); //MATH

Если нужна валидация данных, конструктор можно расширить или написать свой:

Расширенный конструктор:

public record Student(String name, CourseType courseType) {

   public Student {
       if (name == null || name.isBlank()) {
           throw new IllegalArgumentException();
       }
   }
}

Кастомный конструктор:

public record Student(String name, CourseType courseType) {

   public Student(String name){
       this(name, CourseType.MATH);
   }
}

При этом стандартный конструктор со всеми параметрами класса остаётся доступным.

У класса record есть ограничения и особенности:

  • все объявленные поля получают модификатор final;
  • все поля класса объявляются в заголовке, дополнительные объявить нельзя:
//ошибка компиляции:
public record Student(String name, CourseType courseType) {
	private int id;
}
  • можно объявлять static-поля класса;
  • класс record неявно объявлен как final, поэтому его нельзя наследовать;
  • он не может быть абстрактным и наследовать другие классы;
  • можно добавлять свои конструкторы;
  • для конструктора можно использовать проверку аргументов;
  • можно переопределить стандартные методы — геттеры, toString(), equals() и hashCode();
  • можно добавлять статические и нестатические методы.

Благодаря компактному синтаксису класс records несложно обновлять локально:

public static List<Student> findTreeStudentsByAlphabet(List<Student> students) {
   record CourseTypeToFirstLetter(Student student, char firstLetter) {}

   return students.stream()
       .map(st -> new CourseTypeToFirstLetter(st, st.name().charAt(0)))
       .sorted((Comparator.comparing(CourseTypeToFirstLetter::firstLetter)))
       .map(CourseTypeToFirstLetter::student)
       .limit(3)
       .collect(Collectors.toList());
}

Это упрощает код — структура данных хранится в этом же методе, нет необходимости создавать её в другом месте и слишком расширять область видимости.

Подробнее о текстовых блоках написано на сайте OpenJDK — JEP 395: Records.

Запечатанные классы из Java 17

Sealed class дословно переводится как «запечатанный класс». В этом классе нужно сразу объявить список классов-наследников, потому что кроме них наследников быть не может. Это похоже на enum, только в разрезе наследования.

Посмотрим, как это выглядит в коде:

sealed class Person permits Student, Teacher, Curator {}

Классы Student, Teacher и Curator должны быть в том же пакете или модуле, что и Person. Кроме этого, у них обязательно должен быть один из модификаторов:

  • final, если класс запрещён к дальнейшему наследованию:
public final class Student extends Person{}
  • sealed, если наследование допустимо, но с заранее указанным списком наследников:
public sealed class Teacher extends Person permits MathTeacher, LanguageTeacher {}
  • non-sealed, когда для класса нужно снять любые ограничения по наследованию:
public non-sealed class Curator extends Person {}

У интерфейсов тоже может быть модификатор sealed:

sealed interface Person
   permits Student, Teacher, Curator {
}

С учётом record мы можем имплементировать интерфейс и записать класс Student:

public record Student(String name) implements Person{}

Здесь record по умолчанию final, поэтому ограничения класса sealed соблюдены.

Запечатанные классы помогают установить ограничение на число наследников, когда их набор определён и его не собираются часто менять. Это похоже на перечисление (enum), но sealed class гибче, потому что одни ветки наследования можно открыть для расширения, а другие ограничить только для использования.

Подробности: JEP 409: Sealed Classes.

Вывод: что за зверь такой Java 17

В этой статье мы рассказали только о самых заметных изменениях в синтаксисе. А вообще, Java 17 позволяет писать более понятный и компактный код и принёс немало полезных возможностей и ограничений.

Много JEP-нововведений (JDK Enhancement Proposal) в виртуальной машине, сборщике мусора, новых методах и уже существующих классах. Полный список изменений в Java 17 можно изучить на сайте Oracle.


Изучайте IT на практике — бесплатно

Курсы за 2990 0 р.

Я не знаю, с чего начать
Научитесь: Профессия Java-разработчик PRO Узнать больше
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована