Java значительно изменилась с годами. Прочтите сравнение версий 8 и 17 и узнайте ответ на вопрос: стоит ли обновляться?
Через несколько месяцев, в марте 2022 года, Java 8 закончится поддержка Oracle Premier Support. Это не означает, что он не будет получать никаких новых обновлений, но усилия Oracle, вложенные в его поддержку, вероятно, будут значительно меньше, чем сейчас.
Это означает, что будет веская причина для перехода на новую версию, тем более, что 14 сентября 2021 года была выпущена Java 17. Это новая версия долгосрочной поддержки, с Oracle Premier Support, которая продлится до сентября 2026 года (как минимум).
Что дает Java 17? Насколько трудной будет миграция? Стоит ли оно того? Я постараюсь ответить на эти вопросы в этой статье.
Популярность Java 8: немного истории
Java 8, выпущенная в марте 2014 года, в настоящее время используется 69% программистов в своем основном приложении. Почему по прошествии более 7 лет это все еще самая распространенная версия? На то есть много причин.
Java 8 предоставила множество языковых функций, которые заставили разработчиков захотеть перейти с предыдущих версий: Lambdas, потоки, функциональное программирование, обширные расширения API - не говоря уже о расширениях MetaSpace или G1. Эту версию Java стоило использовать.
Java 9 появилась 3 года спустя, в сентябре 2017 года, и для типичного разработчика она практически ничего не изменила: новый HTTP-клиент, API процессов, незначительный diamond оператор и улучшения в методе try-with-resources.
Фактически, Java 9 действительно внесла одно существенное изменение - новаторский проект Jigsaw. Java сильно изменилась, многое изменилось - но внутренне.
Модульность Java дает большие возможности, позволяет решать множество технических проблем и применима ко всем, но действительно необходима только относительно небольшой группе пользователей, глубоко понимающих изменения.
Из-за изменений, внесенных в Jigsaw Project, многие библиотеки потребовали дополнительных доработок, были выпущены новые версии, некоторые из них не работали должным образом.
Миграция на Java 9 - в частности, для крупных корпоративных приложений - часто была сложной, трудоемкой и вызывала проблемы регрессии. Так зачем же это делать, если выгоды мало, а это стоит много времени и денег?
Это подводит нас к сегодняшнему дню, октябрю 2021 года. Java Development Kit 17 (JDK 17) был выпущен всего месяц назад. Не пора ли отказаться от Java 8 7-летней давности? Во-первых, давайте посмотрим, что есть в Java 17. Что это дает программисту и администратору или SRE по сравнению с Java 8?
Java 17 против Java 8: изменения
В этой статье рассматриваются только те изменения, которые я считаю достаточно важными или интересными, чтобы упомянуть их. Это не включает все, что было изменено, улучшено, оптимизировано за все годы развития Java. Если вы хотите увидеть полный список изменений в JDK, вы должны знать, что они отслеживаются как JEP (предложения по расширению JDK). Список можно найти в JEP-0.
Кроме того, если вы хотите сравнить Java API между версиями, есть отличный инструмент под названием Java Version Almanac. Было много полезных небольших дополнений к Java API, и посещение этого веб-сайта, вероятно, лучший вариант, если кто-то хочет узнать обо всех этих изменениях.
А пока давайте проанализируем изменения и новые функции в каждой итерации Java, которые наиболее важны с точки зрения большинства из нас, разработчиков Java.
Новое ключевое слово var
Было добавлено новое ключевое слово var, которое позволяет более кратко объявлять локальные переменные. Рассмотрим этот код:
// java 8 способ
Map<String, List<MyDtoType>> myMap = new HashMap<String, List<MyDtoType>>();
List<MyDomainObjectWithLongName> myList = aDelegate.fetchDomainObjects();
// java 10 способ
var myMap = new HashMap<String, List<MyDtoType>>();
var myList = aDelegate.fetchDomainObjects()
При использовании var объявление становится намного короче и, возможно, немного более читаемым, чем раньше. Сначала необходимо принять во внимание удобочитаемость, поэтому в некоторых случаях может быть неправильно скрывать тип от программиста. Позаботьтесь о том, чтобы правильно назвать переменные.
К сожалению, с помощью ключевого слова var нельзя присвоить значение лямбда переменной:
var fun = MyObject::mySpecialFunction;
// вызывает ошибку компиляции: (method reference needs an explicit target-type)
Однако можно использовать var в лямбда-выражениях. Взгляните на пример ниже:
boolean isThereAneedle = stringsList.stream()
.anyMatch((@NonNull var s) -> s.equals(“needle”));
Используя var в лямбда-аргументах, мы можем добавлять аннотации к аргументам.
Records
Можно сказать, что Records (записи) - это ответ Java на Lombok. По крайней мере, частично. Запись - это тип, предназначенный для хранения некоторых данных. Позвольте мне процитировать фрагмент JEP 395, который хорошо их описывает:
[…] Запись автоматически получает множество стандартных членов:
Private final поле для каждого компонента описания состояния;
Public метод доступа для чтения для каждого компонента описания состояния с тем же именем и типом, что и компонент;
Public конструктор, сигнатура которого совпадает с описанием состояния, который инициализирует каждое поле из соответствующего аргумента;
Реализации equals и hashCode, которые говорят, что две записи равны, если они одного типа и содержат одно и то же состояние; а также
Реализацию toString, которая включает строковое представление всех компонентов записи с их именами.
Другими словами, это примерно эквивалент @Value Lombok. С точки зрения языка это похоже на enum. Однако вместо объявления возможных значений вы объявляете поля. Java генерирует некоторый код на основе этого объявления и может обрабатывать его более оптимизированным способом. Как и enum, он не может расширяться или расширяться другими классами, но может реализовывать интерфейс и иметь статические поля и методы. В отличие от enum, запись может быть создана с помощью ключевого слова new.
Запись может выглядеть так:
record BankAccount(String bankName, String accountNumber) implements HasAccountNumber {}
Вот и все: довольно коротко. Коротко - это хорошо!
Любые автоматически сгенерированные методы могут быть объявлены программистом вручную. Также можно объявить набор конструкторов. Более того, в конструкторах все поля, которые явно не присвоены, неявно назначаются соответствующим параметрам конструктора. Это означает, что присвоение можно полностью пропустить в конструкторе!
record BankAccount(String bankName, String accountNumber) implements HasAccountNumber {
public BankAccount { // <-- это конструктор! нет () !
if (accountNumber == null || accountNumber.length() != 26) {
throw new ValidationException(“Account number invalid”);
}
// здесь нет необходимости в присвоении!
}
}
Для получения всех подробностей, таких как формальная грамматика, примечания по использованию и реализации, обязательно обратитесь к JEP 359. Вы также можете посмотреть StackOverflow на самые популярные вопросы о Java Records.
Расширенные Switch выражения
Switch присутствует на многих языках, но с годами он становился все менее и менее полезным из-за имеющихся у него ограничений. Остальные части Java выросли, switch - нет. В настоящее время case ветви Switch могут быть сгруппированы намного проще и более читаемым образом (обратите внимание, что нет никакого break!), а само Switch выражение фактически возвращает результат.
DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
boolean freeDay = switch (dayOfWeek) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> false;
case SATURDAY, SUNDAY -> true;
};
Еще большего можно достичь с помощью нового ключевого слова yield, которое позволяет возвращать значение из блока кода. Это фактически return, который работает внутри case блока и устанавливает значение как результат выполения switch. Он также может принимать выражение вместо одного значения. Давайте посмотрим на пример:
DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
boolean freeDay = switch (dayOfWeek) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> {
System.out.println("Work work work");
yield false;
}
case SATURDAY, SUNDAY -> {
System.out.println("Yey, a free day!");
yield true;
}
};
Сопоставление с образцом Instanceof
На мой взгляд, instanceof не является революционным изменением, но решает одну из вызывающий наибольшее раздражение проблем языка Java. Вам когда-нибудь приходилось использовать такой синтаксис?
if (obj instanceof MyObject) {
MyObject myObject = (MyObject) obj;
// … дальнейшая логика
}
Теперь вам не придется это писать. Теперь Java может создать локальную переменную внутри if, например:
if (obj instanceof MyObject myObject) {
// … та же логика
}
Всего лишь одна строка удалена, но это была совершенно ненужная строка с точки зрения потока кода. Более того, объявленную переменную можно использовать в том же условии if, например:
if (obj instanceof MyObject myObject && myObject.isValid()) {
// … та же логика
}
Sealed классы
Это сложно объяснить. Начнем с этого: раздражало ли вас когда-нибудь предупреждение «no default» в switch? Вы рассмотрели все варианты, которые поддерживал домен, но, тем не менее, получали предупреждение. Sealed (запечатанные) классы позволяют избавиться от такого предупреждения при проверке типа instanceof.
Если у вас есть такая иерархия:
public abstract sealed class Animal
permits Dog, Cat {
}
public final class Dog extends Animal {
}
public final class Cat extends Animal {
}
You will now be able to do this:
if (animal instanceof Dog d) {
return d.woof();
}
else if (animal instanceof Cat c) {
return c.meow();
}
. . . вы не получите предупреждения. Что ж, позвольте мне перефразировать это: если вы получите предупреждение с аналогичной последовательностью, это предупреждение будет осмысленным! Больше информации всегда хорошо.
У меня смешанные чувства по поводу этого изменения. Введение циклической ссылки не кажется хорошей практикой. Если бы я использовал это в своем производственном коде, я бы сделал все возможное, чтобы спрятать это где-нибудь глубоко и никогда не показывать внешнему миру. Я имею в виду, что я бы никогда не раскрыл его через API, не то чтобы мне было бы стыдно использовать его в допустимой ситуации.
TextBlocks
Объявление длинных строк не часто происходит в программировании на Java, но когда это происходит, это утомительно и сбивает с толку. В Java 13 это было исправлено и в более поздних выпусках улучшено. Многострочный текстовый блок теперь можно объявить следующим образом:
String myWallOfText = ”””
______ _ _
| ___ \ | | (_)
| |_/ / __ ___| |_ _ _ _ ___
| __/ '__/ _ \ __| | | | / __|
| | | | | __/ |_| | |_| \__ \
\_| |_| \___|\__|_|\__,_|___/
”””
Нет необходимости в экранировании кавычек или новых строк. Можно избежать символа новой строки и оставить строку однострочной, например:
String myPoem = ”””
Roses are red, violets are blue - \
Pretius makes the best software, that is always true
”””
эквивалентно:
String myPoem = ”Roses are red, violets are blue - Pretius makes the best software, that still is true”.
Текстовые блоки можно использовать для сохранения в коде разумно читаемого шаблона JSON или XML. Внешние файлы по-прежнему, вероятно, лучше, но все же хороший вариант сделать это на чистой Java, если это необходимо.
Улучшенные NullPointerExceptions
Однажды в моем приложении была такая цепочка вызовов, и я думаю, она вам тоже может показаться знакомой:
company.getOwner().getAddress().getCity();
Я получил NPE, который точно сказал мне, в какой строке был обнаружен null. Да, это была эта строка. Без отладчика я не мог бы сказать, какой объект был null, или, скорее, какая операция вызова на самом деле вызвала проблему. Теперь сообщение будет конкретным, и в нем будет сказано, что JVM «не может вызвать Person.getAddress()».
Фактически, это скорее изменение JVM, чем изменение Java, поскольку анализ байт-кода для построения подробного сообщения выполняется во время выполнения JVM; но это очень нравится программистам.
Новый HttpClient
Есть много библиотек, которые делают то же самое, но хорошо иметь правильный HTTP-клиент в самой Java. Вы можете найти хорошее введение в новые API на сайте Baeldung.
Новый метод Optional.orElseThrow()
Для Optional метод Get() используется для получения значения в соответствии со значением Optional. Если значение отсутствует, этот метод вызывает исключение, как показано в приведенном ниже коде:
MyObject myObject = myList.stream()
.filter(MyObject::someBoolean)
.filter((b) -> false)
.findFirst()
.get();
В Java 10 появился новый метод в Optional, называемый orElseThrow(). Что он делает? Точно то же! Однако рассмотрите эту возможность с позици изменения удобочитаемости для программиста.
MyObject myObject = myList.stream()
.filter(MyObject::someBoolean)
.filter((b) -> false)
.findFirst()
.orElseThrow();
Теперь программист точно знает, что произойдет, если объект не будет найден. Фактически, рекомендуется использовать этот метод вместо простого (хотя и повсеместного) get().
Другие небольшие, но приятные изменения API
Разговор не дорого стоит. Вот код, в котором Вы можете увидеть новые вещи в строках 3, 7, 8, 9, 10, 14, 18 и 19.
// инвертировав предикат, будет еще короче со статическим импортом
collection.stream()
.filter(Predicate.not(MyObject::isEmpty))
.collect(Collectors.toList());
// String тоже получила кое-что новое
“\nPretius\n rules\n all!”.repeat(10).lines().
.filter(Predictions.not(String::isBlank))
.map(String::strip)
.map(s -> s.indent(2))
.collect(Collectors.toList());
// нет необходимости передавать экземпляр массива в качестве аргумента
String[] myArray= aList.toArray(String[]::new);
// читать и писать файлы быстро!
// не забудьте отловить все возможные исключения
Path path = Files.writeString(myFile, "Pretius Rules All !");
String fileContent = Files.readString(path);
// .toList() в stream()
String[] arr={"a", "b", "c"};
var list = Arrays.stream(arr).toList();
Проект Jigsaw
Project Jigsaw из JDK 9 значительно изменил внутреннее устройство JVM. Он изменил как JLS, так и JVMS, добавил несколько JEP (список доступен по ссылке выше на Project Jigsaw) и, что наиболее важно, внес некоторые критические изменения, несовместимые с предыдущими версиями Java.
Модули Java 9 были представлены как дополнительный, самый высокий уровень организации jar и классов. По этой теме много вводного контента, например, этот на сайте Baeldung или эти слайды от Юичи Сакурабы.
Выигрыш был значительным, хотя и не видимым невооруженным глазом. Так называемого JAR-ада больше нет (ты сталкивался с этим? Я да… и это был действительно ад), хотя ад модулей теперь тоже возможен.
С точки зрения типичного программиста, эти изменения теперь почти незаметны. Каким-то образом могут быть затронуты только самые большие и самые сложные проекты. Новые версии практически всех часто используемых библиотек придерживаются новых правил и учитывают их внутри компании.
Сборщики мусора
Начиная с Java 9, G1 является сборщиком мусора по умолчанию. Он сокращает время задержки по сравнению с параллельным сборщиком мусора, хотя в целом может иметь меньшую пропускную способность. Он претерпел некоторые изменения с тех пор, как он был установлен по умолчанию, включая возможность возвращать неиспользуемую выделенную память в ОС (JEP 346).
Сборщик мусора ZGC был представлен в Java 11 и достиг состояния продукта в Java 15 (JEP 377). Его цель - еще больше сократить паузы. Начиная с Java 13, он также способен возвращать в ОС неиспользованную выделенную память (JEP 351).
Сборщик мусора Shenandoah был представлен в JDK 14 и достиг состояния продукта в Java 15 (JEP 379). Он направлен на то, чтобы время паузы было небольшим и не зависело от размера кучи.
Я рекомендую прочитать отличную серию статей Марко Топольника, в которой сравниваются GC.
Обратите внимание, что в Java 8 у вас было гораздо меньше вариантов, и если вы не меняли свой GC вручную, вы все равно использовали Parallel GC. Простое переключение на Java 17 может привести к тому, что ваше приложение будет работать быстрее и иметь более консистентное время выполнения методов. Переход на недоступные в то время ZGC или Shenandoah может дать еще лучшие результаты.
Наконец, появился новый сборщик мусора No-Op Garbage Collector (JEP 318), хотя это экспериментальная функция. Этот сборщик мусора фактически не выполняет никакой работы, что позволяет вам точно измерить использование памяти вашим приложением. Это полезно, если вы хотите, чтобы нагрузка операций с памятью была как можно ниже.
Если вы хотите узнать больше о доступных вариантах, я рекомендую прочитать отличную серию статей Марко Топольника, в которой сравниваются GC.
Осведомленность о контейнерах
Возможно вы не знали, что было время, когда Java не подозревала, что работает в контейнере. Она не учитывала ограничения памяти контейнера и вместо этого считывала размер доступной системной памяти. Итак, когда у вас был компьютер с 16 ГБ ОЗУ, максимальный объем памяти вашего контейнера был установлен на 1 ГБ и на нем работало приложение Java, тогда приложение часто отказывало, поскольку оно пыталось выделить больше памяти, чем было доступно в контейнере. Хорошая статья Карлоса Санчеса объясняет это более подробно.
Эти проблемы остались в прошлом. Начиная с Java 10, интеграция с контейнерами включена по умолчанию. Однако для вас это может быть незаметным улучшением, поскольку такое же изменение было внесено в обновление Java 8 131, хотя для этого потребовалось включить экспериментальные параметры и использовать -XX: + UseCGroupMemoryLimitForHeap.
PS: Рекомендуется указывать максимальный объем памяти для Java с помощью параметра -Xmx. В таких случаях проблема не возникает.
CDS архивы
Чтобы ускорить запуск JVM, архивы CDS претерпели некоторые изменения за время, прошедшее с момента выпуска Java 8. Начиная с JDK 12, создание архивов CDS в процессе сборки включено по умолчанию (JEP 341). Усовершенствование в JDK 13 (JEP 350) позволило обновлять архивы после каждого запуска приложения.
В отличной статье Николая Парлога показано, как использовать эту функцию для сокращения времени запуска вашего приложения.
Java Flight Recorder и Java Mission Control
Java Flight Recorder (JEP 328) позволяет отслеживать и профилировать работающее Java-приложение при низких затратах на производительность (целевой 1%). Java Mission Control позволяет принимать и визуализировать данные JFR. См. Руководство Baeldung, чтобы получить общее представление о том, как его использовать и что от него можно получить.
Стоит ли переходить с Java 8 на Java 17?
Коротко говоря, да, вам следует. Если у вас есть большое корпоративное приложение с высокой нагрузкой, и вы по-прежнему используете Java 8, вы обязательно увидите лучшую производительность, более быстрое время запуска и меньший объем памяти после миграции. Программисты, работающие над этим приложением, тоже должны быть счастливы, поскольку в сам язык внесено множество улучшений.
Стоимость этого, однако, трудно оценить и сильно варьируется в зависимости от используемых серверов приложений, библиотек и сложности самого приложения (или, скорее, количества низкоуровневых функций, которые оно использует/повторно реализует).
Если ваши приложения представляют собой микросервисы, вполне вероятно, что все, что вам нужно сделать, это изменить базовый образ докера на 17-alpine, версию кода в Maven на 17, и все будет работать нормально. Некоторые обновления фреймворков или библиотек могут пригодиться (но вы все равно делаете их периодически, верно?).
К настоящему времени все популярные серверы и фреймворки имеют поддержку Java 9's Jigsaw Project. Это производственный уровень, он был тщательно протестирован и исправлен на протяжении многих лет. Многие продукты предлагают руководства по миграции или, по крайней мере, подробные примечания к выпуску для версии, совместимой с Java 9. См. Красивую статью OSGI или некоторые примечания к выпуску Wildfly 15, в которых упоминается поддержка модулей.
Если вы используете Spring Boot в качестве фреймворка, есть несколько статей с советами по миграции, например, эта в вики по Spring Boot, эта на Baeldung и еще одна на DZone. Еще есть интересный пример от infoq. Перенос Spring Boot 1 в Spring Boot 2 - это отдельная тема, ее тоже стоит рассмотреть. Есть учебник на самой Spring Boot и статья о Baeldung, посвященная этой теме.
Если в вашем приложении не было настраиваемых загрузчиков классов, вы не сильно полагались на Unsafe, не часто использовали sun.misc или sun.security, у вас, скорее всего, все будет в порядке. Обратитесь к этой статье JDEP об инструменте анализа зависимостей Java, чтобы узнать о некоторых изменениях, которые вам, возможно, придется внести.
Некоторые вещи были удалены из Java, начиная с версии 8, включая Nashorn JS Engine, API и инструменты Pack200, порты Solaris/Sparc, компиляторы AOT и JIT, модули Java EE и Corba. Некоторые вещи все еще остаются, но не рекомендуется удалять, например, Applet API или Security Manager. Поскольку для их удаления есть веские причины, вам в любом случае следует пересмотреть их использование в вашем приложении.
Я спросил наших технических руководителей проектов в Pretius об их опыте миграции с Java 8 на Java 9+. Было несколько примеров, и ни один из них не был проблематичным. Здесь библиотека не работала и ее нужно было обновить; там требовалась некоторая дополнительная библиотека или конфигурация, но в целом это не был неудачный опыт.
Заключение
Java 17 LTS уже выпущена и будет поддерживаться долгие годы. С другой стороны, поддержка Java 8 закончится всего через несколько месяцев. Это, безусловно, веская причина подумать о переходе на новейшую версию Java. В этой статье я рассмотрел наиболее важные изменения языка и JVM между версиями 8 и 17 (включая некоторую информацию о процессе миграции с Java 8 на Java 9+), чтобы было легче понять различия между ними, а также оценить риски и выгоды миграции.
Если вы принимаете решения в своей компании, задайте себе следующий вопрос: наступит ли когда-нибудь «хорошее время», чтобы оставить Java 8 позади? Всегда нужно будет потратить определенное количество денег, всегда придется потратить какое-то время, и всегда будет существовать риск того, что потребуется выполнить некоторую дополнительную работу. Если никогда не бывает «хорошего времени», то это конкретное окно - несколько месяцев между выпуском Java 17 и окончанием Java 8 Premier Support - вероятно, лучшее, что когда-либо было.
Комментарии (3)
grossws
25.11.2021 00:47+2Map<String, List<MyDtoType>> myMap = new HashMap<String, List<MyDtoType>>();
В java 8 вы скорее напишите
Map<String, List<MyDtoType>> myMap = new HashMap<>();
, приведённый в статье способ -- это этоха 1.5/1.6.
grossws
25.11.2021 01:10+3Мне нравится потенциальная возможность перехода на 17, но по реальным ощущениям вы нарвётесь на необходимость использования
--add-opens
в каком-то количестве, практическую невозможность использования JPMS (например, если где-нибудь в зависимостях закралась guava, которая транзитивно тащит jsr305 с аннотациями вjavax.annotation
) и т. п.Apache Maven 3.6.x, Gradle 6.x, всякие плагины с неистовой любовью к xml типа maven-jaxb2-plugin, некоторые annotation processor'ы, библиотеки сериализации, использующие defineClass и т. п. радостно разваливаются при запуске под 17. Рано или поздно это, конечно, пройдёт, но переход безболезненный только в тривиальных случаях.
В реальности -- надо пробовать и тестировать. В том числе гонять свои приложения под нагрузкой на 17, смотреть как реально поведёт себя ZGC/Shenandoah, если вы до этого жили с CMS. Если у вас используется jni/jna/jnr-ffi, kryo или какой-нибудь offheap, смириться с возможной болью и потихоньку сменить рантайм.
В исходниках версию с 1.8/11 поднять всегда успеете, вслепую прыгать дальше 11, имхо, не стоит. Легче отскочить на 11 при наличии проблем.
csl
Что интересно (частность), даже в Java 17 ещё нельзя поставить var вместо PhoneNumber в выражении с переменной COMPARATOR:
Comparator<PhoneNumber> COMPARATOR = Comparator.comparingInt((PhoneNumber pn) -> pn.areaCode) .thenComparingInt(pn -> pn.prefix);