Превью Java 23: еще удобнее, безопаснее и быстрее

В обзоре Java 23 расскажем, какие новые фичи появляются в релизе, который выходит в сентябре 2024. Вы узнаете, чего коснулись изменения и как начать использовать фичи из предварительной версии.

5К открытий32К показов
Превью Java 23: еще удобнее, безопаснее и быстрее

Выход Java 23 ждем в середине сентября, а список JDK Enhancement Proposal (JEP) уже определён и доступен. Расскажем подробнее. Релиз включает 13 JEP, которые традиционно улучшают Java по следующим направлениям:

  • безопасность,
  • производительность,
  • удобство разработки.

Для начинающих важно, что в релизе есть улучшения специально для них. Эти JEP упрощают изучение Java, чтобы не пугать новичков с первого взгляда:

  • Неявные классы и main-методы экземпляра класса (третья предварительная версия).
  • Импортирование модулей (предварительная версия).

В превью вы узнаете, какие JEP вошли в новую Java и чего коснулись эти изменения.

Как начать использовать фичи в предварительной версии

Чтобы попробовать экспериментальные фичи или фичи в предварительной версии, нужно их явно включить. Вы можете это сделать как через командную строку, так и настроить в интегрированной среде разработки (IDE).

В командной строке включите фичу в предварительной версии одним из следующих способов:

  • Скомпилируйте программу с помощью javac --release 23 --enable-preview Main.java и запустите ее с помощью java --enable-preview Main.
  • При использовании source code launcher запустите программу с java --enable-preview Main.java.
  • При использовании jshell запустите его с помощью jshell --enable-preview.

Повышение безопасности

Scoped Values (третья предварительная версия)

Scoped values впервые появились в JDK 20 в режиме инкубатора. В третьей предварительной версии метод ScopedValue.getWhere будет удалён. Этот метод заменит новый функциональный интерфейс, позволяющий JVM понять, будет ли выброшено исключение.

Scoped values позволяют совместно использовать неизменяемые данные в рамках одного потока и между потоками. Scoped values следует использовать вместо класса ThreadLocal, потому что они уменьшают сложность кода и повышают надёжность многопоточных приложений.

Вот пример того, как два метода фреймворка, выполняющиеся в одном потоке обработки запросов, могут использовать переменную типа ThreadLocal для совместного использования FrameworkContext. Так, не нужно передавать FrameworkContext в качестве аргумента методу, когда фреймворк вызывает пользовательский код, и наоборот: пользовательский код вызывает метод фреймворка.

  1. Фреймворк объявляет переменную типа ThreadLocal CONTEXT (1). 
  2. Когда Framework.serve выполняется в потоке обработки запросов, он записывает подходящий FrameworkContext в переменную типа ThreadLocal (2), а затем вызывает пользовательский код. 
  3. Если и когда пользовательский код вызывает Framework.readKey, этот метод считывает переменную ThreadLocal (3), чтобы получить FrameworkContext потока, обрабатывающего запрос.
			public class Framework {

    private final Application application;

    public Framework(Application app) { this.application = app; }
    
    private final static ThreadLocal CONTEXT 
                       = new ThreadLocal<>();    // (1)

    void serve(Request request, Response response) {
        var context = createContext(request);
        CONTEXT.set(context);                    // (2)
        Application.handle(request, response);
    }

    public PersistedObject readKey(String key) {
        var context = CONTEXT.get();              // (3)
        var db = getDBConnection(context);
        db.readKey(key);
    }

}
		

Переменная типа ThreadLocal служит неким скрытым параметром метода: поток, вызывающий CONTEXT.set в Framework.serve и CONTEXT.get в Framework.readKey, автоматически увидит свою собственную локальную копию переменной CONTEXT. По сути, поле ThreadLocal служит ключом, который используется для поиска значения FrameworkContext для текущего потока.

Но у ThreadLocal есть несколько недостатков:

  1. Все переменные ThreadLocal изменяемые. Метод get() переменной ThreadLocal может быть вызван в любом месте кода, так же, как и метод set().
  2. Если переменная ThreadLocal копии потока определена с помощью метода set(), то значение переменной сохраняется в течение всего времени существования потока или до тех пор, пока код в потоке не вызовет метод remove(). Разработчики часто забывают вызывать метод remove(), поэтому значение переменной хранится дольше, чем нужно.
  3. Дочерние потоки наследуют переменные ThreadLocal родительского потока и обязаны выделять часть памяти для каждой переменной ThreadLocal родительского потока, что очень накладно по памяти. Учитывая, что на практике дочерние потоки редко используют метод set() на унаследованных переменных ThreadLocal.

Эти недостатки усугубляются с появлением виртуальных потоков — их может быть тысячи или даже миллионы. Получается, переменные ThreadLocal только усложняют передачу значения переменной в рамках одного потока без использования явного параметра и накладывают расходы, которых нельзя избежать.

Недостатки ThreadLocal решает ScopedValue.

А вот этот же пример кода, но переписанный с использованием класса ScopedValue:

  1. Фреймворк объявляет переменную CONTEXT типа ScopedValue вместо ThreadLocal (1).
  2. Метод serve() вызывает ScopedValue.runWhere вместо метода set() класса ThreadLocal (2).
			class Framework {

    private final static ScopedValue CONTEXT
                        = ScopedValue.newInstance();    // (1)

    void serve(Request request, Response response) {
        var context = createContext(request);
        ScopedValue.runWhere(CONTEXT, context,          // (2)
                   () -> Application.handle(request, response));
    }
    
    public PersistedObject readKey(String key) {
        var context = CONTEXT.get();                    // (3)
        var db = getDBConnection(context);
        db.readKey(key);
    }
  }
		

Метод runWhere обеспечивает односторонний обмен данными от метода serve() к методу readKey(). Метод runWhere() связывает переданное значение с соответствующим объектом на время выполнения, что позволяет любому методу, вызванному из runWhere(), получить это значение через CONTEXT.get()(3).

Если Framework.serve вызывает пользовательский код, который затем вызывает Framework.readKey, то значение будет таким же, как и в момент вызова runWhere(). Эта связь действует только внутри runWhere(). Попытка получить значение через CONTEXT.get() в Framework.serve после завершения runWhere() приведёт к исключению, так как связь больше не существует.

Таким образом, значение ScopedValue связывается с объектом только на время выполнения метода runWhere(), что освобождает разработчиков от необходимости помнить об очистке значений переменных. Также значение переменной ScopedValue можно только получить методом get() и нельзя изменить, так как у ScopedValue нет метода set(). Так решается проблема с дорогим наследованием: дочерним потокам не нужно копировать значение, которое не изменяется в течение периода жизни.

Подготовка к удалению методов доступа к памяти в sun.misc.Unsafe

Цель создания класса sun.misc.Unsafe — выполнение низкоуровневых операций в JDK, поскольку этот класс содержит методы для доступа к памяти on-heap и off-heap. Эти методы могут помочь увеличить производительность в некоторых специфических сценариях, но только в том случае, если по пути выполняются исчерпывающие проверки на безопасность. Иначе использование этих методов может привести к неожиданному поведению приложения, сбоям JVM или снижению производительности. Многие библиотеки используют sun.misc.Unsafe, но не все выполняют требуемые проверки безопасности.

Для решения этой проблемы введены два запасных API:

  • Variable Handles — для доступа к памяти on-heap,
  • Foreign Function & Memory API — для работы с памятью off-heap.

С помощью этих API стало возможным вывести sun.misc.Unsafe из использования с помощью JEP 471 и удалить этот класс в будущих выпусках.

Рассмотрим небольшой пример миграции. Допустим, что в классе Foo есть целочисленное поле int, над которым мы хотим провести операцию атомарного удвоения. Мы можем сделать это с помощью sun.misc.Unsafe:

			class Foo {

    private static final Unsafe UNSAFE = ...;    // Объект sun.misc.Unsafe

    private static final long X_OFFSET;

    static {
        try {
            X_OFFSET = UNSAFE.objectFieldOffset(Foo.class.getDeclaredField("x"));
        } catch (Exception ex) { throw new AssertionError(ex); }
    }

    private int x;

    public boolean tryToDoubleAtomically() {
        int oldValue = x;
        return UNSAFE.compareAndSwapInt(this, X_OFFSET, oldValue, oldValue * 2);
    }

}
		

А теперь улучшим этот пример с помощью VarHandle API:

			class Foo {

    private static final VarHandle X_VH;

    static {
        try {
            X_VH = MethodHandles.lookup().findVarHandle(Foo.class, "x", int.class);
        } catch (Exception ex) { throw new AssertionError(ex); }
    }

    private int x;

    public boolean tryAtomicallyDoubleX() {
        int oldValue = x;
        return X_VH.compareAndSet(this, oldValue, oldValue * 2);
    }

}
		

Увеличение производительности

С каждым релизом производительность Java увеличивается. Этот релиз не исключение.

Vector API (восьмой инкубатор)

Vector API появилось в JDK 16 и пока остаётся в инкубационном периоде. В восьмом инкубаторе всё без изменений.

Vector API повышает производительность расчётов на массивах однотипных данных, которые компилируются в векторные инструкции во время исполнения приложения.

Vector API зависит от некоторых фич проекта Valhalla, который находится в разработке. Как только эти функциональности станут доступны в предварительной версии, векторное API так же попадёт из инкубатора в предварительную версию.

Вот пример простого скалярного вычисления над элементами массива:

			void scalarComputation(float[] a, float[] b, float[] c) {
   for (int i = 0; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
   }
}
		

Этот же пример, только переписанный с использованием 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);
    }
    for (; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
    }
}
		

Кажется сильно больше и страшнее. Сначала мы получаем предпочтительный вид из FloatVector, оптимальный для текущей архитектуры, и сохраняем его в static final поле, чтобы компилятор рассматривал его как константу для лучшей оптимизации вычислений. Затем основной цикл проходит по массивам с шагом, равным длине вектора (т.е. длине SPECIES), загружает векторы типа float из массивов a и b, выполняет арифметические операции и сохраняет результат в массив c. Остаток элементов после последней итерации обрабатывается обычным скалярным циклом.

Сборщик мусора ZGC по умолчанию — generational

Сборщик мусора Z Garbage Collector (ZGC) появился ещё в Java 21. В Java 23 сборщик мусора без поколений (non-generational) признан устаревшим и будет удалён в следующей версии. Цели этого JEP 474:

  • снизить затраты на поддержку двух сборщиков мусора ZGC: без поколений (non-generational) и generational, 
  • предупредить о дальнейшем фокусе разработки на generational.

Раньше ZGC хранил молодые и старые объекты вместе и собирал все объекты сразу. С созданием generational ZGC хранит молодые и старые объекты в разных областях и чаще собирает молодые объекты, что уменьшает потребление ресурсов CPU и кучи при сборке мусора.

Чтобы включить ZGC generational в командной строке или IDE, передайте следующие аргументы при выполнении программы:

  • -XX:+UseZGC
			$ java -XX:+UseZGC …
		

Используется generational ZGC.

  • -XX:+UseZGC -XX:+ZGenerational
			Используется generational ZGC.

-XX:+UseZGC -XX:+ZGenerational
		

Используется generational ZGC. Выдается предупреждение о том, что опция ZGenerational является устаревшей. Выдается предупреждение о том, что режим без поколений (non-generational) устарел и подлежит удалению.

Команда Axiom JDK реализовала доработку сборщиков мусора G1 GC, CMS GC, Parallel GC и Serial GC для выполнения требований ФСТЭК. Эта доработка включает в себя патчи, которые зануляют содержимое объектов, деаллоцируемых сборщиком мусора, и не дают другим приложениям получить доступ к конфиденциальным данным.

Структурированный параллелизм (третья предварительная версия)

Чтобы упростить многопоточное программирование, в JDK 19 было представлено API структурированного параллелизма. Это третья предварительная версия этого API, которая будет включена в JDK 23 без изменений для получения обратной связи от разработчиков.

Структурированный параллелизм (Structured Concurrency) рассматривает группы связанных задач, выполняемых в разных потоках как единое целое. Задача расщепляется на несколько подзадач, которые выполняются в отдельных потоках. Затем эти подзадачи воссоединяются в блоке кода главной задачи. Таким образом, задача координирует выполнение подзадач и отслеживать ошибки их выполнения. Это устраняет риски, связанные с отменой и внезапным завершением работы.

  • Взаимосвязь между задачами и подзадачами будет чётко прослеживаться в структуре кода, что повышает наблюдаемость.
  • Если происходит сбой в подзадаче или поток, выполняющий задачи, прерывается до вызова join(), то другие подзадачи отменяются. Это повышает надёжность кода.
Превью Java 23: еще удобнее, безопаснее и быстрее 1

Удобство разработки

Всё больше фичей Java позволяют упрощать написание кода и делать это быстрее.

Примитивные типы в шаблонах pattern matching, instanceof и switch (предварительная версия)

Благодаря JEP 455 можно использовать примитивные типы во всех контекстах шаблонов. Это улучшит pattern matching и расширит функции instanceof и switch для работы со всеми примитивными типами.

Цели этого JEP:

  • устранить ошибки времени исполнения,
  • позволить исследовать данные примитивных или ссылочных типов единым образом.

Раньше instanceof работал только с ссылочными типами, а удобного способа проверить примитивные значения на безопасность приведения типа не было. Из-за этого примитивное значение могло быть «по-тихому» преобразовано без выброса исключения. Теперь оператор instanceof проверяет и примитивные типы. Если значение может быть безопасно приведено, instanceof вернёт true, иначе — false.

С поддержкой примитивных типов в конструкции switch выражение ниже:

			switch (x.getStatus()) {
    case 0 -> "okay";
    case 1 -> "warning";
    case 2 -> "error";
    default -> "unknown status: " + x.getStatus();
}
		

Можно переписать c заменой блока default на блок с паттерном примитивного типа int, который раскрывает сопоставляемое значение:

			switch (x.getStatus()) {
    case 0 -> "okay";
    case 1 -> "warning";
    case 2 -> "error";
    case int i -> "unknown status: " + i;
}
		

Теперь к instance of. Преобразование значения int в float выполняется автоматически оператором присваивания, несмотря на то, что это потенциально может привести к потере данных. Разработчик не получает предупреждения об этом:

			int getPopulation() {...}
float pop = getPopulation();  // потенциальная потеря информации без предупреждения
		

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

			if (i >= -128 && i <= 127) {
    byte b = (byte)i;
    ... b ...
}
		

instanceof сможет проверять как значения, так и типы. Два предыдущих примера можно переписать так:

			if (getPopulation() instanceof float pop) {
    ... pop ...
}

if (i instanceof byte b) {
    ... b ...
}
		

Stream Gatherers (вторая предварительная версия)

Stream Gatherers появились в JDK 22 для усовершенствования Stream API поддержкой пользовательских intermediate-операторов. Во второй предварительной версии нет изменений по сравнению с JDK 22, цель этой версии — собрать дополнительную обратную связь и улучшить опыт использования.

Stream Gatherers позволит стримам преобразовывать данные более лёгкими способами по сравнению с существующими встроенными intermediate-операторами. Цели создания Stream Gatherers:

  • сделать стримы более гибкими,
  • позволить пользовательским intermediate-операторам управлять бесконечными потоками.

До Stream Gatherers для создания необходимой логики в уже существующих intermediate-операторах приходилось создавать отдельные классы или методы. Теперь Stream::gather(Gatherer) позволяет обрабатывать потоки в определённом пользователем порядке.

Курс по Java-разработке
  • постоянный доступ
  • бесплатно
  • онлайн
tproger.ru

gather() также является промежуточной операцией, поэтому может быть несколько gather() в одной цепочке:

			source.gather(a).gather(b).gather(c).collect(...)
		

Чтобы добиться такого же результата, как в примере выше, Gatherer поддерживает метод andThen(Gatherer). Так, этот пример эквивалентен следующей записи:

			source.gather(a.andThen(b).andThen(c)).collect(...)
		

Поддержка Markdown-разметки в doc-комментариях

Благодаря JEP 467 для написания doc-комментариев можно использовать Markdown, а не только сочетание HTML и тэгов JavaDoc. Это облегчит чтение и написание комментариев в исходном виде для API.

Сравните, как выглядит doc-комментарий в java.lang.Object.hashCode с использованием HTML и javadoc-тэгов:

			/**
 * Returns a hash code value for the object. This method is
 * supported for the benefit of hash tables such as those provided by
 * {@link java.util.HashMap}.
 * 
 * The general contract of {@code hashCode} is:
 * 
 * Whenever it is invoked on the same object more than once during
 *     an execution of a Java application, the {@code hashCode} method
 *     must consistently return the same integer, provided no information
 *     used in {@code equals} comparisons on the object is modified.
 *     This integer need not remain consistent from one execution of an
 *     application to another execution of the same application.
 * If two objects are equal according to the {@link
 *     #equals(Object) equals} method, then calling the {@code
 *     hashCode} method on each of the two objects must produce the
 *     same integer result.
 * It is not required that if two objects are unequal
 *     according to the {@link #equals(Object) equals} method, then
 *     calling the {@code hashCode} method on each of the two objects
 *     must produce distinct integer results.  However, the programmer
 *     should be aware that producing distinct integer results for
 *     unequal objects may improve the performance of hash tables.
 * 
 *
 * @implSpec
 * As far as is reasonably practical, the {@code hashCode} method defined
 * by class {@code Object} returns distinct integers for distinct objects.
 *
 * @return  a hash code value for this object.
 * @see     java.lang.Object#equals(java.lang.Object)
 * @see     java.lang.System#identityHashCode
 */
		

А теперь посмотрите, как выглядит этот же doc-комментарий, но в стиле Markdown без HTML и с несколькими javadoc-тэгами:

			/// Returns a hash code value for the object. This method is
/// supported for the benefit of hash tables such as those provided by
/// [java.util.HashMap].
///
/// The general contract of `hashCode` is:
///
///   - Whenever it is invoked on the same object more than once during
///     an execution of a Java application, the `hashCode` method
///     must consistently return the same integer, provided no information
///     used in `equals` comparisons on the object is modified.
///     This integer need not remain consistent from one execution of an
///     application to another execution of the same application.
///   - If two objects are equal according to the
///     [equals][#equals(Object)] method, then calling the
///     `hashCode` method on each of the two objects must produce the
///     same integer result.
///   - It is _not_ required that if two objects are unequal
///     according to the [equals][#equals(Object)] method, then
///     calling the `hashCode` method on each of the two objects
///     must produce distinct integer results.  However, the programmer
///     should be aware that producing distinct integer results for
///     unequal objects may improve the performance of hash tables.
///
/// @implSpec
/// As far as is reasonably practical, the `hashCode` method defined
/// by class `Object` returns distinct integers for distinct objects.
///
/// @return  a hash code value for this object.
/// @see     java.lang.Object#equals(java.lang.Object)
/// @see     java.lang.System#identityHashCode
		

Совсем другое дело. Его легче как написать, так и прочитать.

Импортирование модулей (предварительная версия)

JEP 476 позволит импортировать сам модуль вместо явного импортирования его отдельных пакетов. Это упростит переиспользование модульных библиотек путём одновременного импортирования модулей.

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

Так, вместо:

			import java.util.Map; // import java.util.*;
import java.util.function.Function; // import java.util.function.*;
import java.util.stream.Collectors; // import java.util.stream.*;
		

достаточно будет написать:

			import module java.base
		

Неявные классы и main-методы экземпляра класса (третья предварительная версия)

С помощью неявных классов и main-методов экземпляра класса начинающие Java-разработчики могут писать простые программы без сложных конструкций корпоративной разработки и усложнять свои программы с углублением знаний. Опытные разработчики смогут создавать приложения без компонентов, используемых для программирования крупных систем.

Рассмотрим пример. Так выглядит базовая программа Hello World!, которая даётся на начальном уровне изучения Java:

			public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
		

Примитивная по сути программа на Java усложняется большим количеством кода, непонятных новичку понятий и конструкций: class с модификатором доступа public, параметр String[] args, модификатор static и вывод System.out.println(). Прежде чем написать даже такую программу, нужно изучить кучу всего.

Курс по Java-разработке
  • постоянный доступ
  • бесплатно
  • онлайн
tproger.ru

Неявные классы имеют только конструктор по умолчанию без параметров (zero-parameter) находящийся в пакете unnamed, на который нельзя ссылаться по имени. Каждый неявный класс должен содержать метод main и представлять собой самостоятельную программу.

Тогда Hello World! упрощается до трёх строк:

			void main() {
    System.out.println("Hello, World!");
}
		

В третьей предварительной версии:

1. Неявные классы автоматически импортируют три статических метода для простого текстового ввода и вывода с консоли. Эти методы объявлены в новом классе верхнего уровня java.io.IO:

  • public static void println(Object obj),
  • public static void print(Object obj),
  • public static String readln(String prompt).

Теперь можно обойтись без System.in и System.out. Простая интерактивная программа упрощается до:

			void main() {
    String name = readln("Ваше имя: ");
    print("Здравствуйте, ");
    println(name);
}
		

2. Неявные классы автоматически по требованию импортируют все публичные классы верхнего уровня и интерфейсы пакетов, экспортируемых модулем java.base. Можно будет использовать API из широко используемых пакетов в теле неявного класса так, будто эти пакеты были импортированы.

Гибкий конструктор (вторая предварительная версия)

Эта функциональность появилась в JDK 22 под названием JEP 447: Выражения перед super(…). Чтобы уменьшить объём и сложность кода, можно добавлять инструкции в конструкторе перед явным вызовом конструктора super() или this(). Это не нарушает естественный порядок инициализации сверху-вниз (top-down).

Курс по Java-разработке
  • постоянный доступ
  • бесплатно
  • онлайн
tproger.ru

Во второй предварительной версии конструктор сможет инициализировать поля в том же классе перед явным вызовом конструктора. Конструктор суперкласса (superclass) не выполнит код, который увидит значение поля по умолчанию подкласса (например, 0, false или null). Это может произойти, когда из-за переопределения конструктор суперкласса вызывает метод в подклассе, используя некоторое поле.

Class-File API (вторая предварительная версия)

В Java 22 в предварительной версии появилось стандартный API для парсинга, генерации и трансформации class-файлов. Этот API будет развиваться вместе с форматом класс-файлов и позволит компонентам и фреймворкам Java-платформы использовать его вместо сторонних библиотек.

Во второй предварительной версии появилось следующее:

  • Упрощён класс CodeBuilder.
  • Экземпляры AttributeMapper в Attributes теперь доступны через статические методы, а не через статические поля. Это позволяет реализовать отложенную (“ленивую”) инициализацию и снизить затраты на запуск Java.
  • Переделали Signature.TypeArg в алгебраический тип данных для упрощения его использования.
  • Добавлены ClassReader.readEntryOrNull и ConstantPool.entryByIndex, которые выбрасывают ConstantPoolException вместо ClassCastException, если запись по индексу не требуемого типа. Процессоры Class-File смогут указывать, что несоответствие типа записи набора констант — это проблема формата Class-File, а не процессора.
  • Класс ClassSignature точнее моделирует общие сигнатуры суперклассов и суперинтерфейсов.
  • Исправлена опечатка в несоответствии имён в TypeKind.
  • Удалены подробные методы реализации в ClassReader.

Java 23 в России

Законы РФ запрещают использование зарубежных дистрибутивов — Oracle JDK и других поставщиков. Чтобы применять Java в проде легально и без опаски, стоит перейти на отечественную платформу Axiom JDK. Ее, например, использует почти каждый из нас, оплачивая услуги картой «Мир» и в Системе Быстрых Платежей (СБП), а также картами ушедших Visa и Mastercard. Все они процесcятся на российской Java.

Axiom JDK зафиксирован в реестре российского ПО и отвечает жестким требованиям критической информационной инфраструктуры (КИИ) по 4-ому уровню доверия сертификации ФСТЭК. Поэтому все секьюрно.

В целом, технологии Java можно найти почти везде — в соцсетях, добыче нефти и газа, финтехе, облачных сервисах, антивирусах. А ещё кассы самообслуживания в сети магазинов «Пятёрочка» тоже работают на Java. Теперь вы будете вспоминать об этом, когда покупаете там что-либо.

Заключение

Java 23 — это не LTS-версия. Но опыт Java 22 показал, что российские компании стремятся к инновациям и с момента выпуска стараются тестировать улучшения. Поэтому стоит приглядеться к изменениям в JDK 23 уже сейчас, чтобы оценить преимущества и заранее продумать миграцию на одну из LTS-версий.

Следите за новыми постами
Следите за новыми постами по любимым темам
5К открытий32К показов