Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Не за горами новая, 14-я версия Java, а значит самое время посмотреть, какие новые синтаксические возможности будет содержать эта версия Java. Одной из таких синтаксических возможностей является паттерн-матчинг по типу, который будет осуществляться посредством улучшенного (расширенного) оператора
Сегодня я хотел бы поиграться с этим новым оператором и рассмотреть особенности его работы более детально. Так как паттерн-матчинг по типу ещё не вошёл в главный репозиторий JDK, мне пришлось скачать репозиторий проекта Amber, в котором ведётся разработка новых синтаксических конструкций Java, и собрать JDK из этого репозитория.
Итак, первое, что мы сделаем — проверим версию Java, чтобы убедиться, что мы действительно используем JDK 14:
Всё верно.
Теперь напишем небольшой кусок кода со «старым» оператором
Работает. Это стандартная проверка на тип с последующим приведением. Подобные конструкции мы пишем изо дня в день, какую бы версию Java мы бы не использовали, хоть 1.0, хоть 13.
Но теперь у нас в руках Java 14, и давайте перепишем код с использованием улучшенного оператора
Прекрасно. Код стал чище, короче, безопаснее и читабельнее. Было три повторения слова String, стало одно. Заметьте, что мы не забыли указать аргументы
Давайте попробуем написать что-нибудь более навороченное и добавим второе условие, которое использует только что объявленную переменную:
Компилируется и работает. А что если поменять условия местами?
Ошибка компиляции. Чего и следовало ожидать: переменная
Кстати, что с мутабельностью? Переменная final или нет? Пробуем:
Ага, переменная final. Это что-то новенькое. Первый раз в истории Java что-то по умолчанию является final. До этого всё в Java по умолчанию являлось non-final: поля, классы, методы, параметры методов и даже параметры лямбд. А переменная паттерна может быть только final. Это значит, что слово «переменная» здесь вообще не совсем корректно. Да и компилятор использует специальный термин «pattern binding». Поэтому предлагаю отныне говорить не «переменная», а «биндинг паттерна» (к сожалению, слово «binding» не очень хорошо переводится на русский).
С мутабельностью и терминологией разобрались. Поехали экспериментировать дальше. Вдруг у нас получится «сломать» компилятор?
Что если назвать переменную и биндинг паттерна одним и тем же именем?
Логично. Перекрытие переменной из внешней области видимости не работает. Это эквивалентно тому, как если бы мы просто завели переменную
А если так:
Компилятор надёжен как бетон.
Что ещё можно попробовать? Давайте поиграемся с областями видимости. Если в ветке
Сработало. Компилятор не только надёжен, но ещё и умён.
А если так?
Опять сработало. Компилятор корректно понимает, что условие сводится к простому
Неужели не удастся «сломать» компилятор?
Может, так?
Ага! Вот это уже похоже на баг. Ведь все три условия абсолютно эквивалентны:
С другой стороны, правила flow scoping довольно нетривиальны, и возможно такой случай действительно не должен работать. Но если смотреть чисто с человеческой точки зрения, то я считаю, что это баг.
Но да ладно, давайте попробуем ещё что-нибудь. Будет ли работать такое:
Скомпилировалось. Это хорошо, поскольку этот код эквивалентен следующему:
А так как оба варианта эквивалентны, то и программист ожидает, что они будут работать одинаково.
Что насчёт перекрытия полей?
Компилятор не заругался. Это вполне логично, потому что локальные переменные всегда могли перекрывать поля. Для биндингов паттернов, видимо, тоже решили не делать исключения. С другой стороны, такой код довольно хрупок. Одно неосторожное движение, и вы можете не заметить, как ваша ветка
В обеих ветвях теперь используется поле
Что ещё интересного? Как и «старый»
Кстати, используя это свойство, можно укоротить подобные цепочки:
Если использовать
Напишите в комментариях, что вы думаете по поводу такого стиля. Стали ли бы вы использовать такую идиому?
Что насчёт дженериков?
Очень интересно. Если «старый»
ИМХО, это довольно серьёзная проблема. С другой стороны, я не знаю, как можно было бы её исправить. Похоже, опять придётся полагаться на инспекции в IDE.
В целом, новый паттерн-матчинг по типу работает очень круто. Улучшенный оператор
С другой стороны, вызывают небольшие вопросы несколько спорных моментов:
Однако это скорее мелкие придирки, нежели серьёзные претензии. В целом, огромные преимущества нового оператора
P.S. У меня есть канал в Telegram, где я пишу о новостях Java. Призываю вас на него подписаться.
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());
}
Почему это не работает?
Ответ: отсутствие reified generics в Java.
ИМХО, это довольно серьёзная проблема. С другой стороны, я не знаю, как можно было бы её исправить. Похоже, опять придётся полагаться на инспекции в 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;
}
}
Код выше можно написать ещё короче. Как?
С помощью записей, которые также войдут в Java 14. О них мы поговорим в следующий раз.
С другой стороны, вызывают небольшие вопросы несколько спорных моментов:
- Не полностью прозрачные правила области видимости (пример с
instanceof || false
). - Перекрытие полей.
instanceof
и дженерики.
Однако это скорее мелкие придирки, нежели серьёзные претензии. В целом, огромные преимущества нового оператора
instanceof
определённо стоят его добавления язык. А если он ещё выйдет из состояния preview и станет стабильной синтаксической конструкцией, то это будет большой мотивацией наконец-то уйти с Java 8 на новую версию Java.P.S. У меня есть канал в Telegram, где я пишу о новостях Java. Призываю вас на него подписаться.