Java 25: Что нового, простыми словами и простым кодом
Java 25 - новый LTS релиз

Java 25: Что нового, простыми словами и простым кодом


Для начала хочу вас предупредить, что данная статья ориентирована для разработчиков java, будь вы эксперт в этом языке, любитель или только начинаете познавать java и то как она работает. Если у вас нет опыта программирования или вы совсем начинающий, то, скорее всего, вам будет сложно понять все, о чем будет идти речь. Но если вы уже имеете какое-то понимание о том, как работают языки программирования и в частности java, или это предупреждение вас не останавливает, то давайте начнем


О java 25

16 сентября 2025 года вышел 25 релиз jdk. Это обновление стало очень важным, хотя бы потому, что это пятый LTS релиз java, наряду с 8, 11, 17 и 21 релизами. Для тех, кто не знает, LTS означает Long-Term Support, и такие релизы поддерживаются многие годы после их выхода. Именно такими версиями стараются пользоваться в production - средах, ибо поддержка обновлений очень важна (в случае обнаружения уязвимостей, проблем с памятью и других бед, а также просто каких-либо оптимизаций и улучшений), а обновление на более новые полноценные релизы - очень дорого, сложно и не быстро, так как хоть java и поддерживает принцип обратной совместимости (код на java 8 запустится и на jvm 25 версии jdk), но новый релиз может значительно изменить функциональность старого кода, приводя к непредсказуемым результатам, и как правило приходится вносить много изменений в старый код.

У сообщества java в основном положительные отзывы о java 25, большинство пользователей довольны нововведениями, и единственная популярная причина, по которой эту версию не широко используют в бизнесах на данный момент - это её свежесть, что несет за собой риски непредвиденных проблем, и затратность обновления на новую версию.

Заканчивая вступление, теперь можно поговорить и про сами нововведения


Что нового

JEP 507: Примитивы в паттернах, instanceof и switch

Это очень интересное нововведение, правда пока только preview. Теперь, в java можно передавать примитивы в instanceof, switch. Это очень удобно, и теперь можно пользоваться такими конструкциями:

Рисунок 1 - Примитив в switch и instanceof

Я всегда рад такого рода изменениям, с ними использование базовых фичей ощущается более свободным, в языке появляется больше удобства и возможностей

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

Рисунок 2 - Пример модуля в Java

Признаться честно, познакомился я с данной вещью только в ходе написания данной статьи. В production среде она используется довольно редко, ведь у нас уже есть сборщики - maven и gradle, с помощью которых при необходимости можно сделать многомодульный проект. Java 25 добавляет возможность импортировать модули внутри класса конструкцией import module. Сделано это в основном по единственной причине - импортировать модуль java.base, который неявно встроен во все модули, и содержит в себе много важных пакетов, таких как java.util, java.io, java.lang и т.д. По сути, теперь модуль java.base - это просто набор импортов, нужный, чтобы не заполнять код кучей импортов одних и тех же библиотек.

package untitled;
import module java.base;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> map = new IdentityHashMap<>();
    }
}

JEP 512: компактные исходные файлы и экземплярный main метод

Нашумевший JEP, который бурно обсуждают в сообществе java. Одни говорят - круто, меньше шаблонного кода, не надо больше ради одного принта прописывать класс и метод, от которого у новичков волосы дыбом. Другие же плачут - java превращается в python, прощай модульность, структурированность и иерархичность, а новички не будут учиться важным и более сложным концепциям на старте, и вырастут людьми, которые пишут весь код продукта в одном методе на 800 строк. Все, что делает этот JEP, это дает возможность вместо этого:

public class Main{
    public static void main(String[] args){
        System.out.println("Hello world");
    }
}

делать это:

void main(){
    IO.println("Hello world")
}

Я, честно говоря, не знаю, на чьей я стороне. Скорее я склоняюсь ко второму варианту - возможность писать код не в классе ломает принципы java. Такой исходный файл нельзя импортировать, и модификаторы доступа в нем бесполезны. так как даже публичный метод из него не достать. Но я не считаю это чем-то фатальным для java, а скорее просто более быстрый способ накидать небольшой скрипт. Пользоваться этим никого не заставляют)

JEP 513: Гибкие конструкторы

Предположим, есть класс:

public class CoolerClazz {
    public int num;
    public CoolerClazz(int num){
        this.num = num;
    }
}

И есть наследник Clazz. Раньше, так сделать было нельзя:

public class Clazz extends CoolerClazz {
    public int num2;
    public Clazz(int num, int num2){
        if (num < 0){
            throw new RuntimeException();
        }
        super(num);
    }
}

Код бы не скомпилировался, так как конструктор суперкласса обязательно должен был идти первым. Это приводит к лишним действиям - зачем конструировать суперкласс, если потом валидация внутри конструктора провалится и объект не создастся. В java 25 это правило сняли, и теперь можно сначала сделать валидацию значений, а потом конструировать суперкласс.

JEP 505: Структурированная конкурентность

Этот JEP относится уже к изменениям в библиотеке, и тут уже все становится интереснее. Это пятое превью такого класса, как StructuredTaskScope. На данный момент использовать его не рекомендуется, так как это превью, и в будущем его могут удалить. Его представили как альтернативу неструктурированному ExecutorService, который бросает задачи на выполнение в пул потоков и дальше не смотрит, что там происходит и как. В отличие от ExecutorService, у StructuredTaskScope есть возможность определить Joiner, который будет управлять тем, как этот scope будет реагировать на задачи. Вот пример кода ExecutorService:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        Future priceFuture = executor.submit(() -> fetchPrice(id));
        Future inventoryFuture = executor.submit(() -> fetchInventory(id));
        
        try {
            return new Product(priceFuture.get(), inventoryFuture.get());
        } catch (ExecutionException e) {
            priceFuture.cancel(true);
            inventoryFuture.cancel(true);
            throw e;
        }
}

Здесь мы даём экзекутору 2 задачи, fetchPrice и fetchInventory. Заметим, что мы должны сами обработать ошибки экзекутора, и если что-то пошло не так. мы вручную убиваем все задачи. А теперь посмотрим на тот же функционал с StructuredTaskScope:

try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) {
        
        Subtask priceTask = scope.fork(() -> fetchPrice(id));
        Subtask inventoryTask = scope.fork(() -> fetchInventory(id));

        scope.join(); 

        return new Product(priceTask.get(), inventoryTask.get());
    }

Здесь нам не нужно обрабатывать ошибки и закрывать задачи, ведь у нас есть joiner, который следит за тем, что все задачи должны успешно выполниться, и если что-то падает, то он сам закроет все задачи, не дожидаясь их выполнения. В StructuredTaskScope есть много Joiner'ов: allSuccessfulOrThrow, anySuccessfulOrThrow, awaitAll и другие, а также есть allUntil, который позволяет прописать свой собственный joiner, если есть такая нужда. Удобно, полезно, но могу подметить, что для ExecutorService есть класс Executors, с помощью которого можно создать thread pool на свой вкус и цвет - fixed thread pool, scheduled thread pool, work stealing pool, virtual thread per task executor, что пожелаете. В StructuredTaskScope такого нет, если хотите конкретный thread pool с нужными вам настройками - делайте его сами через ThreadFactory, и в целом идея StructuredTaskScope - уйти от ручных thread pool'ов и сделать всё автоконфигурируемыми виртуальными тредами.

JEP 506: Scoped Values

Альтернатива ThreadLocal значениям. Используется в случае, если у нас есть цепочка вызовов, и мы не хотим пробрасывать одно значение из метода в метод. Раньше эту задачу выполнял ThreadLocal, но у него был ряд проблем, такие как отсутствие определенного жизненного цикла (если вручную не удалить переменную после того, как её использовали, она продолжит жить, засоряя память), мутабельность и проблемы с использованием в многопоточной среде. ScopedValue - более надежная альтернатива, предназначенная именно для этого случая.

Её использование довольно просто: Создается ScopedValue, в данном примере в отдельном классе:

public class SecurityContext {
    public static final ScopedValue USER_ID = ScopedValue.newInstance();
}

Далее, в методе, который использует это значение, используем USER_ID.get() для того, чтобы достать значение в этом scope:

public class BillingService {
    public void processPayment() {
        String currentId = SecurityContext.USER_ID.get(); 
        System.out.println("Обработка платежа для: " + currentId);
    }
}

Наконец, в вызывающем классе мы вызываем метод с помощью ScopedValue.with().run(), где мы и задаем значение ScopedValue текущего скоупа:

public class Controller {
    private final BillingService service = new BillingService();

    public void handleRequest() {
        ScopedValue.where(SecurityContext.USER_ID, "user_99")
                   .run(() -> service.processPayment());
    }
}

И всё, нам не нужно больше ничего делать и пробрасывать значение в цепочку методов. В данном примере я использовал только один вызов, но когда начинается большая цепь, становится видно, насколько это удобно, особенно если пробрасывать надо очень много значений.

JEP 502: Stable Value

Не сразу понял, что делает эта вещь, однако, как оказалось, в ней ничего сложного - это ленивое final поле. Final поля вычисляются обязательно на моменте старта приложения если это static и на момент создания объекта если поле обычное, а не final поля изменяемые, что делает их медленнее, т.к. процессор не может допустить себе некоторых оптимизаций, а также опаснее, иммутабельность всегда лучше, особенно в многопоточной среде. StableValue - одновременно иммутабелен и вычисляется только при необходимости, что в некоторых случаях предотвращает простои при инициализации объекта или запуске приложения. Для этой вещи код я вам не покажу, поищите сами, вещь очень легкая :)
Также, хочу заметить, что это тоже preview, и может быть убрана в будущем

JEP 508: Vector API

Это очень умная вещь, находящаяся еще в инкубационной стадии, которая позволяет с помощью широких регистров процессора сложить сразу много чисел за один такт, значительно увеличивая производительность. Вот как это работает:

static final VectorSpecies SPECIES = FloatVector.SPECIES_PREFERRED;

void vectorComputation(float[] a, float[] b, float[] c) {
    int i = 0;
    int upperBound = SPECIES.loopBound(a.length);
    for (; i < upperBound; i += SPECIES.length()) {
        // FloatVector va, vb, vc;
        var va = FloatVector.fromArray(SPECIES, a, i);
        var vb = FloatVector.fromArray(SPECIES, b, i);
        var vc = va.mul(va)
                   .add(vb.mul(vb))
                   .neg();
        vc.intoArray(c, i);
    }
}

Это фрагмент кода со страницы openjdk, который складывает 2 одинаковых по размеру массива. Что здесь происходит:

  1. Создается float вектор с шириной вектора равной той, что предпочтительна в системе
  2. Устанавливается лимит по объектам, которые можно положить в регистр. Это нужно, чтобы массивы разбивались на вектора, длина которых кратна длине массива.
  3. Создается итератор, который за каждую итерацию берет SPECIES.length значений из массива a и b, делает из них вектора, за одну операцию берет значение из a в квадрате, добавляет значение из b в квадрате, умножает на -1 и кладет в массив c

Таким образом, количество итераций для сложения 2 массивов сокращается во много раз, что очень сильно ускоряет многие алгоритмы.

JEP 470: PEM-кодировка криптографических объектов

PEM кодировка нужна для удобной передачи публичных ключей и сертификатов в обычном текстовом формате. Она кодирует ключ в base64, добавляет строку -----BEGIN CERTIFICATE----- и -----END CERTIFICATE----- (в случае сертификата), и в Java 25 создали превью api для PEM формата. Добавили 3 класса - PEMEncoder, PEMDecoder и PEMRecord.

PEMRecord - стандартный класс для хранения PEM объектов, содержащий базовые функции, такие как type(), decode() и leadingData(). PEMEncoder и PEMDecoder нужны для кодирования и декодирования ключей, сертификатов и других криптографических сущностей в PEM формат соответственно. PEMEncoder при кодировании возвращает объект типа DEREncodable, который PEMRecord имплементирует. Эта api убирает нужду в ручной кодировке ключей, сертификатов и подобных вещей без использования сторонних вещей.

JEP 510: Key Derivative Function API

Это финализованная api для упрощения работы с алгоритмами шифрования для создания ключей из криптографического "материала". Она предоставляет набор криптографических алгоритмов и типов нужных нам ключей. Вот небольшой пример использования:

KDF kdf = KDF.getInstance("HKDF-SHA256");

AlgorithmParameterSpec spec = HKDFParameterSpec.ofExtract()
    .addIKM(ikm)
    .addSalt(salt)
    .thenExpand(info, 32);

SecretKey aesKey = kdf.deriveKey("AES", spec);

Сначала мы объявляем алгоритм - HKDF-SHA256, затем делаем данные для ключа, используя соль, ikm - какой-либо исходный материал (как правило какой-либо секретный пароль), и размер ключа. Затем мы получаем ключ типа AES из наших данных.

Производительность

3 JEP-а в 25 java посвящены производительности. В JEP 519 сжатые хедеры объектов сделали не экспериментальной фичей, а JEP 514 и 515 посвящены Ahead Of Time кешам. В крацие, при запуске приложение формирует кеши, которые будут использоваться при следующих запусках для более быстрого прогрева приложения. Здесь говорить особо не о чем, быстро - значит хорошо, особенно когда ничего не ломается.

Мониторинг

В этом пункте тоже мало того, о чем можно поговорить. 3 JEP-а с оптимизациями и улучшениями JFR. Один момент стоит подметить - в JEP 520 добавили возможность отслеживать выполнение методов в JFR, с настройкой через конфигурацию JFR, без изменений в коде, и с точным отслеживанием времени выполнений, что крайне полезно.       

Итог

В 25 java вышло довольно много JEP'ов, и некоторые из них меня по-настоящему заинтересовали. Примитивы в switch и instanceof - это очень удобное нововведение, switch выражения во многих языках очень неудобные и ограниченные, и java в этом плане не имеет больших проблем, а StructuredTaskScope - это очень интересная вещь, которая может революционировать многопоточное программирование. К сожалению, реальность такова, что скорее всего в компаниях еще очень не скоро воспользуются этими вещами, так как многие из них пока превью, а даже когда они выйдут из preview - их начнут применять далеко не сразу. Но java на рынке уже многие годы, и уходить она никуда не собирается, поэтому я уверен, дорогие разработчики, что рано или поздно вам удастся эти вещи потрогать на практике если вы того захотите.

Если вы хотите еще подробнее углубиться в нововведения Java 25, почитайте официальные release notes от Oracle
https://www.oracle.com/java/technologies/javase/25-relnote-issues.html

Оценить публикацию