Java 16 — новые синтаксические возможности языка

В марте этого года Oracle выпускает 16-ю версию Java, а уже осенью выйдет 17-я версия – следующая версия с долгосрочной поддержкой (LTS). Вряд ли за пол года появятся какие-то существенные нововведения, а потому уже сейчас можно взглянуть на то, с чем мы будем работать в ближайшие несколько лет. С момента выхода 11-й версии – текущей LTS версии Java, компанией Oracle было внедрено большое количество новых функций – от новых синтаксических конструкций до новых алгоритмов сборки мусора. В данной статье рассмотрим новые синтаксические возможности языка, появившиеся в версиях 12 – 16.

Записи (Records). JEP 395.

Традиционные классы в Java довольно перегружены деталями, особенно если речь идет о POJO классах, являющихся простыми неизменяемыми (immutable) агрегатами данных. Такой класс, оформленный по правилам, содержит большое количество не очень ценного и повторяющегося кода, такого как конструкторы, методы чтения полей, методы equals(), hashCode() и toString(). Например, взгляните на класс Point, предназначенный для хранения координат на плоскости:

class Point {

    private final int x;
    private final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    int x() { return x; }
    int y() { return y; }

    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point other = (Point) o;
        return other.x == x && other.y = y;
    }

    public int hashCode() {
        return Objects.hash(x, y);
    }

    public String toString() {
        return String.format("Point[x=%d, y=%d]", x, y);
    }
}

Для того, чтобы создавать такие классы было проще и компактнее, был введен новый тип класса – записи. Объявление такого класса состоит из описания его состояния, а JVM затем сама генерирует API, соответсвующее его объявлению. Это значит, что записи жертвуют некоторой свободой декларирования – возможностью отделить API класса от его внутреннего представления, но являются более компактными.

Объявление записи состоит из имени, опциональных параметров типа, заголовка и тела класса. Заголовок состоит из компонентов класса, которые являются переменными, формирующими его состояние, например:

record Point(int x, int y) { }

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

  • Для каждого компонента из заголовка генерируется финальное приватное поле и метод чтения. Обратите внимание, что методы чтения именуются не стандартным для Java способом. Например, для атрибута x из класса Point метод чтения называется x(), а не getX().

  • Публичный конструктор с сигнатурой, совпадающей с заголовком класса, который инициализирует каждое поле значением, переданным при создании объекта (канонический конструктор).

  • Методы equals() и hashCode(), которые гарантируют, что 2 записи “равны”, если они одного типа и имеют одинаковые значения соответствующих полей.

  • Метод toString().

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

record Point(int x, int y) {

  Point(int x, int y) {
    if (x < 0 || x > 100 || y < 0 || y > 100) {
      throw new IllegalArgumentException("Point coordinates must be between 0 and 100");
    }
    this.x = x;
    this.y = y;
  }
}

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

record Point(int x, int y) {

  Point {
    if (x < 0 || x > 100 || y < 0 || y > 100) {
      throw new IllegalArgumentException("Point coordinates must be between 0 and 100");
    }
  }
}

На записи накладываются некоторые ограничения:

  • Записи не могут наследоваться от других классов. Родительским классом для записи всегда является java.lang.Record. Это связано с тем, что иначе они имели бы унаследованное состояние, помимо состояния описанного в заголовке.

  • Классы записей являются финальными и не могут быть абстрактными.

  • Поля записей являются финальными.

  • Нельзя добавлять поля и блоки инициализации экземпляра.

  • Разрешается переопределять генерируемые методы, но тип возвращаемого значения должен в точности совпадать с типом значения генерируемого метода.

  • Нельзя добавлять нативные методы.

В остальном записи являются обычными классами:

  • Записи могут быть верхнеуровневыми или вложенными, могут быть параметризованными.

  • Записи могут иметь статические методы, поля и инициализаторы, а также методы экземпляра.

  • Записи могут реализовывать интерфейсы.

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

  • Класс записи и компоненты его заголовка могут быть декорированы аннотациями. Аннотации компонентов затем переносятся на поля, методы и параметры конструктора в зависимости от типа аннотации. Аннотации типов на типах компонентов также переносятся в места использования этих типов.

  • Объекты записей можно сериализовать и десериализовать, однако процесс стерилизации/десериализации нельзя настраивать writeObject(), readObject(), readObjectNoData(), writeExternal(), readExternal().

Статические члены внутренних классов

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

public class Outer {

    class Inner {

        private String id;

        private static String idPrefix = "Inner_";

        Inner(String id) {
            this.id = idPrefix + id;
        }

        static class StaticClass {
        }

        record Point(int x, int y) {
        }
    }

    public static void main(String[] args) {
        Inner inner = new Outer().new Inner("1");
        System.out.println(inner.id);

        Inner.StaticClass staticClass = new Inner.StaticClass();
        System.out.println(staticClass);

        Inner.Point point = new Inner.Point(1, 2);
        System.out.println(point);
    }
}
java  --enable-preview --source 16 Outer.java 
Inner_1
jdk16.Outer$Inner$StaticClass@6b67034
Point[x=1, y=2]

Текстовые блоки. JEP 378.

Традиционно, задавать в Java многострочный текст было не очень удобно:

String html = "<html>n" +
              "    <body>n" +
              "        <p>Hello, world</p>n" +
              "    </body>n" +
              "</html>n";

Теперь это можно сделать так:

String html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """;

Намного лаконичнее. Есть возможность разбивать длинные строки на несколько строк для удобства восприятия. Для этого используется escape-последовательность <line-terminator>, например, такую строку:

String literal = "Lorem ipsum dolor sit amet, consectetur adipiscing " +
                 "elit, sed do eiusmod tempor incididunt ut labore " +
                 "et dolore magna aliqua.";

можно представить в виде:

String text = """
              Lorem ipsum dolor sit amet, consectetur adipiscing 
              elit, sed do eiusmod tempor incididunt ut labore 
              et dolore magna aliqua.
              """;

Также появилась новая escape-последовательность s, которая транслируется в единичный пробел (u0020). Поскольку escape-последовательности транслируются после удаления пробелов в начале и конце строки, её можно использовать как барьер, чтобы помешать удалению пробелов. Например, в примере ниже последовательность s используется, чтобы сделать каждую строку длиной ровно 6 символов:

String colors = """
                red  s
                greens
                blue s
                """;

Паттерны для instanceof (Pattern Matching for instanceof). JEP 394.

Практически в каждой программе встречается код вида:

if (obj instanceof String) {
    String s = (String) obj;
    ...
}

Проблема этого кода в том, что он излишне многословен. Понятно, что после проверки типа, мы захотим привести объект к нему. Почему бы не сделать это автоматически? Для упрощения этой процедуры и были введены паттерны в оператор instanceof:

if (obj instanceof String s) {
  ...
}

Область видимости переменной s может быть как внутри блока if (как в примере выше), так и за его пределами, например:

if (!(obj instanceof String s)) {
  throw new Exception();
}
System.out.println(s);

Переменную паттерна можно использовать и в выражении оператора if:

if (obj instanceof String s && s.length() > 5) {
    System.out.println(s);
}

Однако такой пример приведет к ошибке компиляции:

if (obj instanceof String s || s.length() > 5) { // Error!
    ...
}

Переменные из паттерна могут затенять поля класса, следует быть внимательным при именовании и использовании переменных:

class Example1 {
    String s;

    void test1(Object o) {
        if (o instanceof String s) {
            System.out.println(s);      // Field s is shadowed
            s = s + "n";               // Assignment to pattern variable
            ...
        }
        System.out.println(s);          // Refers to field s
        ...
    }
}

class Example2 {
    Point p;

    void test2(Object o) {
        if (o instanceof Point p) {
            // p refers to the pattern variable
            ...
        } else {
            // p refers to the field
            ...
        }
    }
}

Изолированные типы (Sealed Classes). JEP 397.

Изолированные классы и интерфейсы могут быть расширены и реализованы только теми классами и интерфейсами, которым это разрешено. Это позволяет передать компилятору знания о том, что существует ограниченная иерархия каких-либо классов. Для объявления изолированных типов используется модификатор sealed. Затем, после ключевых слов extends и implements идет ключевое слово permits, после которого перечисляются классы, которым разрешено расширять или реализовывать данный класс/интерфейс. Взглянем на пример:

package com.example.geometry;

public abstract sealed class Shape
    permits Circle, Rectangle, Square { ... }
... class Circle    extends Shape { ... }
... class Rectangle extends Shape { ... }
... class Square    extends Shape { ... }

Классы, перечисленные после ключевого слова permits должны находиться рядом с родительским классом: в том же модуле или пакете. Если они малы и их не так много, их можно разместить в одном файле с родительским классом, в этом случае ключевое слово permits можно опустить. Каждый дочерний класс должен быть прямым наследником изолированного класса. Каждый дочерний класс должен использовать один из трех модификаторов:

  • Модификатор final, если иерархия типов не должна расширяться далее.

  • Модификатор sealed, если иерархия типов может расширяться далее, но в ограниченном ключе.

  • Модификатор non-sealed, если эта часть иерархии может расширяться произвольным образом.

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

Shape rotate(Shape shape, double angle) {
    if (shape instanceof Circle) return shape;
    else if (shape instanceof Rectangle) return shape.rotate(angle);
    else if (shape instanceof Square) return shape.rotate(angle);
    // no else needed!
}

Однако, мне так и не удалось заставить такой код работать (возможно, потому что это все еще превью реализация):

public class Main {

    static abstract sealed class Shape permits Rect, Circle {
    }

    static final class Rect extends Shape {
    }

    static final class Circle extends Shape {
    }

    public Shape getShape(Shape shape) {
        if (shape instanceof Rect) return shape;
        else if (shape instanceof Circle) return shape;
    }

    public static void main(String[] args) {
        new Main().getShape(new Rect());
    }
}
javac -Xlint:preview --enable-preview --release 16 Main.java 
Main.java:9: warning: [preview] sealed classes are a preview feature and may be removed in a future release.
    static abstract sealed class Shape permits Rect, Circle {
                    ^
Main.java:9: warning: [preview] sealed classes are a preview feature and may be removed in a future release.
    static abstract sealed class Shape permits Rect, Circle {
                    ^
Main.java:9: warning: [preview] sealed classes are a preview feature and may be removed in a future release.
    static abstract sealed class Shape permits Rect, Circle {
                                       ^
Main.java:21: error: missing return statement
    }
    ^
1 error
3 warnings

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

Использование оператора switch чревато ошибками из-за его сквозной семантики. Взгляните на пример:

switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

Из-за большого количества ключевых слов break легко запутаться и пропустить его где-то.

Кроме того, очень часто оператор switch используется для эмуляции switch выражения, но это не удобно и тоже чревато ошибками:

int numLetters;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        numLetters = 6;
        break;
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
    default:
        throw new IllegalStateException("Wat: " + day);
}

Для решения перечисленных проблем был введен новый способ записи условий в операторе switch в виде “case L ->” и сам оператор стал еще и выражением.

Если условие записано в виде “case L ->”, то при его срабатывании выполняется только инструкция справа от него. Сквозная семантика в этом случае не работает. Пример такой записи:

static void howMany(int k) {
    switch (k) {
        case 1  -> System.out.println("one");
        case 2  -> System.out.println("two");
        default -> System.out.println("many");
    }
}

Теперь рассмотрим пример switch выражения:

static void howMany(int k) {
    System.out.println(
        switch (k) {
            case  1 -> "one";
            case  2 -> "two";
            default -> "many";
        }
    );
}

Большинство выражений будут иметь единственную инструкцию справа от условия “case L ->”. На случай, если понадобится целый блок, вводится ключевое слово yield для возврата значения из выражения:

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};

Условия в switch выражении должны быть исчерпывающими, то есть охватывать все возможные варианты. На практике это означает, что обязательно присутствие общего условия – default (в случае с простым оператором switch это не обязательно). Однако, в случае со switch выражениями на enum типах, которые покрывают все возможные константы, наличие общего условия необязательно. В таком случае, при добавлении новой константы в enum, компилятор выдаст ошибку, чего не случилось бы, будь общее условие задано.

Заключение

В данной статье мы рассмотрели новые синтаксические возможности Java 16: записи, текстовые блоки, паттерны для instanceof, изолированные типы и switch выражения. Стоит отметить, что изолированные типы все еще находятся на стадии preview, а потому в Java 17 могут и не войти.

Ссылки

Let’s block ads! (Why?)

Read More

Recent Posts

Роскомнадзор рекомендовал хостинг-провайдерам ограничить сбор данных с сайтов для иностранных ботов

Центр управления связью общего пользования (ЦМУ ССОП) Роскомнадзора рекомендовал компаниям из реестра провайдеров ограничить доступ поисковых ботов к информации на российских сайтах.…

15 часов ago

Apple возобновила переговоры с OpenAI и Google для интеграции ИИ в iPhone

Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…

5 дней ago

Российская «дочка» Google подготовила 23 иска к крупнейшим игрокам рекламного рынка

Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…

6 дней ago

Google завершил обновление основного алгоритма March 2024 Core Update

Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…

6 дней ago

Нейросети будут писать тексты объявления за продавцов на Авито

У частных продавцов на Авито появилась возможность составлять текст объявлений с помощью нейросети. Новый функционал доступен в категории «Обувь, одежда,…

6 дней ago

Объявлены победители международной премии Workspace Digital Awards-2024

24 апреля 2024 года в Москве состоялась церемония вручения наград международного конкурса Workspace Digital Awards. В этом году участниками стали…

6 дней ago