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

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

![1337_hitch_guide_to_the_lts_8_11_ru/image1.png](https://import.viva64.com/docx/blog/1337_hitch_guide_to_the_lts_8_11_ru/image1.png)

## Введение

Когда выходила Java 25, мы выпустили об этом [статью](https://pvs-studio.ru/ru/blog/posts/java/1284/)\. Её автор рассмотрел основные изменения и порассуждал об их удобстве и крутости для разработчика\. После публикации один из читателей написал нам и выразил желание увидеть материал о том, с чем сталкивается разработчик при переходе с одной LTS\-версии Java на следующую, начиная с Java 8\.

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

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

<details>
   <summary>LTS?</summary>

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


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

Лично мне первыми на ум приходят [Stream](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html) API, [лямбды](https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html) и [ссылки на методы/конструкторы](https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html), которые превращают вот такие конструкции:

```cpp
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));
}
```

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

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

Не стоит забывать и множество [функциональных интерфейсов](https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html), благодаря которым появление вышеупомянутых конструкций стало возможным\. Среди них:

* [Predicate](https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html);
* [Function](https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html);
* [Consumer](https://docs.oracle.com/javase/8/docs/api/java/util/function/Consumer.html);
* [Supplier](https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html);
* и т\.д\.

И именно в Java 8 появились реализации [default](https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html) методов в интерфейсах\.

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

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

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

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

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

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

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

[JEP 286](https://openjdk.org/jeps/286) предоставил нам такую возможность\. Теперь вместо какого\-нибудь абстрактного `StateDatabaseHelperContainerMapMessage` мы можем использовать лаконичный `var`:

```cpp
var stateDbHelper = new StateDatabaseHelperContainerMapMessage();
```

P\.S\. За enterprise название спасибо [ему](https://projects.haykranen.nl/java/)\.

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

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

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

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

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

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

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

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

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

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

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

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

```cpp
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](https://openjdk.org/jeps/260): Encapsulate Most Internal APIs;
* [JEP 396](https://openjdk.org/jeps/396): Strongly Encapsulate JDK Internals by Default;
* [JEP 403](https://openjdk.org/jeps/403): Strongly Encapsulate JDK Internals\.

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

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

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

```cpp
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 можно ознакомиться [здесь](https://openjdk.org/projects/jigsaw/spec/sotms/)\.

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

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

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

Именно поэтому, начиная с Java 9, [G1 стал сборщиком мусора по умолчанию](https://openjdk.org/jeps/248)\. В отличие от Parallel GC, G1 сознательно жертвует частью пропускной способности ради более коротких и предсказуемых пауз\. Он выполняет сборку мусора чаще и стремится удерживать длительность остановок в заданных пределах\. В результате общее время GC может увеличиться, но влияние на время отклика приложения становится более стабильным и управляемым\.

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

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

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

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

Или:

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

Начиная с Java 9 и по сей день у вас [есть возможность](https://openjdk.org/jeps/269) определять неизменяемые коллекции таким образом:

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

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

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

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

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

До Java 9 внутренним представлением строки являлся массив `char`, поскольку строки в Java представлены в кодировке UTF\-16, где под каждый символ требуется два байта\. Однако в [JEP 254](https://openjdk.org/jeps/254) говорится, что:

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

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

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

И раз мы заговорили про строки, не могу не рассказать о новых методах в классе [`String`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html):

* [`repeat`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#repeat(int)) — создаёт новую строку, повторяя исходную заданное количество раз;
* [`strip`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#strip()) — удаляет пробельные символы в начале и конце строки;
* [`stripLeading`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#stripLeading()) — удаляет пробельные символы только в начале строки;
* [`stringTralling`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#stripTrailing()) — удаляет пробельные символы на конце строки;
* [`isBlank`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#isBlank()) — проверяет, содержит ли строка что\-то кроме пробельных символов;
* [`lines`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#lines()) — разделяет одну строку на несколько по символам\-разделителям\.

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

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

Начиная с JDK 11, из стандартного дистрибутива Java были удалены некоторые крупные модули\. В частности JavaFX [был исключён](https://docs.oracle.com/en/java/javase/24/migrate/significant-changes-jdk-11.html) из состава JDK и переведён в отдельный проект [OpenJFX](https://openjfx.io/), который теперь распространяется и развивается независимо\. 

Также в рамках [JEP 320](https://openjdk.org/jeps/320) из JDK были удалены модули Java EE и CORBA, поскольку они устарели и фактически не развивались\. Это решение позволило упростить платформу Java и сосредоточить её развитие на базовых возможностях, передав Enterprise и UI\-решения во внешние экосистемы\.

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

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

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

До Java 9 эта аннотация не несла вместе с собой какой\-либо дополнительной информации\. Это порождало некоторую неопределённость в вопросе того, что она значит в конкретной ситуации\. Сейчас же, чтобы разработчик имел больше информации об устаревшем API, в [`Deprecated`](https://docs.oracle.com/javase/9/docs/api/java/lang/Deprecated.html) [добавили](https://openjdk.org/jeps/277) два параметра:

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

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

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

До Java 11 работа с Http\-запросами осуществлялась через [`HttpUrlConnection`](https://javadoc.scijava.org/Java9/java/net/HttpURLConnection.html)\. Отправка GET\-запроса и вывод результата ответа выглядели следующим образом:

```cpp
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](https://openjdk.org/jeps/321) появился [`HttpClient`](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html), выставив в противовес [`HttpUrlConnection`](https://javadoc.scijava.org/Java9/java/net/HttpURLConnection.html) следующие преимущества:

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

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

```cpp
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`, позволяющий формировать цепочку асинхронных действий:

```cpp
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` соответственно:

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

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

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

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

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

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

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

```cpp
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);
  }
}
```

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

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

Сейчас же, благодаря [JEP 290](https://openjdk.org/jeps/290), такая возможность появилась на уровне стандартной библиотеки\. Для того, чтобы указать, какие объекты доступны для десериализации, достаточно воспользоваться фильтром [`ObjectInputFilter`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/ObjectInputFilter.html):

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

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

К слову, мы [выпускали статью](https://pvs-studio.ru/ru/blog/posts/java/1296/) о том, к чему может привести небезопасная десериализация\. Рекомендую к прочтению\.

## Завершение

Статья подходит к концу\. Безусловно, всё охватить не получилось, но мы постарались осветить то, с чем разработчик может столкнуться в первую очередь\. Украдкой хотелось бы всё же упомянуть про появление [JShell](https://openjdk.org/jeps/222) и [возможность](https://openjdk.org/jeps/330) командой `java` сразу скомпилировать и запустить одиночный файл\. Ну и, конечно, не стоит забывать про то, что 8\-ая версия была последней, которая имела префикс 1\.\*: начиная с 9\-ой, версии Java представляют собой просто целое число\.

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