Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top
>
>
>
Автостопом по LTS: основные...

Автостопом по LTS: основные изменения при переходе с 8 на 11 Java

26 Янв 2026

Мы начинаем цикл статей, в котором рассказываем, с чем придётся столкнуться разработчику при переходе между LTS-версиями Java. В этой статье рассмотрим основные изменения, которые ждут программиста, если он решит перейти с Java 8 на Java 11.

Введение

Когда выходила Java 25, мы выпустили об этом статью. Её автор рассмотрел основные изменения и порассуждал об их удобстве и крутости для разработчика. После публикации один из читателей написал нам и выразил желание увидеть материал о том, с чем сталкивается разработчик при переходе с одной LTS-версии Java на следующую, начиная с Java 8.

Мы подумали и решили: "А почему бы и нет?!". Всё же, читая различные блоги, приходится сталкиваться с комментариями, в которых разработчики рассказывают о том, что до сих пор сидят на Java 8. И для них такая статья будет пищей для размышления об осуществлении перехода. Ну, а для других — просто приятной ретроспективой.

И начинаем мы этот цикл со сравнения Java 8 со следующей LTS-версией — Java 11.

LTS (Long Term Supported) — это модель выпуска программного обеспечения, где определённые стабильные версии получают расширенную поддержку (обновления безопасности, исправления ошибок, техническую поддержку) в течение более длительного периода, чем стандартные релизы.

Но сначала считаю нужным задать немного фактуры Java 8. Она принесла с собой достаточно передовые изменения (раз находятся те, кто спустя 11 лет до сих пор на ней сидит), и я хочу напомнить вам про самые главные из них.

Лично мне первыми на ум приходят Stream API, лямбды и ссылки на методы/конструкторы, которые превращают вот такие конструкции:

List<User> activeUsers = new ArrayList<>();
for (User user : users) {
  if (user.isActive()) {
    activeUsers.add(user);
  }
}

activeUsers.sort(new Comparator<User>() {
  @Override
  public int compare(User u1, User u2) {
    return u1.getCreatedAt().compareTo(u2.getCreatedAt());
  }
});

List<UserDto> result = new ArrayList<>();
for (User user : activeUsers) {
  result.add(UserMapper.toDto(user));
}

В более лаконичные:

List<UserDto> result = users.stream()
        .filter(User::isActive)
        .sorted(Comparator.comparing(User::getCreatedAt))
        .map(UserMapper::toDto)
        .collect(Collectors.toList());

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

И именно в Java 8 появились реализации default методов в интерфейсах.

Первые шаги Java в сторону функционально-парадигменных конструкций действительно произвели фурор в 2014 году. Но время не стоит на месте, и спустя 4 года мир увидел следующую LTS-версию — Java 11.

Вероятно, между 8 и 11 версией языка тоже произошло немало важных изменений? Давайте посмотрим.

Изменения, которые ждут разработчика в Java 11

Сразу хочу предупредить, статья — не полный сборник изменений, а лишь агрегация тех моментов, на которые Java-программисту нужно обратить внимание в первую очередь. Некоторые из них могут встать поперёк горла, поскольку в особых случаях из-за них ваш проект, написанный на Java 8, может просто так не запуститься в окружении 11-ой. Другие же помогают сделать код более лаконичным за счёт новых возможностей и конструкций языка. Итак, приступим.

Вместо тысячи слов. var, JEP 286

На всякий случай напомню, что Java — строго типизированный язык, то есть все типы должны быть известны на этапе компиляции. И до Java 10 каждый раз при инициализации переменной необходимо было полностью указывать её тип.

Но ведь компилятор может определить тип этой переменной при инициализации по выражению, а нам всё равно приходится писать её тип, даже если он длинный и неудобный. Так почему бы просто не оставить лишнее представление за кадром, использовав специальное ключевое слово?

JEP 286 предоставил нам такую возможность. Теперь вместо какого-нибудь абстрактного StateDatabaseHelperContainerMapMessage мы можем использовать лаконичный var:

var stateDbHelper = new StateDatabaseHelperContainerMapMessage();

P.S. За enterprise название спасибо ему.

Отличная практика, которую определённо стоит взять в оборот. Но стоит помнить о том, что по инициализирующему выражению должно быть понятно, с каким объектом мы имеем дело. Ниже будут вредные советы, так пользоваться var не стоит:

var x = foo();
var data = get();

Если по имени переменной и инициализирующему выражению тип однозначно непонятен —лучше использовать явную типизацию!

Система модулей в Java. JEP 261

До 9-ой версии Java-проект, будь то приложение или библиотека, представлял собой лишь набор классов, загружаемых через classpath — просто список необходимых программе классов, JAR-файлов и директорий с ними. В рамках такого архитектурного подхода мы сталкивались со следующими проблемами:

  • Отсутствие более высокого уровня инкапсуляции. Любой public-класс на classpath-е был доступен всему приложению, независимо от того, предназначался ли он для внешнего использования или представлен как внутренняя реализация.
  • Проблемы с зависимостями. Если зависимость есть на этапе компиляции, но отсутствует в classpath приложения, то оно упадёт во время выполнения, а не при запуске.

Система модулей в Java (JPMS, Java Platform Module System), представленная в JEP 261, дала возможность представить ваше Java-приложение или библиотеку не как набор классов, а как набор модулей, которые:

  • объявляют свои зависимости от других модулей;
  • скрывают пакеты с внутренней реализацией от внешнего использования.

Здесь я приведу краткий пример того, как этим можно пользоваться. Предположим, у нас есть следующая структура библиотеки:

src/
 └─ com.example.lib/
    ├─ com/example/lib/api/LibPublicApi.java
    └─ com/example/lib/internal/InternalClass.java

Мы хотим, чтобы пользователи библиотеки взаимодействовали с ней только через классы из пакета api, а классы из пакета internal, несмотря на то, что они объявлены как public, были недоступны извне. Этого можно добиться, определив библиотеку как отдельный Java-модуль.

Для этого нужно в корне пакета создать файл module-info.java и настроить соответствующим образом:

module com.example.lib {
 exports com.example.lib.api;
}

Наличие файла module-info.java и конструкции module com.example.lib { .... } сообщают о том, что этот пакет и его подпакеты являются Java-модулем. А конструкция exports открывает пакет com.example.lib.api для всех, кто этот модуль использует. Это что касается инкапсуляции. Остальные пакеты, явно не экспортированные, будут недоступны вне этого модуля.

Если же наш модуль нуждается в другом модуле, в конфигурации к нашему необходимо явно об этом сообщить, добавив строку:

requires com.example.lib

Тогда, если мы будем запускать наше приложение, а нужный модуль не будет обнаружен JVM — приложение/библиотека упадёт на старте, а не во время выполнения.

К слову, для стандартной библиотеки Java модульная система оказалась особенно полезной. Разделив JDK на модули, разработчики платформы смогли чётко зафиксировать, какие части являются публичным API, а какие — внутренней реализацией. Это позволило постепенно ограничить доступ к внутренним API и предоставить официальные замены, не опасаясь, что изменения внутренней реализации сломают пользовательский код. Частичная хронология всех этих действий отражена в:

  • JEP 260: Encapsulate Most Internal APIs;
  • JEP 396: Strongly Encapsulate JDK Internals by Default;
  • JEP 403: Strongly Encapsulate JDK Internals.

Ещё одним важным следствием появления системы модулей стал инструмент jlink, предназначенный для создания кастомизируемых runtime-образов Java. Поскольку стандартная библиотека Java была разбита на модули, стало возможным формировать минимальное окружение, включающее только те части платформы, которые действительно требуются конкретному приложению.

jlink анализирует зависимости модулей приложения и собирает самодостаточный runtime, содержащий лишь необходимые модули стандартной библиотеки. Это позволяет значительно уменьшить размер дистрибутива, ускорить запуск приложения.

Предположим, у нас есть модульное приложение com.example.app. Свой runtime для него можно собрать одной командой:

jlink \
  --module-path $JAVA_HOME/jmods:mods \
  --add-modules com.example.app \
  --output app-runtime

В результате будет создан каталог app-runtime, содержащий минимальный runtime-образ Java, включающий только модули, которые нужны нашему com.example.app. Запуск осуществляется с помощью JVM из этого каталога, без использования установленной в системе Java. Так Java-приложение может поставляться вместе с собственным runtime.

Подробнее с системой модулей в Java можно ознакомиться здесь.

G1 – теперь GC по умолчанию. JEP 248

В Java 8 сборщик мусора Parallel GC по умолчанию был ориентирован на максимальную пропускную способность, которая обеспечивала редкие, но длительные Stop-The-World паузы.

С развитием экосистемы Java и переходом к Java 11 требования к приложениям заметно изменились. Выросли объёмы используемой кучи JVM, распространились микросервисная архитектура и контейнеризация. В этих условиях высокая пропускная способность GC перестала быть единственным приоритетом: даже редкие секундные паузы начали восприниматься как недопустимые для онлайн-сервисов несмотря на то, что суммарное время, проведённое в GC, оставалось небольшим.

Именно поэтому, начиная с Java 9, G1 стал сборщиком мусора по умолчанию. В отличие от Parallel GC, G1 сознательно жертвует частью пропускной способности ради более коротких и предсказуемых пауз. Он выполняет сборку мусора чаще и стремится удерживать длительность остановок в заданных пределах. В результате общее время GC может увеличиться, но влияние на время отклика приложения становится более стабильным и управляемым.

Так что если вы при запуске вашего приложения намеренно выставляли JVM флаг -XX:+UseG1GC, после ухода с Java 8 вам этого делать не придётся.

API для Immutable коллекций. JEP 269

Создавать неизменяемые коллекции с константными значениями приходится довольно часто. До Java 9 для этого действия не было удобного API. Приходилось городить что-то такое:

Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
set = Collections.unmodifiableSet(set);

Или:

Set<String> set = 
    Collections.unmodifiableSet(Stream.of("a", "b", "c").collect(toSet()));

Начиная с Java 9 и по сей день у вас есть возможность определять неизменяемые коллекции таким образом:

Set<String> set = Set.of("a", "b", "c");

Вот это уже совсем другое дело!

Важно помнить, что эти методы не поддерживают в качестве аргументов null и дубликаты. Иначе столкнётесь с NullPointerException и IllegalArgumentException соответственно.

Такие методы представлены для всех коллекций и ассоциативных массивов (то есть для Map). При переходе с Java 8 обязательно не забудьте про это!

Компактные строки. JEP 254

До Java 9 внутренним представлением строки являлся массив char, поскольку строки в Java представлены в кодировке UTF-16, где под каждый символ требуется два байта. Однако в JEP 254 говорится, что:

  • зачастую строки занимают существенную часть кучи;
  • большинство строк содержат символы только латинского алфавита.

А каждый символ латинского алфавита можно уместить в один байт. Поэтому ради экономии памяти внутренне представление строк изменили с char[] на byte[] и добавили флаг, который сообщает о том, в какой кодировке строка представлена:

  • ISO-8859-1/Latin-1 (один байт на символ), если все символы в строке в неё умещаются;
  • UTF-16 в противном случае (два байта на символ).

И раз мы заговорили про строки, не могу не рассказать о новых методах в классе String:

  • repeat — создаёт новую строку, повторяя исходную заданное количество раз;
  • strip — удаляет пробельные символы в начале и конце строки;
  • stripLeading — удаляет пробельные символы только в начале строки;
  • stringTralling — удаляет пробельные символы на конце строки;
  • isBlank — проверяет, содержит ли строка что-то кроме пробельных символов;
  • lines — разделяет одну строку на несколько по символам-разделителям.

При переходе на Java 11 не забудьте об этом. В собственном проекте теперь не придётся городить все эти методы или же тащить ради них какую-нибудь стороннюю зависимость.

Вышедшие из состава JDK

Начиная с JDK 11, из стандартного дистрибутива Java были удалены некоторые крупные модули. В частности JavaFX был исключён из состава JDK и переведён в отдельный проект OpenJFX, который теперь распространяется и развивается независимо.

Также в рамках JEP 320 из JDK были удалены модули Java EE и CORBA, поскольку они устарели и фактически не развивались. Это решение позволило упростить платформу Java и сосредоточить её развитие на базовых возможностях, передав Enterprise и UI-решения во внешние экосистемы.

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

Дополнительные поля в @Deprecated. JEP 277

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

До Java 9 эта аннотация не несла вместе с собой какой-либо дополнительной информации. Это порождало некоторую неопределённость в вопросе того, что она значит в конкретной ситуации. Сейчас же, чтобы разработчик имел больше информации об устаревшем API, в Deprecated добавили два параметра:

  • since — строковый параметр, показывающий с какой версии размеченное API признано устаревшим;
  • forRemovalboolean параметр, который является маркером для разработчика, будет API удалено в следующих версиях или нет.

Благодаря этим уточняющим параметрам есть возможность сделать API более понятным для тех, кто им пользуется.

HttpClient вместо HttpUrlConnection. JEP 321 и JEP 110

До Java 11 работа с Http-запросами осуществлялась через HttpUrlConnection. Отправка GET-запроса и вывод результата ответа выглядели следующим образом:

URL url = new URL("https://api.example.com/data");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestProperty("Accept", "application/json");

int status = connection.getResponseCode();

InputStream inputStream;
if (status >= 200 && status < 300) {
    inputStream = connection.getInputStream();
} else {
    inputStream = connection.getErrorStream();
}

BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder responseBody = new StringBuilder();

String line;
while ((line = reader.readLine()) != null) {
    responseBody.append(line);
}

reader.close();
connection.disconnect();

System.out.println(responseBody);

К моменту выпуска Java 11 в JEP 321 появился HttpClient, выставив в противовес HttpUrlConnection следующие преимущества:

  • является неблокирующим, т.е. позволяет выполнять асинхронные запросы;
  • имеет более удобный API;
  • является более высокоуровневым;
  • поддерживает протокол HTTP/2 и WebSocket.

Вот так выглядит простой пример запроса через HttpClient:

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/data"))
        .header("Accept", "application/json")
        .GET()
        .build();

HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());

И, конечно же, sendAsync, позволяющий формировать цепочку асинхронных действий:

client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
      .thenApply(HttpResponse::body)
      .thenAccept(System.out::println)
      .exceptionally(ex -> {
          ex.printStackTrace();
          return null;
      });

Очень важное изменение, о котором стоит помнить, чтобы реализовывать новые фичи, не завязываясь на не самом актуальном HttpUrlConnection.

Files.readString и Files.writeString

Небольшие изменения, связанные с чтением и записью файлов. В Java 11, чтобы прочитать содержимое файла или записать в него что-то, достаточно воспользоваться методами Files.readString и Files.writeString соответственно:

var fileContent = Files.readString(Path.of("file.txt"));
var content = "Hello, File!";
Files.writeString(Path.of("file.txt"), content);

До этого самым лаконичным вариантом был:

String text = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);

К слову, определение пути через Path.of теперь считается более предпочтительным вариантом. Paths.get, который до этого использовался, с Java 11 считается устаревшим.

Filter данных для сериализации. JEP 290

Данное изменение будет полезно тем, кому в приложении необходимо с помощью механизмов Java десериализовывать данные, поступающие извне.

До Java 9, чтобы контролировать, какие классы поступают на десериализацию, приходилось писать собственные классы. К примеру, такие:

public class ObjectInputStreamWithClassCheck extends ObjectInputStream {
  private final static List<String> ALLOWED_CLASSES = Arrays.asList(
        User.class.getName()
  );

  public ObjectInputStreamWithClassCheck(InputStream in) throws .... {
    super(in);
  }

  @Override
  protected Class<?> resolveClass(ObjectStreamClass desc) throws .... {
    if (!ALLOWED_CLASSES.contains(desc.getName())) {
      throw new NotSerializableException(
          "Class is not available for deserialization"
      );
    }

    return super.resolveClass(desc);
  }
}

И десериализовывать через них:

var ois = new ObjectInputStreamWithClassCheck(externalData);
Object obj = ois.readObject();

Сейчас же, благодаря JEP 290, такая возможность появилась на уровне стандартной библиотеки. Для того, чтобы указать, какие объекты доступны для десериализации, достаточно воспользоваться фильтром ObjectInputFilter:

ObjectInputFilter myFilter = 
                  ObjectInputFilter.Config.createFilter("java.util.Date;!*");
ObjectInputStream ois = new ObjectInputStream(externalData);
ois.setObjectInputFilter(myFilter);
Object obj = ois.readObject();

Фильтры настраиваются через специальные строки-выражения. В примере выше мы разрешили десериализовывать только объект класса java.util.Date. Если что-то кроме него поступит на десериализацию, мы столкнёмся с исключением InvalidClassException.

К слову, мы выпускали статью о том, к чему может привести небезопасная десериализация. Рекомендую к прочтению.

Завершение

Статья подходит к концу. Безусловно, всё охватить не получилось, но мы постарались осветить то, с чем разработчик может столкнуться в первую очередь. Украдкой хотелось бы всё же упомянуть про появление JShell и возможность командой java сразу скомпилировать и запустить одиночный файл. Ну и, конечно, не стоит забывать про то, что 8-ая версия была последней, которая имела префикс 1.*: начиная с 9-ой, версии Java представляют собой просто целое число.

А здесь мы будем с вами прощаться! В дальнейшем мы продолжим эту рубрику и расскажем вам про переход на следующую LTS-версию — Java 17. Так что, если вам было интересно, подписывайтесь на наш блог! До скорых встреч!

Последние статьи:

Опрос:

book gost

Дарим
электронную книгу
за подписку!

Популярные статьи по теме


Комментарии (0)

Следующие комментарии next comments
close comment form