Java 17, 21 и 25: отличия версий и долгосрочная поддержка LTS
Автор: Казачкин Даниил Михайлович · Обновлено
Java 17, Java 21 и Java 25 — последовательные поколения одной платформы. Они различаются доступным синтаксисом, стандартными API и поведением JVM. Пометка LTS относится к длительности сопровождения выбранной поставки JDK. После урока вы сможете объяснить эти различия, проверить совместимость примера и обосновать выбор версии для проекта.
Для практики пригодятся [устройство JDK и JVM](/lessons/java/java-for-developers/java-developer-01), [настройка среды](/lessons/java/java-for-developers/java-developer-02) и знакомство с коллекциями. Все примеры ниже самостоятельные, с указанной минимальной версией. Preview-флаги для них не нужны. Сведения о поддержке проверены 23 сентября 2026 года.
Что означает номер версии
Java SE задаёт платформу: язык, виртуальную машину и стандартные API. OpenJDK — открытый проект разработки JDK; Oracle JDK, Eclipse Temurin и другие дистрибутивы поставляют конкретные сборки. При установке выбирают и поколение Java, и поставщика, и ОС с архитектурой. Роль JDK отличается от роли IDE: новое окно редактора само по себе не обновляет компилятор проекта.
В обозначении вроде 21.0.8 число 21 — feature release, а последующие числа обозначают обновления этой ветки. Это пример формата, а не рекомендация устанавливать именно такой патч. Переход 17 → 21 меняет поколение платформы; установка очередного исправления для 21 оставляет проект в той же ветке. Даже LTS нужно регулярно обновлять внутри выбранной версии.
Новые feature-релизы Java выходят раз в полгода. LTS выбираются из этого ряда по политике сопровождения поставщика; у Oracle начиная с Java 17 интервал между такими выпусками — два года. Промежуточные версии тоже содержат законченные возможности. Поэтому сравнение 21 и 25 включает изменения Java 22, 23 и 24, а не только новинки непосредственно 25.
LTS: длительная поддержка и её границы
LTS — Long-Term Support, то есть долгосрочная поддержка. Поставщик сопровождает выбранную ветку дольше обычного релизного цикла: выпускает исправления безопасности и ошибок в соответствии со своей политикой. Это удобно, когда обновлять поколение Java каждые полгода слишком дорого.
| Выпуск | Первый выпуск | Статус в дорожной карте Oracle |
|---|---|---|
| Java 17 | Сентябрь 2021 | LTS |
| Java 21 | Сентябрь 2023 | LTS |
| Java 25 | Сентябрь 2025 | LTS |
LTS не является режимом компилятора, гарантией отсутствия ошибок или обещанием вечных бесплатных обновлений. Поддержка определяется сочетанием дистрибутив + версия + условия поставщика. Не переносите сроки Oracle на Temurin или другого поставщика. Наличие скачиваемого архива также не доказывает, что эта ветка продолжает получать исправления.
Срок технической поддержки и лицензия на конкретную сборку — отдельные вопросы. Перед выбором для организации проверьте оба на сайте поставщика. В этом треке Java 25 выбрана как учебная база; слово LTS не означает «самая новая из всех выпущенных Java». Поддержка Oracle Java SE.
Карта основных отличий
В таблице «стабильно» означает, что возможность доступна без preview-флагов. Это обзор заметных изменений для разработчика, а не полный список JEP — предложений по развитию JDK.
| Возможность | Java 17 | Java 21 | Java 25 |
|---|---|---|---|
| Records и pattern matching для instanceof | Стабильно с 16 | Стабильно | Стабильно |
| Sealed-классы и интерфейсы | Стабильно с 17 | Стабильно | Стабильно |
| Record patterns и pattern matching для switch | Нет стабильного варианта | Стабильно с 21 | Стабильно |
| Virtual threads — виртуальные потоки | Нет | Стабильно с 21 | Стабильно; улучшено взаимодействие с synchronized |
| Sequenced Collections | Нет общего нового контракта | Стабильно с 21 | Стабильно |
| Foreign Function & Memory API | Нет стабильного API | Preview | Стабильно с 22 |
| Stream Gatherers | Нет | Нет | Стабильно с 24 |
| Scoped Values | Нет | Preview | Стабильно с 25 |
| Компактные исходники, instance main, module imports, гибкие тела конструкторов | Нет | Нет стабильного набора | Стабильно с 25 |
| Structured Concurrency | Нет | Preview | Всё ещё preview |
Важно различать «есть в Java 17» и «впервые появилось в Java 17». Records стали стабильными в 16, выражения switch — в 14, текстовые блоки — в 15. Все они уже входят в базу 17. Обычный switch по числам или строкам также не следует путать с более новым сопоставлением типов и record-шаблонов. Изменения языка по версиям.
Java 17: records, sealed и проверка типа
Record удобно описывает набор данных: компилятор создаёт конструктор, методы доступа, equals, hashCode и toString. Это поверхностная неизменяемость полей, а не заморозка всего графа объектов. Sealed-интерфейс ограничивает набор непосредственных реализаций. Вместе они помогают явно описать варианты результата операции.
Сохраните Result17.java. Этот пример работает на Java 17, 21 и 25:
public class Result17 {
sealed interface Result permits Success, Failure {}
record Success(String value) implements Result {}
record Failure(int code) implements Result {}
static String describe(Result result) {
if (result instanceof Success success) {
return "OK: " + success.value();
}
if (result instanceof Failure failure) {
return "ERROR: " + failure.code();
}
return "EMPTY";
}
public static void main(String[] args) {
System.out.println(describe(new Success("saved")));
System.out.println(describe(new Failure(404)));
}
}javac -encoding UTF-8 --release 17 Result17.java
java Result17Результат — две строки: OK: saved и ERROR: 404. После успешного instanceof переменная success уже имеет тип Success: отдельное приведение не требуется. Если передать null, обе проверки дадут false и метод вернёт EMPTY. Следующий вариант сохранит это поведение, но выразит его через switch.
Java 21: шаблоны switch и единый порядок коллекций
Record pattern извлекает компоненты записи прямо в ветке. Компилятор проверяет полноту switch по sealed-иерархии. Если добавить ещё одну разрешённую реализацию Result, этот switch потребуется пересмотреть. Ветка null задаётся отдельно: полнота по типам не делает null обычным объектом одной из реализаций.
Файл Result21.java, Java 21 или 25:
import java.util.List;
public class Result21 {
sealed interface Result permits Success, Failure {}
record Success(String value) implements Result {}
record Failure(int code) implements Result {}
static String describe(Result result) {
return switch (result) {
case Success(var value) -> "OK: " + value;
case Failure(var code) -> "ERROR: " + code;
case null -> "EMPTY";
};
}
public static void main(String[] args) {
List<Result> results = List.of(new Success("saved"), new Failure(404));
System.out.println(describe(results.getFirst()));
System.out.println(describe(results.getLast()));
System.out.println(describe(null));
}
}javac -encoding UTF-8 --release 21 Result21.java
java Result21Ожидаются строки OK: saved, ERROR: 404, EMPTY. Здесь одновременно видно изменение стандартной библиотеки: List получил getFirst и getLast через SequencedCollection. У нового семейства есть также reversed для представления в обратном порядке. Это не означает, что HashSet приобрёл гарантированный порядок, а List.of стал изменяемым. Вызов getFirst у пустой коллекции выбросит NoSuchElementException. SequencedCollection.
Java 21: для чего нужны виртуальные потоки
Виртуальные потоки позволяют обслуживать много задач, которые значительную часть времени ждут ввода-вывода. JVM распределяет их выполнение по потокам ОС. Они не ускоряют арифметику и не увеличивают число процессорных ядер. Ограничения внешних ресурсов, например числа соединений с БД, тоже сохраняются.
Файл Threads21.java, Java 21 или 25. Искусственная задержка показывает ожидание; это демонстрация API, а не тест производительности:
import java.util.concurrent.Executors;
public class Threads21 {
public static void main(String[] args) throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var result = executor.submit(() -> {
Thread.sleep(20);
return "virtual: " + Thread.currentThread().isVirtual();
});
System.out.println(result.get());
}
}
}javac -encoding UTF-8 --release 21 Threads21.java
java Threads21Вывод — virtual: true. Executor создаёт виртуальный поток на задачу; get дожидается результата, а try-with-resources закрывает executor. Это не фиксированный пул переиспользуемых виртуальных потоков. Виртуальные потоки в Java 21.
Есть существенная разница реализаций: в JDK 21 блокировка внутри synchronized могла удерживать поток ОС — это называют pinning. Начиная с JDK 24 данный источник pinning устранён, что относится и к JDK 25. При этом возможны другие случаи, связанные с нативным кодом; обещать отсутствие любых ограничений неверно. Изменения JDK 24, Виртуальные потоки в Java 25.
Java 25: что стало проще и что пришло из промежуточных релизов
Компактные исходные файлы позволяют начать с instance main без явного объявления класса. Сохраните Hello25.java; нужен JDK 25:
void main() {
IO.println("Hello, Java 25");
}javac -encoding UTF-8 --release 25 Hello25.java
java Hello25Вывод — Hello, Java 25. Метод main здесь экземплярный, а IO доступен в компактном исходнике без ручного импорта. Привычная форма public static void main(String[] args) по-прежнему работает; существующие приложения переписывать не требуется.
Ещё две стабильные возможности 25: import module java.base; сокращает импорт публичных типов из экспортируемых пакетов модуля, а гибкие тела конструкторов разрешают часть проверок и вычислений перед super(...) или this(...). Ограничения доступа к строящемуся объекту при этом сохраняются. Module import не скачивает зависимости, как пакетный менеджер, и не требует переводить простой пример в модульный проект.
При переходе с 21 также доступны Stream Gatherers, стабильные с 24: они расширяют промежуточные операции Stream, например группируют элементы в окна. Это дополнение к map/filter/collect, уже знакомым из [урока о Stream](/lessons/java/java-for-developers/java-developer-07). Gatherers.
Foreign Function & Memory API, стабильный с 22, предназначен для работы с нативными функциями и памятью вне обычной Java-кучи. Он полезен при интеграции с нативными библиотеками, но для привычной обработки списков или HTTP-запросов не обязателен. Foreign Function & Memory API.
Java 25: контекст с ограниченным временем жизни
ScopedValue стал стабильным в 25. Он позволяет передать контекст вниз по цепочке вызовов без дополнительного параметра каждого метода. Привязка действует во время выполнения определённого блока и снимается при выходе из него, включая выход по исключению. Значение не становится глобальной изменяемой переменной.
Файл Context25.java, JDK 25:
public class Context25 {
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
static void writeLog() {
System.out.println("request: " + REQUEST_ID.get());
}
public static void main(String[] args) {
ScopedValue.where(REQUEST_ID, "lesson-42").run(Context25::writeLog);
System.out.println("bound after: " + REQUEST_ID.isBound());
}
}javac -encoding UTF-8 --release 25 Context25.java
java Context25Результат — request: lesson-42, затем bound after: false. Вызов get вне привязки выбросит NoSuchElementException. Пример выполняется в одном потоке: произвольно созданные дочерние потоки не получают такой контекст автоматически. ScopedValue удобен для передачи контекста запроса, но не заменяет все случаи использования ThreadLocal и не делает переданный изменяемый объект потокобезопасным. ScopedValue в Java 25.
Stable, preview и incubator — разные статусы
LTS описывает поддержку релиза, а статус возможности — зрелость конкретного синтаксиса или API. Поэтому в одном LTS-выпуске соседствуют стабильные и предварительные возможности.
- Stable / final: возможность включена в обычную платформу без preview-флагов.
- Preview: возможность предназначена для оценки и обратной связи; в следующем релизе она может измениться или исчезнуть. Для сборки нужен
javac --enable-preview --release 25 Example.java, для запуска —java --enable-preview Exampleна соответствующей версии JDK. Эти команды — шаблон для отдельного preview-примера, а не для программ выше. - Incubator: экспериментальный API поставляется в отдельном модуле, обычно jdk.incubator.*; его явно подключают через --add-modules. Это иной механизм, чем preview языка.
Конкретный пример — Vector API, который в Java 25 остаётся incubator-модулем jdk.incubator.vector. Он предназначен для векторных вычислений; это не класс java.util.Vector из коллекций. Модуль Vector API.
В Java 25 Structured Concurrency остаётся preview, хотя виртуальные потоки и ScopedValue уже стабильны. Примитивные типы в patterns, instanceof и switch тоже ещё preview. Нельзя включить preview 21 с помощью JDK 25: компилятор поддерживает preview своего выпуска, а не произвольные исторические варианты. StructuredTaskScope, Параметры javac.
Компилятор, целевая версия и runtime
В проекте есть три отдельных выбора: каким JDK собираем, какую платформу указываем через --release и какой JVM запускаем результат. Начните с javac -version и java -version в том же окружении, где выполняете команды. IDE, сборщик, CI и контейнер могут выбирать разные JDK.
JDK 25 умеет собирать приведённый Result17.java с --release 17: компилятор проверит синтаксис и доступные API Java 17 и создаст подходящий байткод. Но Result21.java с таким флагом не скомпилируется: новые конструкции и List.getFirst нельзя «понизить» одним переключателем. Зависимости проекта тоже должны поддерживать целевую JVM — --release не переписывает чужие JAR.
Байткод Java 25 нельзя просто запустить на JVM 17: возникнет UnsupportedClassVersionError. Старые приложения часто работают на новой JVM, однако удалённые API, сильная инкапсуляция внутренних классов JDK, параметры запуска и изменения поведения библиотек требуют проверки. Не обещайте совместимость только на основании большего номера версии.
Как выбрать версию и перейти на неё
Для этого курса используйте обновляемую сборку JDK 25: она запускает все стабильные примеры трека. Для существующего проекта начните с его требований. Java 17 может оставаться ограничением инфраструктуры, Java 21 — поддерживаемой базой зависимостей, а Java 25 — кандидатом на обновление после проверки инструментов. Пометка LTS сама по себе не выбирает за вас дистрибутив и график перехода.
Переход удобнее разделить на проверяемые шаги:
- Зафиксируйте версии JDK в разработке, сборке и production, целевой --release и поставщика обновлений.
- Проверьте совместимость фреймворка, Maven/Gradle, плагинов, annotation processors, драйверов и агентов мониторинга с выбранным JDK.
- Запустите существующий артефакт и тесты на новой JVM; затем отдельно пересоберите проект и повторите проверки. Не меняйте заодно весь синтаксис.
- Проверьте интеграции, время запуска, память и задержки под характерной нагрузкой. Подготовьте возможность возврата прежнего артефакта и runtime.
- После подтверждения совместимости меняйте целевую версию и осознанно вводите новые возможности.
Это рабочая последовательность, а не гарантия беспроблемного обновления. Конкретные ограничения перечисляет Подготовка миграции JDK.
Практика: предскажите результат до запуска
Сначала запустите пять файлов на JDK 25 с указанными для каждого --release. Сверьте вывод с уроком. Затем, если установлены JDK 17 и 21, используйте явные пути к их java и javac, чтобы PATH не скрывал выбранную версию. Держите результаты сборки разных экспериментов в отдельных каталогах: после неудачной компиляции может остаться старый class-файл.
- Соберите Result17.java компилятором 25 с --release 17. Запустится ли результат на JVM 17 и 21?
- Попробуйте собрать Result21.java с --release 17. Достаточно ли заменить getFirst на get(0)?
- Соберите Hello25.java для 25 и попробуйте запустить его на JVM 21. Поможет ли --enable-preview?
- Команда утверждает: «У нас LTS 25, значит Structured Concurrency уже стабильна, а установленные патчи можно не обновлять». Какие две ошибки вы видите?
Разбор: в первом случае программа работает на обеих JVM. Во втором замена метода не уберёт несовместимый синтаксис switch и record patterns. В третьем JVM 21 не поддерживает class-файл 25; preview-флаг не обновляет виртуальную машину. В четвёртом перепутаны поддержка ветки, зрелость API и установка исправлений: Structured Concurrency остаётся preview в 25, а LTS требует получения свежих обновлений выбранной ветки.
Проверка понимания: вы собираете сервис JDK 25, но запускаете на Java 17. Какой --release нужен, и почему одной настройки компилятора недостаточно? Ответ: 17; дополнительно нужны совместимые библиотеки, инструменты и реальная проверка на целевом runtime.
Частые вопросы
Нужно ли сначала изучить Java 17, затем 21 и только потом 25?
Нет: основы языка общие. Можно учиться на JDK 25, отмечая минимальные версии используемых возможностей. Для работы с существующим проектом ориентируйтесь на его целевую платформу: пример со стабильным API Java 25 не обязан компилироваться на 17.
LTS означает, что Java бесплатна и поддерживается одинаково у всех?
Нет. LTS обозначает длительное сопровождение конкретной ветки у поставщика. Доступность обновлений, сроки и условия использования сборки проверяются отдельно; политика одной компании не определяет условия всех дистрибутивов OpenJDK.