ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

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Сентябрь 2021LTS
Java 21Сентябрь 2023LTS
Java 25Сентябрь 2025LTS

LTS не является режимом компилятора, гарантией отсутствия ошибок или обещанием вечных бесплатных обновлений. Поддержка определяется сочетанием дистрибутив + версия + условия поставщика. Не переносите сроки Oracle на Temurin или другого поставщика. Наличие скачиваемого архива также не доказывает, что эта ветка продолжает получать исправления.

Срок технической поддержки и лицензия на конкретную сборку — отдельные вопросы. Перед выбором для организации проверьте оба на сайте поставщика. В этом треке Java 25 выбрана как учебная база; слово LTS не означает «самая новая из всех выпущенных Java». Поддержка Oracle Java SE.

Карта основных отличий

В таблице «стабильно» означает, что возможность доступна без preview-флагов. Это обзор заметных изменений для разработчика, а не полный список JEP — предложений по развитию JDK.

ВозможностьJava 17Java 21Java 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Нет стабильного APIPreviewСтабильно с 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-выпуске соседствуют стабильные и предварительные возможности.

Конкретный пример — 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 сама по себе не выбирает за вас дистрибутив и график перехода.

Переход удобнее разделить на проверяемые шаги:

  1. Зафиксируйте версии JDK в разработке, сборке и production, целевой --release и поставщика обновлений.
  2. Проверьте совместимость фреймворка, Maven/Gradle, плагинов, annotation processors, драйверов и агентов мониторинга с выбранным JDK.
  3. Запустите существующий артефакт и тесты на новой JVM; затем отдельно пересоберите проект и повторите проверки. Не меняйте заодно весь синтаксис.
  4. Проверьте интеграции, время запуска, память и задержки под характерной нагрузкой. Подготовьте возможность возврата прежнего артефакта и runtime.
  5. После подтверждения совместимости меняйте целевую версию и осознанно вводите новые возможности.

Это рабочая последовательность, а не гарантия беспроблемного обновления. Конкретные ограничения перечисляет Подготовка миграции JDK.

Практика: предскажите результат до запуска

Сначала запустите пять файлов на JDK 25 с указанными для каждого --release. Сверьте вывод с уроком. Затем, если установлены JDK 17 и 21, используйте явные пути к их java и javac, чтобы PATH не скрывал выбранную версию. Держите результаты сборки разных экспериментов в отдельных каталогах: после неудачной компиляции может остаться старый class-файл.

  1. Соберите Result17.java компилятором 25 с --release 17. Запустится ли результат на JVM 17 и 21?
  2. Попробуйте собрать Result21.java с --release 17. Достаточно ли заменить getFirst на get(0)?
  3. Соберите Hello25.java для 25 и попробуйте запустить его на JVM 21. Поможет ли --enable-preview?
  4. Команда утверждает: «У нас 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.

Источники