Статьи

JDK14 экземпляр проблемы советника

Тагир Валеев недавно написал в твиттере о функции предварительного просмотра скорого релиза Java JDK14:

# Сопоставление с шаблоном Java14 выводит дублирование имен на новый уровень безумия. Здесь я добавляю или удаляю модификатор `final` для поля` FLAG`, доступ к которому возможен только в недоступной ветке if. Это фактически меняет семантику программы! #ProgrammingIsFun . pic.twitter.com/UToRY3mpW9

Проблема заключается в том, что в запланированном и в выпуске EA уже доступна новая функция Java, которая вводит переменные шаблона, а текущая версия предлагаемого нового стандарта оставляет место для некоторой действительно пугающей проблемы кодирования.

После твита, детали были обсуждены достаточно подробно, чтобы понять реальную проблему. Однако в этой статье я кратко расскажу о том, что все это значит, так что вам не нужно копаться в твиттах и ​​стандартах.

Что такое переменная шаблона

Прежде чем углубляться в детали вопроса, изложенные выше в твите, давайте немного обсудим, что такое переменная шаблона. (Может быть, немного неаккуратный, более объяснительный, чем точный и полный, но вот он.)

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

1
2
3
4
5
6
7
// HOW THIS IS TODAY, JAVA < 14
 
Object z = "alma";
if (!(z instanceof String)){
    throw new IllegalArgumentException();
}
System.out.println(((String)z).length());

В реальной жизни переменная z может происходить откуда-то еще, и в этом случае не так очевидно, что это строка. Когда мы хотим распечатать длину строки, используя println мы уже знаем, что объект, на который ссылается z является строкой. Компилятор, с другой стороны, этого не делает. Мы должны привести переменную к String а затем мы можем использовать метод length() . Другие языки делают это лучше. В идеале я мог бы написать:

1
2
3
4
5
6
7
// HOW IT WOULD BE THE SIMPLEST
 
Object z = "alma";
if (!(z instanceof String)){
    throw new IllegalArgumentException();
}
System.out.println(z.length());

Это не способ Java, а также способ, которым JDK14 упрощает этот шаблон программирования. Вместо этого предложенная функция вводит новый синтаксис для оператора instanceof который вводит новую переменную: переменную шаблона .

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

1
2
3
4
5
6
7
// HOW IT IS IN JDK14-EA / OpenJDK (build 14-ea+28-1366)
 
Object z = "alma";
if (!(z instanceof String s)){
    throw new IllegalArgumentException();
}
System.out.println(s.length());

Он вводит новую переменную s которая находится в области видимости только тогда, когда указанным объектом является String . Более простая версия кода без части исключения будет

1
2
3
4
5
6
7
Object z = "alma";
if (z instanceof String s){
    // we have here 's' and it is a String
    System.out.println(s.length());
}
 
// we do not have 's' here

Когда условие истинно, объект является строкой, поэтому мы имеем ‘s’. Если условие ложно, то мы перепрыгиваем через then_statement, и там у нас нет ‘s’, поскольку у нас нет строки. ‘s’ доступно в коде, который запускается только тогда, когда объект является строкой. Таким образом, область действия переменной шаблона определяется и ограничивается не только синтаксической областью действия переменной, но также и возможным потоком управления. Только поток управления, который может быть проанализирован с уверенностью, принимается во внимание.

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

Пока что все кажется простым, и мы все рады получить новую функцию в Java 14.

Стандарт JSL14

Точный расчет объема определен в стандарте JLS14 (спецификация языка Java 14). На момент написания этой статьи спецификация была доступна только в качестве предварительного просмотра.

http://cr.openjdk.java.net/~gbierman/jep305/jep305-20191021/specs/patterns-instanceof-jls.html#jls-6.3.2.2

Поскольку поток выполнения Java-программы может контролироваться многими различными языковыми конструкциями, область действия переменной шаблона определяется для каждой из этих структур. Существуют отдельные разделы для различных логических операторов, которые оценивают короткое замыкание, оператор «если», оператор «пока» и так далее. Я не хочу подробно обсуждать различные случаи. Здесь я остановлюсь только на случае выражения «if» без части «else». Приведенный выше стандарт гласит:

Следующие правила применяются к выражению `if (e) S` (14.9.1):

* Переменная шаблона, введенная e, когда true определенно совпадает с `S`.

Это ошибка времени компиляции, если любая переменная шаблона, введенная `e`, когда true уже находится в области видимости в` S`.

* `V` вводится` if (e) S` тогда и только тогда, когда `V` вводится` e`, когда `false` и` S` не могут завершиться нормально.

Это ошибка времени компиляции, если любая переменная шаблона, введенная оператором `if`, уже находится в области видимости.

Интересная часть — «не может завершиться нормально». Хорошим примером этого является наш пример выше: мы создаем так называемый защитный оператор if . Если переменная z не является String то мы генерируем исключение, возвращаем или делаем что-то еще, что всегда будет препятствовать выполнению кода после выполнения оператора if если переменная не является String .

В случае оператора throw или return обычно очень просто и легко увидеть, что код «не может завершиться нормально». В случае бесконечного цикла это не всегда так очевидно.

Эта проблема

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

01
02
03
04
05
06
07
08
09
10
11
12
private static boolean FLAG = true;
static String variable = "Hello from field";
 
public static void main() {
    Object z = "Hello from pattern matching";
    if (!(z instanceof String variable)){
        while (FLAG) {
            System.out.println("We are in an endless loop");
        }
    }
    System.out.println(variable);
}

В этом случае у нас есть цикл, который бесконечен или нет. Это зависит от другой части кода, которая может изменить значение поля класса FLAG с true на false . Эта часть кода «может завершиться нормально».

Если мы немного изменим приведенный выше код, сделав поле FLAG final , как

01
02
03
04
05
06
07
08
09
10
11
12
private static final boolean FLAG = true;
static String variable = "Hello from field";
 
public static void main() {
    Object z = "Hello from pattern matching";
    if (!(z instanceof String variable)){
        while (FLAG) {
            System.out.println("We are in an endless loop");
        }
    }
    System.out.println(variable);
}

тогда компилятор увидит, что цикл бесконечен и не может нормально завершиться. В первом случае программа распечатает поле Hello from field и напечатает Hello from pattern matching . variable pattern во втором случае скрывает variable поля, поскольку область действия переменной pattern распространяется на команды, следующие за оператором if потому что часть then не может завершиться нормально.

Это действительно проблема с этой функцией предварительного просмотра, как она есть. Читаемость кода в этом случае очень сомнительна. Область действия переменной шаблона и то, скрывает ли она поле или нет, зависит от final модификатора поля, которого там нет. Когда мы смотрим на некоторый код, фактическое выполнение и результат кода должны быть простыми и не должны зависеть от некоторого кода, который находится далеко и может пропустить наше внимание при чтении кода локально.

Это не единственная ситуация в Java, которая имеет эту аномалию. Вы можете иметь класс с именем String например, в вашей кодовой базе. Код классов, находящихся в одном пакете, будет использовать этот класс, когда они ссылаются на тип String . Если мы удаляем класс String из пользовательского кода, то значение типа String становится java.lang.String . Фактическое значение кода зависит от некоторого другого кода, который «далеко».

Этот второй пример, однако, является взломом, и маловероятно, что программист Java, который не сошел с ума, называет класс String (серьезно https://github.com/verhas/jScriptBasic/blob/master/src/main/ java / com / scriptbasic /ification / String.java ?) или другое имя, которое также существует в JDK в пакете java.lang . Может быть, это просто удача, может быть, это было хорошо продумано при принятии решения, чтобы избежать обязательного импорта классов из пакета java.lang . Это история.

Затенение имен переменных и описанная выше ситуация, с другой стороны, не кажутся такими уж странными, и это наверняка не произойдет случайно в некотором коде Java.

К счастью, это только функция предварительного просмотра. Он будет в JDK14 как есть, но в качестве функции предварительного просмотра он доступен только тогда, когда компилятор javac и выполнение java используют флаг --enable-preview и функция предварительного просмотра может измениться в будущем несовместимым способом.

Решение

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

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

Другая возможность — ограничить область видимости переменных шаблона оператором then или оператором else .

Таким образом, мы не полагаемся на функцию «не могу нормально завершить» кода. Команда else гарантирует, что ветвь else выполняется только тогда, когда условие оператора if false . Это сделает решение менее элегантным.

Опять же, другая возможность — запретить переменным шаблона затенять любую переменную поля. Это решило бы проблему, изложенную выше, но представило бы другую. С этим ограничением может случиться так, что существующий класс с методами и шаблонной переменной V перестает компилироваться, когда мы вводим новую переменную поля с именем V По крайней мере, эта проблема связана с компиляцией, а не с кодом, который глючит во время выполнения.

У меня скорее 100 ошибок времени компиляции, чем одна ошибка времени выполнения.

Еще одна возможность состоит в том, чтобы отказаться от переменной pattern и просто использовать исходную переменную с расширенной информацией о типе, где текущее решение предварительного просмотра использует переменную pattern. Поклонникам Kotlin понравится это решение. Это также элегантно устранит проблему с теневым копированием, потому что локальная переменная уже затеняет (или не делает) переменную поля. Недостаток этого решения состоит в том, что изменяемая область видимости переменной будет иметь разные типы в разных местах кода. Давайте посмотрим на следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package javax0.jdk14.instanceof0;
 
public class Sample2 {
    public static class A {
        public static void m(){
            System.out.println("A");
        }
    }
    public static class B extends A {
        public static void m(){
            System.out.println("B");
        }
    }
    public static void main(String[] args) {
        A a = new B();
        if( a instanceof B b){
            b.m();
        }
        a.m();
    }
}

Этот код выведет B затем A потому что вызов bm() такой же, как Bm() на основе объявленного типа переменной b и тот же способ, что am() такой же, как Am() на основе объявленного типа переменной a . Пропуск переменной шаблона и использование исходной переменной может привести к путанице:

1
2
3
4
5
6
7
8
// NOT ACTUAL CODE
    public static void main(String[] args) {
        A a = new B();
        if( a instanceof B){
            a.m();
        }
        a.m();
    }

Будет ли am() вызывать разные методы в разных строках?

Как вы можете видеть, нет известного хорошего или лучшего решения этой проблемы … кроме одного. Позвоните своему представителю в парламенте JDK и скажите им, что это плохо. (Psst: они уже знают это из оригинального твита.)

навынос

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

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

Используйте последнюю выпущенную версию Java для своих экспериментов и проектов с открытым исходным кодом и будьте готовы поддержать более старые версии Java, если это потребуется пользователям.

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

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

Опубликовано на Java Code Geeks с разрешения Питера Верхаса, партнера нашей программы JCG . Смотрите оригинальную статью здесь: JDK14, экземпляр проблемы EA

Мнения, высказанные участниками Java Code Geeks, являются их собственными.