instanceof
.
Сегодня я хотел бы поиграться с этим новым оператором и рассмотреть особенности его работы более детально. Так как паттерн-матчинг по типу ещё не вошёл в главный репозиторий JDK, мне пришлось скачать репозиторий проекта Amber, в котором ведётся разработка новых синтаксических конструкций Java, и собрать JDK из этого репозитория.
Итак, первое, что мы сделаем — проверим версию Java, чтобы убедиться, что мы действительно используем JDK 14:
> java -version openjdk version "14-internal" 2020-03-17 OpenJDK Runtime Environment (build 14-internal+0-adhoc.osboxes.amber-amber) OpenJDK 64-Bit Server VM (build 14-internal+0-adhoc.osboxes.amber-amber, mixed mode, sharing)
Всё верно.
Теперь напишем небольшой кусок кода со «старым» оператором instanceof
и запустим его:
public class A { public static void main(String[] args) { new A().f("Hello, world!"); } public void f(Object obj) { if (obj instanceof String) { String str = (String) obj; System.out.println(str.toLowerCase()); } } }
> java A.java hello, world!
Работает. Это стандартная проверка на тип с последующим приведением. Подобные конструкции мы пишем изо дня в день, какую бы версию Java мы бы не использовали, хоть 1.0, хоть 13.
Но теперь у нас в руках Java 14, и давайте перепишем код с использованием улучшенного оператора instanceof
(повторяющиеся строки кода в дальнейшем буду опускать):
if (obj instanceof String str) { System.out.println(str.toLowerCase()); }
> java --enable-preview --source 14 A.java hello, world!
Прекрасно. Код стал чище, короче, безопаснее и читабельнее. Было три повторения слова String, стало одно. Заметьте, что мы не забыли указать аргументы --enable-preview --source 14
, т.к. новый оператор является preview feature. Кроме того, внимательный читатель, наверное, заметил, что мы запустили исходный файл A.java напрямую, без компиляции. Такая возможность появилась в Java 11.
Давайте попробуем написать что-нибудь более навороченное и добавим второе условие, которое использует только что объявленную переменную:
if (obj instanceof String str && str.length() > 5) { System.out.println(str.toLowerCase()); }
Компилируется и работает. А что если поменять условия местами?
if (str.length() > 5 && obj instanceof String str) { System.out.println(str.toLowerCase()); }
A.java:7: error: cannot find symbol if (str.length() > 5 && obj instanceof String str) { ^
Ошибка компиляции. Чего и следовало ожидать: переменная str
ещё не объявлена, а значит не может быть использована.
Кстати, что с мутабельностью? Переменная final или нет? Пробуем:
if (obj instanceof String str) { str = "World, hello!"; System.out.println(str.toLowerCase()); }
A.java:8: error: pattern binding str may not be assigned str = "World, hello!"; ^
Ага, переменная final. Это что-то новенькое. Первый раз в истории Java что-то по умолчанию является final. До этого всё в Java по умолчанию являлось non-final: поля, классы, методы, параметры методов и даже параметры лямбд. А переменная паттерна может быть только final. Это значит, что слово «переменная» здесь вообще не совсем корректно. Да и компилятор использует специальный термин «pattern binding». Поэтому предлагаю отныне говорить не «переменная», а «биндинг паттерна» (к сожалению, слово «binding» не очень хорошо переводится на русский).
С мутабельностью и терминологией разобрались. Поехали экспериментировать дальше. Вдруг у нас получится «сломать» компилятор?
Что если назвать переменную и биндинг паттерна одним и тем же именем?
if (obj instanceof String obj) { System.out.println(obj.toLowerCase()); }
A.java:7: error: variable obj is already defined in method f(Object) if (obj instanceof String obj) { ^
Логично. Перекрытие переменной из внешней области видимости не работает. Это эквивалентно тому, как если бы мы просто завели переменную obj
второй раз в той же области видимости.
А если так:
if (obj instanceof String str && obj instanceof String str) { System.out.println(str.toLowerCase()); }
A.java:7: error: illegal attempt to redefine an existing match binding if (obj instanceof String str && obj instanceof String str) { ^
Компилятор надёжен как бетон.
Что ещё можно попробовать? Давайте поиграемся с областями видимости. Если в ветке if
определён биндинг, то будет ли он определён в ветке else
, если инвертировать условие?
if (!(obj instanceof String str)) { System.out.println("not a string"); } else { System.out.println(str.toLowerCase()); }
Сработало. Компилятор не только надёжен, но ещё и умён.
А если так?
if (obj instanceof String str && true) { System.out.println(str.toLowerCase()); }
Опять сработало. Компилятор корректно понимает, что условие сводится к простому obj instanceof String str
.
Неужели не удастся «сломать» компилятор?
Может, так?
if (obj instanceof String str || false) { System.out.println(str.toLowerCase()); }
A.java:8: error: cannot find symbol System.out.println(str.toLowerCase()); ^
Ага! Вот это уже похоже на баг. Ведь все три условия абсолютно эквивалентны:
obj instanceof String str
obj instanceof String str && true
obj instanceof String str || false
С другой стороны, правила flow scoping довольно нетривиальны, и возможно такой случай действительно не должен работать. Но если смотреть чисто с человеческой точки зрения, то я считаю, что это баг.
Но да ладно, давайте попробуем ещё что-нибудь. Будет ли работать такое:
if (!(obj instanceof String str)) { throw new RuntimeException(); } System.out.println(str.toLowerCase());
Скомпилировалось. Это хорошо, поскольку этот код эквивалентен следующему:
if (!(obj instanceof String str)) { throw new RuntimeException(); } else { System.out.println(str.toLowerCase()); }
А так как оба варианта эквивалентны, то и программист ожидает, что они будут работать одинаково.
Что насчёт перекрытия полей?
public class A { private String str; public void f(Object obj) { if (obj instanceof String str) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } } }
Компилятор не заругался. Это вполне логично, потому что локальные переменные всегда могли перекрывать поля. Для биндингов паттернов, видимо, тоже решили не делать исключения. С другой стороны, такой код довольно хрупок. Одно неосторожное движение, и вы можете не заметить, как ваша ветка if
сломалась:
private boolean isOK() { return false; } public void f(Object obj) { if (obj instanceof String str || isOK()) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } }
В обеих ветвях теперь используется поле str
, чего может не ожидать невнимательный программист. Чтобы как можно раньше обнаруживать подобные ошибки, используйте инспекции в IDE и разную подсветку синтаксиса для полей и переменных. А ещё я рекомендую всегда использовать квалификатор this
для полей. Это добавит ещё больше надёжности.
Что ещё интересного? Как и «старый» instanceof
, новый никогда не матчит null
. Это значит, что можно всегда полагаться на то, что биндинги паттернов никогда не могут быть null
:
if (obj instanceof String str) { System.out.println(str.toLowerCase()); // Никогда не выбросит NullPointerException }
Кстати, используя это свойство, можно укоротить подобные цепочки:
if (a != null) { B b = a.getB(); if (b != null) { C c = b.getC(); if (c != null) { System.out.println(c.getSize()); } } }
Если использовать instanceof
, то код выше можно переписать так:
if (a != null && a.getB() instanceof B b && b.getC() instanceof C c) { System.out.println(c.getSize()); }
Напишите в комментариях, что вы думаете по поводу такого стиля. Стали ли бы вы использовать такую идиому?
Что насчёт дженериков?
import java.util.List; public class A { public static void main(String[] args) { new A().f(List.of(1, 2, 3)); } public void f(Object obj) { if (obj instanceof List<Integer> list) { System.out.println(list.size()); } } }
> java --enable-preview --source 14 A.java Note: A.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. 3
Очень интересно. Если «старый» instanceof
поддерживает только instanceof List
или instanceof List<?>
, то новый работает с любым конкретным типом. Ждём первого человека, который попадётся вот в такую ловушку:
if (obj instanceof List<Integer> list) { System.out.println("Int list of size " + list.size()); } else if (obj instanceof List<String> list) { System.out.println("String list of size " + list.size()); }
ИМХО, это довольно серьёзная проблема. С другой стороны, я не знаю, как можно было бы её исправить. Похоже, опять придётся полагаться на инспекции в IDE.
Выводы
В целом, новый паттерн-матчинг по типу работает очень круто. Улучшенный оператор instanceof
позволяет делать не только тест на тип, но ещё и объявлять готовый биндинг этого типа, избавляя от необходимости ручного приведения. Это означает, что в коде будет меньше шума, и читателю будет гораздо проще разглядеть полезную логику. Например, большинство реализаций equals()
можно будет писать в одну строчку:
public class Point { private final int x, y; … @Override public int hashCode() { return Objects.hash(x, y); } @Override public boolean equals(Object obj) { return obj instanceof Point p && p.x == this.x && p.y == this.y; } }
С другой стороны, вызывают небольшие вопросы несколько спорных моментов:
- Не полностью прозрачные правила области видимости (пример с
instanceof || false
). - Перекрытие полей.
instanceof
и дженерики.
Однако это скорее мелкие придирки, нежели серьёзные претензии. В целом, огромные преимущества нового оператора instanceof
определённо стоят его добавления язык. А если он ещё выйдет из состояния preview и станет стабильной синтаксической конструкцией, то это будет большой мотивацией наконец-то уйти с Java 8 на новую версию Java.
P.S. У меня есть канал в Telegram, где я пишу о новостях Java. Призываю вас на него подписаться.
ссылка на оригинал статьи https://habr.com/ru/post/477654/
Добавить комментарий