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. Это связано с тем, что иначе они имели бы унаследованное состояние, помимо состояния описанного в заголовке.

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

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

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

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

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

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

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

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

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

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

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

    • Объекты записей можно сериализовать и десериализовать, однако процесс сериaлизации/десериализации нельзя настраивать 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
                    green\s
                    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
    

    UPD: как выяснилось в комментариях, ожидание анализа на exhaustiveness в цепочках if-else появилось в результате ошибочного понимания объяснения в JEP.

    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 могут и не войти.

    Ссылки

    Комментарии 79

      +4

      Ну в общем, они взяли весь сахар из Котлина. И это замечательно!

        +1

        Data/Case классы как по мне наиболее ожидаемое.
        С Null правда ещё сложно, хоть и есть optional.
        Чуть синтаксиса со стримами и make Java great again.

          +4
          Не совсем. Скорее из Скалы (оттуда же Котлин фичи надергал). Скала тоже многое из других языков тянет, но в классе языков под JVM на мой взгляд она самая продвинутая. И все равно Java пока сильно отстает и от Котлина, и от Скалы. И, в связи с ее спецификой, догонять будет еще очень долго. У меня даже ощущение, что JVM переживет саму Java. :)
            +2

            Да ну нет же. Надергали фичей, но не весь сахар.

              +5

              На мой взгляд, это слишком легкомысленное замечание. Во-первых, речь не только о сахаре. Например, рекорды — это модельная сущность, nominal tuple, тип-произведение, если хотите. Это штука с сильной семантикой, а не просто синтаксический шорткат. Во-вторых, "взяли из Котлина" — это просто неверно. Синтаксически эти штуки непохожи на Котлин, а, например, instanceof patterns вообще в Котлине прямого аналога не имеют. У Котлина иной путь — flow typing, который в Java никто тащить не собирается. Sealed types — это больше не фича языка Java, а фича виртуальной машины: проверка иерархии делается на уровне VM, и там надо было аккуратно поработать с раздельной компиляцией и т. д. И теперь как раз котлиновские sealed-типы адаптируют, чтобы они были sealed-типами на уровне JVM. И, кстати, sealed-интерфейсов в Котлине не было, и теперь их затаскивают в Котлин из Java. Тоже берут сахар? Даже текстовые блоки синтаксически похожи совсем не на Котлин (где они плохо сделаны), а на Swift. За каждой из этих фич годы исследований и дизайна, а не просто "давайте сделаем как в языке X".

                +1
                а, например, instanceof patterns вообще в Котлине прямого аналога не имеют.

                Шта? Вообще-то это одна из ключевых фишек языка — вывод типов где только возможно


                    fun test(any: Any): String = when(any){
                        is String -> any.replace('a', 'b')
                        is Double -> sin(any).toString()
                        is Thread -> any.isAlive.toString()
                        else -> "rrrrrr"
                    }
                  +1

                  Это и называется flow typing. И в Java он не реализован и не будет. Об этом я и говорю. Вместо этого в джаве вводится новая переменная — такого синтаксиса в Котлине нет.

              –6

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

                +8
                ну их же не обязательно знать на старте, все это просто сахар. Для чтения другого кода сложнее, а так можно и по старым туториалам и книгам учить классическую Java.
                  +1

                  А лучше по новым. Например, изучать только новый синтаксис switch, который удобнее и логичнее. А старый с двоеточием не изучать вообще или оставить как дополнительный материал.

                    –1

                    Извините, но читать другой код это ключевое.

                  0
                  Однако, мне так и не удалось заставить такой код работать

                  Как минимум не хватает “else if (shape == null) ...”
                  Пробовали добавить?

                    0
                    Попробовал — ошибка та же (missing return statement). Начинает работать только после добавления ветки «else».
                      +1

                      Никто нигде никогда не обещал, что в цепочках if-else будет анализ на exhaustiveness. Он планируется в switch, когда там прикрутят паттерны. Но пока этого нет.

                        +1
                        Меня натолкнул на эту мысль вот этот абзац из JEP:
                        Sealing a class restricts its subclasses. User code can inspect an instance of a sealed class with an if-else chain of instanceof tests, one test per subclass; no catch-all else clause is needed. For example, the following code looks for the three permitted subclasses of Shape:

                        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!
                        }


                        Пересмотрел JEP еще раз — в нем есть и такое утверждение:
                        For example, consider this code from earlier:
                        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!
                        }
                        The Java compiler cannot ensure that the instanceof tests cover all the permitted subclasses of Shape. For example, no compile-time error message would be issued if the instanceof Rectangle test was omitted.


                        Немного запутанное описание, но, похоже, вы правы.
                          +1

                          Согласен, написано немного запутано.

                    +1
                    Мне очень понравилось вот это — Moving ZGC (Z Garbage Collector) thread-stack processing from safepoints to a concurrent phase Обещают, что затраты на пропускную способность улучшенной задержки должны быть незначительными, а время, проведенное в ZGC safepoints на типичных машинах, должно быть меньше одной миллисекунды.
                      +1

                      После switch expressions очень хочется, чтобы такую же штуку сделали с if-else. Ну или просто завезли when, как в Kotlin.

                        +3
                        Название статьи: «Java 16 — новые синтаксические возможности языка»
                        В тексте: " В данной статье рассмотрим новые синтаксические возможности языка, появившиеся в версиях 12 — 16."
                        Читал по диагонали, и, увидев в списке фич текстовые блоки, первая мысль была: «Что!? Опять?!!!»
                          +4
                          Остров Джава становится все более cкалистый )
                            +6
                            Всё более острый (sharpen)
                              +3
                              Да нет, именно скалистый (Scala)
                            0
                            Область видимости переменной s может быть как внутри блока if (как в примере выше), так и за его пределами

                            Так и не понял, всё-таки область видимости внутри или и за пределами тоже? Если область видимости может быть переменной, то от чего зависит?
                              +1
                              Она может быть и внутри и за пределами — зависит от условия if.
                              Например, тут она будет внутри:
                              if (obj instanceof String s) {
                                  System.out.println(s);
                              }
                              

                              А тут за пределами:
                              if (!(obj instanceof String s)) {
                                throw new Exception();
                              }
                              System.out.println(s);
                              
                                +3
                                Не поверил, запустил у себя. Впервые вижу динамическую область видимости, еще и зависящую от условия if'а. Мир больше не будет прежним.
                                  +1

                                  Тогда вам совсем вынесет мозг такой пример:



                                  Здесь область видимости переменной паттерна состоит из двух несвязанных кусков, разделённых телом if'а.

                                    0
                                    Здесь, как раз, нет. Фактически это одна область, если определять её не в исходниках, а в байткоде. Меня удивила именно вариативность области видимости.
                                      0

                                      Байткоду-то какая разница? В байткоде это тоже может быть две области. Байткод тут ни при чём.

                                        0
                                        Я всего лишь пытаюсь понять логику. Если за таким поведением кроется более простая техническая реализация — для меня это понятно и логично. Недоумение вызвала бы только попытка вынуть гланды через неподходящие отверстия только ради самого процесса)
                                          0
                                          Я тут Dart пишу, скоро в 2.12 будет null safety (AKA non-nullable by default), и для этого прикрутили в язык flow analysis, которому например не нужно объявлять новую переменную, просто instance of делает type promotion существующей переменной. Возможно в Java решили, что type promotion слишком радикально вводить, и вместо этого добавили сахар экономии type cast(а).
                                      0
                                      Жесть какая-то)
                                0
                                java  --enable-preview --source 16 Outer.java 
                                Inner_1
                                jdk16.Outer$Inner$StaticClass@6b67034
                                Point[x=1, y=2]

                                А зачем здесь preview? В Java 16 это стандартная фича.

                                  0
                                  Да, в этом случае не нужен — запускал разные примеры и флаг остался от предыдущего запуска.
                                  +1
                                  Обратите внимание, что методы чтения именуются не стандартным для Java способом.

                                  Хм. String.length(), Collection.size(), Process.pid(), Process.exitValue(), Runtime.availableProcessors(), Runtime.freeMemory(), ThreadGroup.activeCount() и т. д. Я бы не сказал, что префикс get — это прямо "стандарт".

                                    +3
                                    Скорее всего, имелся ввиду стандарт именования JavaBeans, на который завязаны многие фреймворки и утилиты.
                                      +3

                                      Это не стандарт, а соглашение. Стандарт говорит "Accessor methods can have arbitrary names" (JavaBeans 1.01a, 7.1). В разделе 8.3 действительно описывается именование на get/is, но в разделе 8.2 ещё раз подчёркивается "However, within Java Beans the use of method and type names that match design patterns is entirely optional".


                                      Фреймворки и утилиты не так уж сильно уже завязаны на него и могут адаптироваться. К примеру, Jackson давно адаптировался.

                                        0

                                        Нет, jackson адаптирован начиная с версии 2.12 которая релизнулась всего несколько недель назад.

                                      +2

                                      Методы (геттеры) с префиксом количественно доминируют, а значит они — стандарт де-факто.

                                        0
                                        lany, именно это я и имел ввиду, говоря о стандарте JavaBeans.
                                      0
                                      Ну почему нельзя было сделать record'ы как @Value-классы в Lombok? Невозможность наследовать сводит все преимущества record'ов на нет: если есть Point(x, y), то очень хотелось бы отнаследовать от него Point3D(x, y, z).
                                        +9

                                        К сожалению, старая школа ООП вбила этот ужасный паттерн наследования реализаций. Наследование — это специализация. Совершенно очевидно, что точка в трёхмерном пространстве не является специальным случаем точки в двумерном пространстве. В таком наследовании нет смысла.


                                        Возможность наследования убила бы инварианты рекордов. Например, совершенно непонятно как правильно автоматически сгенерировать equals для рекорда, если его можно наследовать. Ломбок не заморачивается семантикой и дизайном языка, он просто пихает фичи. Ломбок как раз является синтаксическим сахаром в отличие от рекордов, которые несут семантическую нагрузку.

                                          +1
                                          Совершенно очевидно, что точка в трёхмерном пространстве не является специальным случаем точки в двумерном пространстве

                                          Эх, так и знал, что нужно было приводить другой пример. ОК, давайте тогда возьмем классы Shape, Square, Cirle, у которых есть поле center типа Point и метод getArea().
                                          совершенно непонятно как правильно автоматически сгенерировать equals для рекорда, если его можно наследовать

                                          Ну вот IDEA же как-то с этим справляется (если проставить флажок «Accept subclasses as parameter to equals() method», а если проставить флажок «Use getters during code generation», так вообще красота получается):
                                          Пример кода, который сгенерировала IDEA
                                          public abstract class Shape {
                                              private final int x;
                                              private final int y;
                                          
                                              public Shape(int x, int y) {
                                                  this.x = x;
                                                  this.y = y;
                                              }
                                          
                                              public int getX() {
                                                  return x;
                                              }
                                          
                                              public int getY() {
                                                  return y;
                                              }
                                              
                                              public abstract double getArea();
                                          
                                              @Override
                                              public boolean equals(Object o) {
                                                  if (this == o) return true;
                                                  if (!(o instanceof Shape)) return false;
                                                  Shape shape = (Shape) o;
                                                  return getX() == shape.getX() && getY() == shape.getY();
                                              }
                                          
                                              @Override
                                              public int hashCode() {
                                                  return Objects.hash(getX(), getY());
                                              }
                                          }
                                          
                                          public class Circle extends Shape {
                                              private final int radius;
                                          
                                              public Circle(int x, int y, int radius) {
                                                  super(x, y);
                                                  this.radius = radius;
                                              }
                                          
                                              public int getRadius() {
                                                  return radius;
                                              }
                                              
                                              @Override
                                              public double getArea() {
                                                  return Math.PI * getRadius() * getRadius();
                                              }
                                          
                                              @Override
                                              public boolean equals(Object o) {
                                                  if (this == o) return true;
                                                  if (!(o instanceof Circle)) return false;
                                                  if (!super.equals(o)) return false;
                                                  Circle circle = (Circle) o;
                                                  return getRadius() == circle.getRadius();
                                              }
                                          
                                              @Override
                                              public int hashCode() {
                                                  return Objects.hash(super.hashCode(), getRadius());
                                              }
                                          }
                                          

                                            +3

                                            Всё просто. Shape — это интерфейс. Рекорды могут реализовывать интерфейсы. Есть у него точка в виде поля или нету — никого волновать не должно. Квадрат может быть удобно задать центром и длиной стороны, а может быть углом и длиной стороны. В одном из случаев метод, который возвращает центральную точку, не будет тривиальным геттером, а будет вычислять значение. Должна быть возможность тривиальным образом сменить одну реализацию на другую, не сломав ни одного клиента. Абстракция! Рекорд по определению имеет прозрачную структуру, он не обеспечивает абстракции.


                                            Ну вот IDEA же как-то с этим справляется

                                            Тут, например, можно почитать на эту тему.

                                            0
                                            Например, совершенно непонятно как правильно автоматически сгенерировать equals для рекорда, если его можно наследовать.

                                            В C# это сделали, добавив в сравнение виртуальное свойство EqualityContract, которое возвращает реальный тип:


                                            var b = new Base("Hello");
                                            var d = new Derived("Hello", "World");
                                            Console.WriteLine(b.Equals(d));

                                            Base.Equals сравнит не только два Hello, но и сами типы, а потому вернёт false.

                                          0
                                          А нет подобной статьи для Java 8 — Java 11 ?))
                                          +1
                                          non-sealed

                                          Служебное слово с дефисом это, конечно, образцово-показательный ХХВП. Из сотен инженеров никто не потрудился нормальный антоним подобрать.

                                            +2

                                            Чего ж ты не написал свои гениальные идеи в мейлинг-лист? :-) Кстати, ещё не поздно: фича не финализирована, ключевое слово вполне можно поменять. Иди в amber-spec-comments и предлагай! На Хабре-то тебя Брайан не прочитает :-)

                                              +1

                                              Формальных антонимов там не так уж много:


                                              • open
                                                — агащаз, никто такое ключевое слово делать не станет, иначе накроет весь язык, и первой будет стандартная библиотека
                                              • unsealed
                                                — ну понятно, да?
                                              • unrestricted
                                                — семантически не подходит, потому что подразумевает отсутствие любых препятствий, а никто не хочет настолько сильные требования вводить с налёта.
                                              • non-sealed
                                                — Слово составное и страшноватое, зато абсолютно безопасно в качестве ключевого слова — потому что единственный случай, когда из-за этого что-то перестанет компилироваться — это если у вас есть две переменные, non и sealed, и вы из первой вычитаете вторую, а форматировать с пробелами почему-то не захотели.

                                              Вот и получается, что либо так слегка кривовато, либо unsealed, который тоже не особо-то прямоватый, и кому-нибудь опять же триггеры зажмёт.

                                                +1

                                                Ну теоретически можно сделать контекстно-зависимое ключевое слово open, которое является ключевым только в списке модификаторов. Собственно, слово sealed так и добавлено: вы спокойно можете иметь метод sealed или переменную sealed.

                                                  0

                                                  Со словом open другая проблема — это уже ключевое слово! Да-да, только оно используется исключительно в файлах module-info.java, означая, что в данном модуле все классы доступны в рантайме (к примеру, для грязного рефлекшна). Кажется, что это может привнести некоторую путаницу: если класс объявлен open, это может быть воспринято в том же контексте (другим модулям разрешено лазить в потроха класса).

                                                    +1

                                                    Ну так и быть, вкинул слово 'open'. А то Maccimo вряд ли сам напишет. Посмотрим.

                                                      0

                                                      А вот и ответ уже, от Самого.
                                                      https://mail.openjdk.java.net/pipermail/amber-spec-experts/2021-January/002806.html


                                                      Так что будет non-sealed.

                                                        +1

                                                        Ну вот да. Я, кстати, не стал специально писать про смысл open в module-info, но и Реми, и Брайан сами про это упомянули. Maccimo, иди ворвись и скажи им, что они не потрудились :-)

                                                          0

                                                          У вас там элитарный экспертный клуб с фейс-контролем при подписке, ворваться не получится.

                                                            0

                                                            Я же тебе сказал, в amber-spec-comments, даже ссылку приложил.

                                                        0

                                                        Пока что не понятно зачем отдельный модификатор non-sealed вообще нужен, кроме как для «The goal is to make it Java-ish, by adopting this convention». Если класс не объявлен явно как final или sealed, то по логике вещей он как раз non-sealed.


                                                        Особенно забавно это буйство новых ключевых слов сочетается с фразой «It is a common (and often deserved) complaint that "Java is too verbose"» из Data Classes and Sealed Types for Java.


                                                        А то Maccimo вряд ли сам напишет.

                                                        Не в *-experts точно, ибо "You probably want amber-spec-observers".

                                                          0
                                                          Если класс не объявлен явно как final или sealed, то по логике вещей он как раз non-sealed.

                                                          Этот вопрос обсуждали, естественно. Смысл в том, что хорошего дефолта нет, а если сделать, что дефолт = non-sealed, то люди будут просто по забывчивости или лени открывать свои иерархии. Поэтому надо явно указывать.

                                                            0

                                                            Руководствуясь такой логикой можно начать требовать non-final, non-volatile, non-strictfp и так далее. Вдруг программист забыл?

                                                              0

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

                                                                0

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

                                                                  0

                                                                  Я бы предпочёл для этого аннотацию, по аналогии с @Override. К ключевым словам-маркерам у меня идиосинкразия со времён Delphi.

                                                                    0

                                                                    Аннотации — это фичи уровня компилятора, и вряд ли будут поддерживаться на уровне JVM, в отличие от ключевых слов.

                                                                      0
                                                                      Ну кастомные так-то поддерживаются, ничто не мешает технически поддержать и другие.
                                                                        0

                                                                        Кастомные что? Кастомные аннотации — это просто, там фреймворки есть. А вот о кастомных ключевых словах JVM ничего знать не может.

                                                                          0
                                                                          Кастомные аннотации. При чем тут фреймворки? Аннотации, хоть и не всякие, фиксируются в class-файле. Например.
                                                                        0

                                                                        Вы о чём?
                                                                        JVM не знает ни о каких ключевых словах. Ни о кастомных, ни о каких-либо других. Ключевые слова заканчиваются на уровне javac.

                                                                          0

                                                                          Если бы они действительно заканчивались на уровне javac, не было бы надёжных понятий sealed type и final field в Java. С другой стороны, JVM прекрасно обходится без @Override, она ей вообще не нужна для определения shadowing.

                                                                            0

                                                                            На уровне class-файла final field это взведённый бит в соответствующем поле, а sealed type — аттрибут PermittedSubclasses.


                                                                            Естественно, что эти флаги и аттрибуты генерируются на основе ключевых слов в исходном тексте, но утверждать, что на уровне JVM есть «поддержка ключевых слов» — всё равно что называть системный блок компьютера «процессором».

                                                                              0

                                                                              А как это ещё назвать, если не поддержкой ключевых слов на уровне JVM? Да, ключевые слова не встречаются на том уровне напрямую, но именно для поддержки функционирования этих ключевых слов делаются фичи JVM вроде PermittedSubclasses.


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


                                                                              Кроме того, вернувшись к исходному сообщению, хочу возразить — sealed и non-sealed это не просто маркеры. Это @Override и @FunctionalInterface — маркеры, а эти ключевые слова именно приводят в действие дополнительные механизмы в JVM, так же, как это делают final, private и volatile.

                                                                                0
                                                                                Напомню, что предполагаемая альтернатива новым ключевым словам — это использовать аннотации. И они как раз работать так же не будут, несмотря на то, что тоже могут включаться а класс-файл. Но нет механизма, согласно которому процессор аннотаций включал бы какие-нибудь биты.

                                                                                Сильное утверждение. Надеюсь увидеть аргументы, его подтверждающие. Пока что я не вижу, почему это может быть нереализуемо. Компилятор имеет доступ к аннотациям, иначе бы они не попадали в class-файл. Компилятор имеет доступ к флагам. Почему компилятор, имеющий доступ к флагам, не может выставить флаги на основе аннотаций, к которым он тоже имеет доступ?
                                                                                  0
                                                                                  Почему компилятор, имеющий доступ к флагам, не может выставить флаги на основе аннотаций, к которым он тоже имеет доступ?

                                                                                  Если угодно, это противоречит текущей философии языка. Аннотации в Java являются помечающей сущностью, и с точки зрения компилятора никак на выполняемый байткод не вляиют. То что куча фреймворков злоупотребляет этой особенностью и генерирует посредством аннотации байткод, ещё не означает, что так должен делать и javac тоже.


                                                                                  Maccimo


                                                                                  По вашей логике в JVM с самой первой версии была поддержка ключевого слова open из тогда ещё не существовавшего языка программирования.

                                                                                  Нет, не так. Kotlin использует механизмы JVM, которые были туда добавлены для поддержки ключевого слова final, но не так, как это делает основной хост-язык. Если бы такой механизм в Java не был добавлен вообще, то Kotlin нечего бы было там использовать. То, что первым тут было именно яйцо, видно и по тому, что в документации
                                                                                  к Kotlin классы описываются именно в семантике ключевых слов Java, а не в семантике внутренних механизмов JVM. Они легко могли написать "by default, Kotlin classes are non-extensible", но вместо этого написали детали имплементации, через которые эта фича была сделана.


                                                                                  Нет никаких технических причин, по которым javac не мог бы обрабатывать определённые аннотации особым образом

                                                                                  Верно, эти причины философские. Точно так же, как философским был выбор ключевого слова non-sealed вместо любого другого. Дизайн языка программирования — это вообще в принципе больше философия, чем технологии.

                                                                                    0
                                                                                    Аннотации в Java являются помечающей сущностью, и с точки зрения компилятора никак на выполняемый байткод не вляиют. То что куча фреймворков злоупотребляет этой особенностью и генерирует посредством аннотации байткод, ещё не означает, что так должен делать и javac тоже.

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


                                                                                    Верно, эти причины философские.

                                                                                    Вновь отсылаю вас к исходникам, которые бесцеремонно попрают ваши философские воззрения.

                                                                                      0

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

                                                                                        0

                                                                                        Так вы сюда в демагогии поупражняться пришли?
                                                                                        Так бы сразу и сказали. Демагогия у вас пока на троечку.

                                                                                          0

                                                                                          Моя основная мысль была про значение аннотаций и про то, что, согласно философии языка, они не являются заменой ключевых слов, и таковыми не станут. Case in point — несколько новых ключевых слов, которые добавлены или будут добавлены(sealed, record), и ни одной compile-bounding аннотации.
                                                                                          На что вы заявили, будто я демагог, потому что вы нашли аж одну (на самом деле их две) аннотации, которые влияют на атрибуты нодов AST в понимании javac. Причём, этот же атрибут вполне мог быть вычислимым на основании эвристик, как это уже происходит для @Override и @FunctionalInterface.
                                                                                          В сухом остатке, вы меня не убедите (одна аннотация против нескольких десятков ключевых слов, причём @Sealed точно могли сделать аннотацией, но сделали ключевым словом, что показательно), и у меня нет больше никаких инструментов, чтобы убедить вас, потому что мои соображения вы уже назвали демагогией, что указывает на желаемый уровень дискуссии.
                                                                                          Посему я и предлагаю разойтись пока с миром — если вы правы, все новые фичи будут добавляться через аннотации, достаточно только подождать, пока очистится пайплайн от текущих проектов, чтобы "новый свежий ветер перемен" наконец задул в полную силу. Если прав я, то будут в основном добавляться ключевые слова. Время рассудит.


                                                                                          PS. Хотя что вам мешает написать в рассылку и предложить перестать добавлять новые ключевые слова в язык? Я-то всего лишь тут в демагогии упражняюсь, а там есть прямой доступ в уши Брайана Гетца, и он довольно активно отвечает на вопросы и даёт разъяснения.

                                                                                  0
                                                                                  А как это ещё назвать, если не поддержкой ключевых слов на уровне JVM?

                                                                                  Так и назвать, поддержкой final/volatile/etc. полей в классе.


                                                                                  Ключевые слова это свойство языка программирования. JVM не работает на уровне языков программирования, она работает на уровне класс-файлов. Вы смешиваете уровни абстракции.


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


                                                                                  By default, Kotlin classes are final: they can’t be inherited. To make a class inheritable, mark it with the open keyword.

                                                                                  https://kotlinlang.org/docs/reference/classes.html#inheritance

                                                                                  По вашей логике в JVM с самой первой версии была поддержка ключевого слова open из тогда ещё не существовавшего языка программирования.


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

                                                                                  Процессор аннотаций может перекорёжить весь класс как ему вздумается начиная с Java 6 как минимум (см. The Hacker’s Guide to Javac). В качестве живого примера см. Lombok.


                                                                                  Про использование аннотаций:


                                                                                  Нет никаких технических причин, по которым javac не мог бы обрабатывать определённые аннотации особым образом, устанавливать любые флаги и создавать любые аттрибуты какие разработчики сочтут нужными.


                                                                                  Пример: https://hg.openjdk.java.net/jdk8/jdk8/langtools/file/e9f118c2bd3c/src/share/classes/com/sun/tools/javac/comp/MemberEnter.java#l788


                                                                                  И комментарий оттуда:


                                                                                  // Internally to java.lang.invoke, a @PolymorphicSignature annotation
                                                                                  // acts like a classfile attribute
                                                                                    0
                                                                                    Every signature-polymorphic method must be declared with the following properties:

                                                                                    It must be native.
                                                                                    It must take a single varargs parameter of the form Object....
                                                                                    It must produce a return value of type Object.
                                                                                    It must be contained within the java.dyn package.
                                                                                    — source https://wiki.openjdk.java.net/display/mlvm/InterfaceDynamic

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


                                                                                    Аннотация здесь помогает скорее не компилятору, а человеку, так как во-первых, ему точно понятно, что данный метод или тип выполняет в тексте определённую роль, а во-вторых, после проставления аннотаций эвристики можно проверить и сгенерировать подсказки если ожидание человека отличается от ожидания javac.


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


                                                                                    @FunctionalInterface
                                                                                    public interface TriFunction <X, Y, Z, R> {
                                                                                    
                                                                                      R apply(X arg0, Y arg1, Z arg2);
                                                                                    
                                                                                      BiFunction<Y, X, R> curry(X arg0);
                                                                                    }

                                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                        Самое читаемое