Как стать автором
Обновить

Вышла Java 21

Уровень сложностиСредний
Время на прочтение18 мин
Количество просмотров55K
Всего голосов 74: ↑74 и ↓0+74
Комментарии49

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

Да, это жестко, волею судеб мы используем на одном проекте Java 11. Но часть зависимостей перешли на Java 17 и пришлось на OpenRewrite писать транслятор рекордов в классы, транслятор instanceof с pattern-матчингом в обычные instanceof с отдельным объявлением переменной, транслятор многострочных литералов в обычные литералы и т.д. Я узнал много всего интересного, например, что можно писать так:

if (!(obj instanceof String str)) {
    throw new IllegalArgumentException();
}
System.out.println(str.length());

А ещё так (это два разных str):

if (!(obj instanceof String str)) {
    Integer str = null;
    System.out.println(str);
} else {
    System.out.println(str);
}

Спорим, вы не догадаетесь просто глядя на код какие варианты корректные (в каких вариантах str доступен в if, а в каких в else):

if (!(obj instanceof String str) || false) {
    System.out.println(obj);
} else {
    System.out.println(str);
}

if (!(obj instanceof String str) || true) {
    System.out.println(obj);
} else {
    System.out.println(str);
}


if (!(obj instanceof String str) && false) {
    System.out.println(obj);
} else {
    System.out.println(str);
}

if (!(obj instanceof String str) && true) {
    System.out.println(obj);
} else {
    System.out.println(str);
}

Если справились, то тут вообще без труда найдёте вариант с ошибкой:

if (!(obj instanceof String str) && obj instanceof String str) {
    System.out.println(str);
} else {
    System.out.println(obj);
}

if (obj instanceof String str || !(obj instanceof String str)) {
    System.out.println(obj);
} else {
    System.out.println(str);
}

if (!(obj instanceof String str) || obj instanceof String str) {
    System.out.println(obj);
} else {
    System.out.println(str);
}

Если это слишком просто для вас, то добавьте ещё условий, вложенных if'ов, с несколькими переменными и т.д. на свой вкус. Это как бы такая переменная Гейзенберга пока не скопируешь код в IDE нельзя быть точно уверенным какая у неё область видимости.

Мне пришлось написать тестов больше чем само преобразование кода. Рекорды - это просто кайф.

А теперь сбылась моя мечта и они соединили instanceof и record! Наконец-то! Я уже предчувствую сколько ещё увлекательной работы мне предстоит! Если вы разрабатываете какой-нибудь популярный фреймвок, то бросьте все дела и срочно переведите его на Java 21! Чтобы всякие ретрограды на Java 11 страдали.

Это конечно всё ирония. Вообще интересно когда люди добавляли pattern-matching в instanceof рассматривали ли они все эти случаи. Понятно что так лучше не писать код, но это открытый вопрос нужно ли добавлять такие конструкции в язык, чтобы потом обвешивать их правилами линтера. И ещё интересный вопрос есть ли теоретическая возможность добавлять в язык новый синтаксический сахар, но чтобы код мог выполняться и на предыдущих версиях JVM...

Да, это странно, реализация практически один-в-один совпадает с С#, но там таких пазлов нет. Вероятно у них какие-то свои соображения были, хотелось бы знать какие.

волею судеб мы используем на одном проекте Java 11

Что мешает обновиться?

Что угодно, полагаю. Начиная от менеджмента и отсутствия подходящих инструментов у девопсов, заканчивая Jigsaw и несовместимыми с новой версией джавы зависимостями.

а вот интересно, что вы имеете в виду под "инструментами девопсов"? и как они привязаны к java 11?

У нас билд сервис, глобальный для всех команд, который поддерживают люди из разных команд на пол ставки так сказать. И билд сервис поддерживает только java 17. Пока они не добавят 21, все будут вынуждены использовать java 17 макс. А когда они это сделают, зависит от менеджмента, от занятости в своих проектах и тд.

НЛО прилетело и опубликовало эту надпись здесь

Менеджменту без разницы какая Java и какие фреймвоки используются, это техническое решение. Есть такая задача: нужно написать приложение, при этом у заказчика Java 11 и с этим сложно что-то сделать и ещё есть фреймвок, который упростит разработку приложения в сто раз, но он на Java 17. Написать транслятор кода - это самое простое решение на мой взгляд в данной ситуации. Они не на столько сложные, если интересно:

Причем первые три я закоммитил в репозиторий OpenRewrite и частично переложил на сообщество ответственность по их сопровождению :) Транслятор рекордов они к сожалению не взяли, потому что у них позиция что они наоборот переводят код на новые версии Java, а даунгрейд рекордов в классы не соответствует целям их проекта. Хотя для меня это спорный вопрос, я думаю, что могут быть разные причины для преобразования рекорда в класс. Хотя, блин, сейчас задумался какие, в принципе для рекордов можно реализовать кастомные геттеры, equals(), toString() и т.д. если нужно. Наверное основная причина - это если понадобилось сделать сущность мутабельной и возможно это не на столько частый сценарий.

Что мешает обновиться?

Это приложение с повышенными требованиями к безопасности. У заказчика кастомная Java, из которой выпилены вещи типа изменения уровня доступа к методам с private на public через рефлексию и т.д. Вносить все эти изменения в JDK долго и сложно и технически, и организационно. Реально на порядок проще просто форкнуть проекты на Java 17 и написать транслятор кода на Java 11. С Java 21 это будет сложнее, но я надеюсь, что люди не побегут сразу переписывать весь код с использованием новых фич языка. Когда внутренний проект переводится на новую Java, то проблем вообще нет. Но я не понимаю зачем спешить с обновлением Java в фреймвоках, которые используются в куче проектов.

Самая жесть, что эти вещи с рефлексией зачем-то используют в достаточно популярных фреймвоках типа Spring Boot. Что очень затрудняет их применение в проектах с высокими требованиями к безопасности.

К тому же Java 11 не на столько древняя. Ей всего лишь 5 лет и поддержка ещё не закончилась.

В общем если вы думаете, что мешает обновиться лень или какие-то надуманные внутренние организационные причины, то точно нет. Например, недавно обновил на фронте React до 18-ой версии, хотя в части зависимостей используется React 17. Там конечно были танцы с бубнами, но всё получилось.

Если в целом говорить об обратной совместимости, то мне конечно больше всего нравится Lisp. Потому что в нём вообще толком нет синтаксиса. Там не нужно 10 лет ждать когда в языке наконец появится какой-то синтаксический сахар. Если он нужен, то можно просто самому его написать. Или наоборот если в каком-то фреймвоке этот синтаксический сахар используется, то чтобы использовать этот фреймвок у себя не нужно обновлять версию компилятора или виртуальной машины. Короче в Lisp просто в принципе проблемы с этим отсутствуют.

И на втором месте после Lisp - это JavaScript. Идея с полифилами просто отличная. Если какая-то фича в языке нужна уже сейчас, то ненужно ждать её релиза. И, наоборот, если эта фича уже есть в языке, но не поддерживается в браузерах у части клиентов, то тоже проблем нет. И это вменяемый нормальный подход. Для Java мне фактически приходится сейчас писать те же полифилы на OpenRewrite. Если бы их написали авторы этого синтаксического сахара, то всё было бы проще.

Самая жесть, что эти вещи с рефлексией зачем-то используют в достаточно популярных фреймвоках типа Spring Boot

Ну это просто. Рефлексия и кодогенерация позволяет очень сильно упростить логику взаимодействия внешнего кода с фреймворком.

Кстати. Именно такие фреймворки и сподвигли сделать оптимизации в JVM благодаря чему мы теперь имеем быструю рефлексию, serialisation/deserialisation, dyn-функции и т.д.

Ужос какой.

"У заказчика кастомная Java, из которой выпилены вещи типа изменения уровня доступа к методам с private на public через рефлексию"

Такая возможность вырезана из java после 9 версии. А вы всё поезда под откос пускаете, не знаете что война закончилась

В шарпах точно есть куча сахара, которая не трогает рантайм. Если сахар действительно сахар и просто разворачивается в "старый добрый" код.

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

Было бы очень круто, если бы код на Java 21 можно было скомпилировать таким образом, чтобы он запускался на Java 11. С одной стороны, возможно это какие-то дополнительные сложности для разработчиков языка. Но в JavaScript это ведь получилось через полифилы. Давно не писал под .NET, но если я правильно помню там тоже с этим проблем нет, можно выбрать предыдущую версию целевого фреймворка.

В Java это целая история т.к. стандартная библиотека прибита к JRE. И компилировать код 17-ой версии для запуска со стандартной библиотекой 11-ой версии - можно, конечно. Но это нужно будет делать очень аккуратно (и не всегда получится, если код требует фишек JVM 17-ой версии)

В этом плане мне больше нравится Котлин, который до сих пор умеет компиляцию в байткод JVM 8 (а стандартная библиотека у него своя). То есть можно писать на последней версии Котлина, используя весь доступный там синтаксический сахар, а затем завернуть в докер-образ на базе JRE 8 и отдать на стенды.

Синтаксически мы стали еще немного ближе к Kotlin. В связи с этим, интересно, через многие десятки версий, сотни ошибок, и тысячу новых идей, могут ли языки прийти к одинаковому синтаксису, но разной подкапотной реализацией (для разных задач) или нет?

Вряд ли, в джаве многие вещи пытаются сделать правильно с математической точки зрения, в то время как в Котлине подход более прагматический. Бреслав как-то рассказывал, что в джаве дженерики у методов идут перед названием метода (Collections.<String>emptyList()), потому что это правильно, а в Котлине они идут после, потому что это естественее (listOf<String>()) .

В джаве так сделали, потому что есть синтаксическая неоднозначность, например, в таком кейсе:

foo(bar<A, B>(x+1))

Кажется, что тут вызов функции bar с двумя типовыми аргументами и одним обычным.
Но можно чуть-чуть поменять форматирование, чтоб стало понятно, что интерпретировать это можно и иначе:

foo(bar < A, B > (x+1))

Тут уже никакой функции bar нет, а просто в функцию foo передаются два аргумента.

В джаве решили эту проблему, переместив дженерики в начало. И это не консистентно с дженериками у конструкторов.
А в котлине сказали "Нам пофиг, интерпретируем это как вызов функции с типовыми аргументами. Хотите чтоб интерпретировалось иначе - поставьте скобки"

Тут про это рассказано

НЛО прилетело и опубликовало эту надпись здесь

Я бы сказал к C# с точностью до особенностей синтаксиса)

С трудом могу себе представить зачем нужен pattern matching по типу. А вот чего мне не хватает, так это case с условиями, типа:

int n = ....;
switch (){
    case n < 6 -> ....;
    case n > 7 && n < 10 -> ....;
    case n in 22..65 -> ....;
}

В типобезопасных системах.

Можно и ограничить наследников и перебрать их в switch. И этот код будет точно работать так как ожидалось или упадёт при компиляции. Джава стала еще на шаг ближе к хорошим языкам.

Эх, когда же discriminated unions уже завезут в Java/C#. Всё какие-то полумеры

Я бы JEP с ними почитал. Мне даже не особо понятно как их корректно и красиво в джава синтаксисе выразить и не сломать при этом весь уже написанный код.

В C# мне ОЧЕНЬ зашёл пакет OneOf, почитайте про него.

Да, я его юзаю во внутренней логике, но всё же его использование выглядит громоздко, AutoMapper не смапит его автоматически, из API OneOf<> не вернешь без приседаний и спеку swagger нативно для него не сгенерирует.


Будь DU нативно в языке, тулинг бы подтянулся, в отличие от библиотеки о которой не слышали 95% разработчиков

Что касается API — Swagger и DU не подхватит, наверное (на крайняк это будет огромный JSON со всеми возможными полями и собственно дискриминаторами). Но оно у нас хорошо зашло в сервисном слое — от бизнес-логики приходит OneOf, а контроллер его уже раскладывает по разным Result'ам. Довольно наглядно получается.

Вообще в спеке есть one-of.
Правда обычно хотя бы один из OneOf это некоторая ошибка, а её не только как body вернуть надо, но и statusCode выставить нужный, так что пожалуй действительно от ручного маппинга до конца не получится избавиться.

А чем вас sealed class + record + pattern matching в качестве этих самых discriminated union не устраивают?

public class Main {

  sealed interface Data permits SuccessData, ErrorData {}
  record SuccessData(String result) implements Data {}
  record ErrorData(int errorCode) implements Data {}

  public static void main(String[] args) {
    final String message = switch(getData()) {
      case SuccessData s -> s.result;
      case ErrorData e -> String.valueOf(e.errorCode);
    };

    System.out.println(message);
  }

  private static Data getData() {
//    return new SuccessData("TEST");
    return new ErrorData(404);
  }

}

Код написал для Java 17, так как она под рукой, в более новой версии будет еще красивее, т.к. можно деконструировать record сразу в pattern matching-е.

Я прост шарпист, за джавой не особо слежу :) Ключевое в вашем примере насколько я понял это sealed interface Data permits SuccessData, ErrorData, благодаря которым компилятор может проверить switch на полноту покрытия, чего не происходит в C#. Тогда действительно это вполне можно юзать как DU

Да, именно так.

А почему нельзя рядом с основным С# кодом, создать F# проект. И в нем создать типы, а в C# использовать.

Как-то я ничего не смог нагуглить про такой трюк, а в официальном дискорде по C#, мой вопрос то ли пропустили, то ли проигнорировали.

Потому что для работы таких фич как паттерн-матчинг или проверка полноты в switch недостаточно импортировать типы, нужна ещё и их поддержка со стороны компилятора. А без "сопутствующих" фич discriminated unions в языке нафиг не нужны.

Хорошо, нечто более продвинутое, увы не работает.
Но как минимум можно создавать всякие CustomerId которым нельзя присвоить просто int или OrderId. Уж это-то компилятор шарпа не даст перепутать.
Я собственно из-за это фичи и спрашивал изначально. Или такое улучшение того не стоит в масштабах проекта?

А зачем для этого F#, если можно просто record struct в С# объявлять?

Изначально кажется что record struct сложный тип, чрезмерный для одного значения. Или иными словами даже в голову не пришло.
Но учитывая то как доступен F# тип в C#, особой разницы уже не видно.
Нет в мире идеала.

Cделать тип CustomerId можно и на C#, это не так сложно (пусть и сложнее чем newtype в F#). Только вот дальше придётся:


  1. объяснить (де)сериализатору как его принимать и передавать;
  2. объяснить сваггеру как его публиковать в API;
  3. объяснить EF как его хранить в базе;
  4. и так с каждой интеграцией.

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


И даже если все пункты будут выполнены — про связь между CustomerId и Customer тот же EF знать не будет, а значит возможность ошибки будет оставаться.


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

Из вне и в базе будет long.
То есть контроллер принимает модель.
Модель валидируется.
Мапится на модель/тип на прямую.
Некие действия/хождения по логике.
EF:
modelBuilder.Entity<TestEFInterop>() .Property(x => x.Id) .HasConversion<long>();
Клиенту возвращается специфичная DTO.
Итого пара строчек для EF на каждое такое поле, и в маппере с/на каждую связанную DTO.
Cериализатора и сваггера не касается.
Да местами чуть больше возни, но и больше гарантий что что-то не то не улетит не туда.

Это полумера. Вы никак не гарантируете, что у сущности Customer будет первичный ключ CustomerId, а у других сущностей будет другой ключ. Да и соответствие типа внешнего ключа первичному можно проверить только при запуске, но не при компиляции.


Cериализатора и сваггера не касается.

Опять полумера, из-за которой у вас будут постоянные преобразования типов туда-сюда. А где преобразование типов — там может и ошибка спрятаться.

Касательно "что у сущности Customer будет первичный ключ CustomerId".
Предполагаю что изначальная ошибка в бойлерплейте существенно менее вероятна и будет отловлена раньше чем ошибка в логике.
К тому же это все пишется однократно, а в логике используются/переписывается много кратно, с большим количеством разнообразного контекста в голове.
Внешний ключ это что? Вне системы есть только int/long и.т.д. . Дальше маппер конвертирует в правильный тип. А ошибка в конвертации - начало это комментария. Неверное использования API, вообще другого рода проблема.
Глобальная мысль в том что добавляются правила в которых можно ошибиться, но вероятность ниже чем ошибка без этих правил.

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

Там в первых примерах был when. Надеюсь, это оно, и что там может быть что угодно, приводимое к boolean'у.

```

Object obj = …
return switch (obj) {
case Integer i when i > 0 -> String.format("positive int %d", i);
case Integer i -> String.format("int %d", i);
case String s -> String.format("String %s", s);
default -> obj.toString();
};

```

UPD: что-то не могу копипасту из статьи в маркдаун с телефона облечь...

Как бы да, но что-то громоздко как-то получается. Приходится мешать одно с другим и лёгкий прозрачный синтаксис switch-case куда-то исчезает...

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

НЛО прилетело и опубликовало эту надпись здесь

Особенно порадовал, что теперь можно писать скрипты на java)

Ещё использование _ для не используемых переменных

Helidon 4, который выйдет в течение месяца, уже полностью перенаписан под виртуальные треды. Нетти просто выкинули за ненадобностью, производительность стала еще лучше (данные есть в прогонах TechEmpower).

Все яп в последнее время становляться все сложнее и сложнее , при этом не давая никаких новых преимуществ... почитаешь и хочется "сбежать" на старый, добрый С . :(

Писали один кроссплатформенный продукт где была часть на С и часть на Java. Моя часть была на Java (в то время 4й). Я, естественно, доказывал преимущества Java в переносимости, зная хорошо С при этом. Самое смешное, что, по факту, в этом выиграл С на то время. Только и выкпутились тогда используя IBM JDK, т.к. от Sun безбожно глючила или фичи нормально работали только на Solaris.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории