Pull to refresh

Comments 69

Интересный подход к изучению нового функционала!
Продолжай в том же духе
биндинг паттерна — говорить точно не нужно. Pattern binding — вполне нормально. И какие проблемы с переводом binding на русский? Связанный, привязанный, прикреплённый,… — уйма вариантов.

Тем более что он "байндинг", а не "биндинг"

Не понятно это привязка или все же присваивание? Не рассмотрен случай, когда obj изменился после instanceof.

Хм, непонятно, почему решили сделать с объявлением дополнительной переменной (myVar instanceof String str)
В котлине, насколько я знаю, можно просто с изначальной переменной дальше работать, будто она этого типа


В JEP про это написано: мы не хотим ad hoc а хотим pattern matching. Но имхо это странно — усложнять имеющийся синтакс ради потенциальной новой фичи, которая может быть ещё совершенно иначе будет реализована
Да ещё и на ровном месте добавлять смесь проблем с перекрытием переменных и умных условий

Насколько я понимаю подход описанный в статье один в один скопирован из C#.
PS: мне тоже вариант из Котлина кажется удобнее.

В C# это объяснили тем, что вместо myVar можно подставить любое выражение, и оно будет вычислено один раз.

Потому что введение в старый язык смарткастов может изменить работу существующего кода.


Например, вот в этом случае:


        if (foo instanceof Bar) {
            foo.new Baz().run();
        }

Если в классе Bar переопределен внутренний класс Baz — поведение кода после введения смарткастов изменится.


Кроме внутреннего класса, можно неудачно переопределить поле.

Логично, спасибо

Лично мне не нравятся смарткасты, потому что они могут работать только с final полями и final/effectively final переменными:
private Object field;
...
if (this.field instanceof String) {
     // Можем использовать this.field как String здесь или нет?
}


В Java-подходе такой проблемы нет:
if (this.field instanceof String str) {
     // Используем str
}

Интересный пример, но "такой проблемы нет" только в случае атомарности оператора?
Я, к сожалению, не нашел ничего про concurrency в pattern matching

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


Вот вам пример к чему это приводит в Typescript:


switch (derivation.dependenciesState) {
    // ...
    case IDerivationState.POSSIBLY_STALE: {
        // ...
        obj.get();

        // ...
        // https://github.com/mobxjs/mobx/blob/master/src/core/derivation.ts#L117
        if ((derivation.dependenciesState as any) === IDerivationState.STALE) {
            // ...
        }
    }
}

Зачем тут каст к any? А чтобы заткнуть компилятор, который уверен, что derivation.dependenciesState никак не может быть равен IDerivationState.STALE внутри блока case IDerivationState.POSSIBLY_STALE. Даже несмотря на то, что obj.get() этот самый dependenciesState обязательно изменяет.

Компилятор надёжен как бетон.

Компилятор просто бетон /sarcasm
Джаву пора закапывать, кажется. Каждый релиз вызывает фейспалм.
Это называется smart cast и должно работать без всякой дополнительной фигни.

Не должно. Обратная совместимость же! Вот если обратную совместимость нарушат — тогда джаву и правда будет пора закапывать...

А что «обратная совместимость»? Чем это лучше, чем введение нового оператора?
У одной нашей команды есть куча проектов, которые работают на Java8, максимум. Совместимость уже частично нарушена.
В мире майнкрафта не получается запускать сервера с плагинами на версиях выше 8: почему-то код сервера, на который можно ставить плагины, требует какую-то дикую интроспекцию, которую Jigsaw запрещает. По крайней мере, мне это так объяснили. :)

Спасибо за хорошую статью, однозначно жирный плюс!


Смущает пара моментов:


1)


До этого всё в Java по умолчанию являлось non-final: поля, классы, методы, параметры методов и даже параметры лямбд.

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


public void foo(String name) {
  String str = "";

  cache.computeIfAbsent(name, beanName -> {
    str = beanName;                                    // <--------- ошибка компиляции
    return beanName;
  });
}

2)
Вместо "биндинг" можно писать "связывание", ИМХО, это как раз тот момент, когда вместо заимствования лучше сделать дословный перевод.


3)


Ага! Вот это уже похоже на баг.

Писали ли вы в указанную в начале статьи рассылку? Что говорят разработчики?

1) У лямбд, как мне кажется, всё же чуть иной случай: чтобы переменная могла быть захвачена, она должна быть либо явно final, либо effectively final. У новых «связываний» финальность появляется от рождения, что чуть отличается от захваченных переменных в лямбдах.

Но это исключительно к слову пришлось. :D

Не туда копаешь. Неявный final внутри метода появился в multi-catch в седьмой джаве. Правда про это мало кто знает кроме разработчиков IDE.


Если же рассматривать объявление класса, то поля, объявленные в интерфейсе, неявно final (и static) с первой джавы. Ну и константы enum туда же.

Да, что-то я в торопях даже не заметил, что про final откровенный бред написал. Я написал, что методы и классы по умолчанию являются final, что очевидно не так.

Ну, поля интерфейсов и перечисления я не упомянул как сами собой разумеющиеся вещи.


Неявный final внутри метода появился в multi-catch в седьмой джаве. Правда про это мало кто знает кроме разработчиков IDE.

Истину глаголешь, до твоего комментария я об этом не знал :)

Ещё вспомнил: переменные в try-with-resources тоже неявно final.
А что произошло с областями видимости вот в этом примере: ↓?
if (!(obj instanceof String str)) {
    throw new RuntimeException();
}
System.out.println(str.toLowerCase());

Скомпилировалось. <...>

"Биндинг Связывание паттерна" str вышло за пределы фигурных скобок условного оператора?
То есть он работает иначе, чем try-with-resources, у которого Closeable-переменные, объявленные в круглых скобках, видны только внутри. Это тоже может доставить проблем, чувствую.
Связывание паттерна" str вышло за пределы фигурных скобок условного оператора?

Да, это называется flow scoping.
А, это не баг, это фича. Ок.

Спасибо за статью. :)
Имхо не хватает еще вот такого случая в примерах:
Object obj = "a";
if (obj instanceof String s) {
    obj = "b";
    System.out.println(s.toLowerCase());
}

Как сработает этот код? Не скомпилируется? Выдаст runtime error? Просто выведет «a»? Просто выведет «b»? Если выведет «a», то поведение уже не как у instnceof + cast, которое вывело бы значение obj на момент выполнения println

Конечно же, поведение именно такое, как у instanceof + cast:


Object obj = "a";
if (obj instanceof String) {
    String s = (String)obj;
    obj = "b";
    System.out.println(s.toLowerCase());
}
Попробуйте еще тренарный оператор помучить. Что-то типа

int a = AA instanceof Integer aa? 1: null;
Это не очень интересно, потому что всё работает аналогично if. А вот что-нибудь другое помучить можно, например:
for (Object obj = ...; obj instanceof String str; obj = ...) {
    // Используем str
}

Или вот такое:
final boolean ok = obj instanceof String str;
if (ok) {
    System.out.println(str.toLowerCase());
}

Последнее, кстати, не работает.
работает аналогично if.

В тренарном есть такой финт, который компилируется, но вызывает ошибку исполнения:

int a=false?1:null;


если преобразовать в if, то не скомпируется
int a;
if(false){a=1;}else{a=null;}


error: incompatible types

В плане скоупинга переменных паттерна работает аналогично. Про вывод типов никто ничего не говорил.

Note: A.java uses unchecked or unsafe operations.
Похоже, опять придётся полагаться на инспекции в IDE.

Поведение идентично unchecked cast, и реакция компилятора соответствующая, так что никаких проблем нет. Предупреждения компилятора надо всё-таки читать.

Проблема есть. Она заключается во внешней обманчивости конструкции instanceof List<Integer>. Когда новичок смотрит на такое, то у него может возникнуть ложное впечатление, будто он на самом деле проверяет список на наличие только целых чисел в нём. Да, раньше было то же самое, но оно по крайней мере всё было явно: сначала проверяем instanceof List, а потом приводим объект к List<Integer>. Сейчас всё это запрятано внутрь нового instanceof'а и разобраться в деталях стало немного сложнее.
ложное впечатление, будто он на самом деле проверяет список на наличие только целых чисел в нём

Но ведь это ничем не отличается от явного приведения:


final var intList = (List<Integer>) list;

Тоже создаёт "ложное впечатление, будто он на самом деле проверяет список на наличие только целых чисел в нём", ведь обычно приведение типа это делает (с массивами например).

Эх, чего только не придумают, лишь бы на Kotlin не переходить...


if (a != null) {
    B b = a.getB();
    if (b != null) {
        C c = b.getC();
        if (c != null) {
            System.out.println(c.getSize());
        }
    }
}

Эквивалент:


a?.b?.c?.let { println(it.size) }

Ну или если с проверкой типов


(((a as? A)?.b as? B)?.c as? C)?.let { println(it.size) }

Не очень красиво, но можно ещё так:


inline fun <reified T> Any?.cast(): T? = this as? T

a.cast<A>()?.b.cast<B>()?.c.cast<C>()?.let { println(it.size) }

Ну потому что куча больших проектов уже написано на Java и на переписывание этого добра на Kotlin нет ни сил, не средств

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

Целый Kotlin разработали, лишь бы на скалу не переходить

Спасибо за статью. Вот паттерн патчинг и в Java завозят. Хотелось также чтобы type test pattern также работал и с switch. Не хватает также deconstruction pattern и других паттернов. Но думаю, должны со временем завести.
Хотя многие паттерны можно реализовать и в виде библиотеки, но поддержка на уровне языка это круто.
Вот паттерн патчинг и в Java завозят

Всё-таки матчинг :)

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

Нет, пока не обсуждается. Честно говоря, мне кажется, что это плохая фича для джавы.

А не расскажете кратко — почему это плохая идея? В Scala и Kotlin перегрузка операторов вроде нормально живет. На ум приходит только, то что появится новое поле для ошибок, но это почти с любой фичей так.

Начнём с того, что ни у кого нет цели сделать из Джавы новый Котлин или Скалу. Какой смысл, если Котлин и Скала уже существуют? Джава — это отдельный язык со своей философией. У Котлина и Скалы другая философия. Это прекрасно, что у программистов есть выбор, какой философии следовать. Если сделать из Джавы второй Котлин, выбор пропадёт.


В Джаве есть одна хорошая вещь: в большинстве случаев локального контекста понятно, чтобы выяснить, что делает данная строчка кода. Скажем, если вы видите a[b] = c * d, вы знаете точно, что здесь у вас происходит запись произведения в элемент массива в куче, вне зависимости от того, что такое a, b, c и d, вне зависимости от того, какие у вас есть импорты и библиотеки, в каком классе вы выполняете этот код. Вы точно знаете, что в этой строчке у вас нет сетевого запроса или доступа к базе данных. В Котлине или Скале вы не уверены, эта строчка может делать абсолютно всё что угодно. Я считаю, что ясность — важная отличительная черта Джавы, и Джава перестанет быть Джавой, если потеряет ясность. Причём это усложнит не только чтение кода человеком, но и средства автоматического анализа кода. Некоторые вещи могут вообще перестать работать.

Причём это усложнит не только чтение кода человеком, но и средства автоматического анализа кода. Некоторые вещи могут вообще перестать работать.

Если честно, мне вот эту мысль в чуть более подробной форме ожидал увидеть. От кого же еще, как ни от вас.
Про философию я и раньше читал, но на то она и философия — очень субъективно.
Мне вот кажется, что код
a.set(b, c.multiply(b))
читается хуже, чем
a[b] = c * d
Особенно если формула вычислений подлиннее. И это стоит рисков по появлению нежелательных действий в перегруженных операторах. К тому же оператор + уже в каком то смысле перегружен, он означает и сложение и конкатенацию, а при помощи var можно вообще содомию JavaScript устроить
int a = 2;
String b = "1";
var c = a + b; // c = "21", wtf?
Кстати var же ввели, на тему него тоже была куча холиваров, но нежелание писать boilerplate code победило желание иметь тотальную ясность, точнее ее отдали на откуп писателям.

P.S. Всякие мимокрокодилы, типа меня, про перегрузку операторов начали спрашивать еще до появления Scala и Kotlin, ибо приходили из С++.
P.P.S. Приду домой — загляну в IDEA с Kotlin, там нет фичи подсветки перегруженного оператора?

Уже есть и давно, в восьмой джаве появилось.

Да, появилась возможность писать плагины к jvm. Например можно взять два BigDecimal a + b, а во время компилации это будет заменено на вызов метода add. Поищите, в интернете есть и видео и статьи как это делается, в том числе и на русском.

А оно с OpenJDK дружит?
Теоретически должно, так как описано в виде JSR-199, но там куча com.sun.tools.* классов.

Основная разработка явы ведётся как раз в OpenJDK, так что дружит.

во время компилации

Не совсем понял вас: будет изменён байт-код, выданный javac-ом, или уже выход оптимизирующего компилятора?

Во время компиляции компилятор проходит через 5-7 этапов (не помню точно) начиная с parse где происходит чтение исходников и заканчивая на generate где он генерирует class файлы, дак вот, можно вмешатся в некоторые стадии этого процесса и получить нужны нам class файл. Чтобы сделать это, необходимо имплементировать интерфейс Plugin.


https://docs.oracle.com/javase/8/docs/jdk/api/javac/tree/com/sun/source/util/Plugin.html

Если рассматривать хакинг javac как допустимое решение, то как минимум с JDK 1.6.
Работа The Hacker’s Guide to Javac была написана в далёком 2008 году. А пролезать во чрево javac через annotation processor или через com.sun.source.util.Plugin — это детали реализации.

Про перекрытие полей спросил. А про || false — это в amber-dev можно спросить, там открытый мейлинг-лист.

Про || false я подумал и похоже понял. С первого взгляда может показаться, что они эквиваленты с && true, однако это не так:
if (obj instanceof String str && <любое условие>) {
    // Можем всегда использовать str
}

Но в случае с || уже совсем другая картина:
if (obj instanceof String str || <любое условие>) {
    // Не можем использовать str
}

Допустим, мы могли бы сделать по-умному, и во втором случае разрешить использовать str, если <любое условие> могло бы быть вычислено в false во время компиляции, но это было бы очень хрупко и делало бы опасным рефакторинг:
if (obj instanceof String str || false) {
    // str работает
}

private boolean isFalse() {
    return false;
}
...

if (obj instanceof String str || isFalse()) {
    // str уже не работает
}

Если программист захочет заинлайнить isFalse(), то код ломается.

А сдается мне, что это улучшит одну десятитысячеую часть кода, если не одну милионную. Приколько, но…

Посмотрел наш проект (320k строк кода). instanceof используется 2290 раз. Почти все из них можно переписать на паттерн-матчинг.

Один instanceof на каждые 150 строчек кода? А может, "что-то в консерватории подправить"?

Зависит от проекта. В исходниках IntelliJ IDEA тоже многие тысячи instanceof. По сути дела многие инспекции — это поиск паттернов по дереву, паттерн-матчинг в чистом виде. Ну и в целом нет ничего плохого в instanceof, если правильно его использовать. Часто это существенно лучше, чем visitor pattern, который в джаве выглядит откровенно по-уродски, занимает больше места в исходниках и имеет больше накладных расходов при исполнении.

Все это так, но сколько таких проектов, как IntelliJ? Одна сотая? Вот и получается та самая одна десятитысячная.

На этой одной сотой держится вся прикладная разработка (и не только проектов на яве, кстати). Вы готовы пересесть на какой-нибудь "Саблайм" и запускать сборку/тесты каждый раз из командной строки? Я — нет. Посему, имхо, именно в этом месте лучше оставить как есть.

  1. IntelliJ уже написана.)
  2. Как я понимаю, все большая ее часть становится написана на Kotlin.

Интересно, а если объявить такую переменную просто в теле метода, или можно только в условных операторах?

Можно просто объявить:
boolean x = obj instanceof String str;

В IDE добавят предупреждение о том, что str не используется.

Но boolean x = obj instanceof String str && !str.isEmpty(); — уже вполне полезный код.

Sign up to leave a comment.

Articles