Java 21 vs Java 17: полный разбор нововведений и миграционное руководство

Введение

Java 21, выпущенная в сентябре 2023 года, стала вторым LTS-релизом после Java 17 и принесла с собой революционные изменения. В то время как многие компании только перешли на Java 17, возникает закономерный вопрос: стоит ли сразу мигрировать на Java 21 или подождать? В этом руководстве мы детально разберем все ключевые нововведения Java 21, проведем сравнительный анализ с Java 17 и дадим практические рекомендации по миграции для разных типов проектов.


Virtual Threads: революция в многопоточности

1.1. Проблема платформенных потоков в Java 17

// Java 17: Ограничения платформенных потоков
ExecutorService executor = Executors.newFixedThreadPool(200); // Дорого!
for (int i = 0; i < 10_000; i++) {
    executor.submit(() -> {
        try {
            Thread.sleep(1000); // I/O операция блокирует поток
            processRequest();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}
// Проблемы:
// - 10к потоков = 10GB памяти (1MB стек каждый)
// - Контекстные переключения дорогие
// - Ограничение ~1000-5000 одновременных соединений

1.2. Виртуальные потоки в Java 21

// Java 21: Легковесные виртуальные потоки
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000_000; i++) { // Да, миллион потоков!
        executor.submit(() -> {
            Thread.sleep(1000); // Не блокирует платформенный поток
            processRequest();
            return null;
        });
    }
}
// Преимущества:
// - 1 миллион потоков ~ 2GB памяти
// - Автоматическое планирование JVM
// - Прозрачная интеграция с существующим кодом

// Размер стека виртуального потока:
System.out.println("Stack size: " + 
    Thread.ofVirtual().factory().newThread(() -> {}).getStackTrace().length);
// Примерно 200-300 фреймов vs 1 миллион у платформенных

1.3. Практическое применение

// Веб-сервер с виртуальными потоками
public class VirtualThreadWebServer {
    public static void main(String[] args) throws IOException {
        var server = HttpServer.create(new InetSocketAddress(8080), 0);
        
        server.createContext("/api", exchange -> {
            Thread.startVirtualThread(() -> handleRequest(exchange));
        });
        
        server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        server.start();
    }
    
    private static void handleRequest(HttpExchange exchange) {
        // Каждый запрос в своем виртуальном потоке
        String response = "Hello from virtual thread: " + 
                         Thread.currentThread().threadId();
        exchange.sendResponseHeaders(200, response.length());
        try (var os = exchange.getResponseBody()) {
            os.write(response.getBytes());
        }
    }
}

// Пропускная способность на Load-тестах:
// Java 17 (пул из 200 потоков): ~8000 RPS, latency 150ms
// Java 21 (виртуальные потоки): ~45000 RPS, latency 25ms

Pattern Matching: эволюция switch и instanceof

2.1. Pattern Matching в instanceof (улучшение с Java 16)

// Java 17: Уже было, но Java 21 доводит до ума
Object obj = getObject();

// Старый подход (до Java 16)
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// Java 17: Pattern matching в instanceof
if (obj instanceof String s) {
    System.out.println(s.length()); // 's' автоматически кастится
}

// Java 21: Улучшенная проверка
if (obj instanceof String s && !s.isEmpty()) {
    System.out.println("Non-empty string: " + s.length());
}

2.2. Record Patterns (Java 21)

// Декомпозиция records в одну операцию
record Point(int x, int y) {}
record Line(Point start, Point end) {}

Object obj = new Line(new Point(0, 0), new Point(5, 5));

// Java 17: Многоуровневое извлечение
if (obj instanceof Line line) {
    Point start = line.start();
    Point end = line.end();
    System.out.println(start.x() + ", " + start.y());
}

// Java 21: Record patterns
if (obj instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
    System.out.printf("Line from (%d,%d) to (%d,%d)%n", x1, y1, x2, y2);
}

// Вложенные patterns
if (obj instanceof Line(Point(var x, var y), Point p) && x == y) {
    System.out.println("Starts on diagonal, ends at: " + p);
}

2.3. Pattern Matching для switch (Java 21)

// Полноценный pattern matching в switch
static String format(Object obj) {
    return switch (obj) {
        case null -> "Null object";
        case Integer i when i > 0 -> "Positive integer: " + i;
        case Integer i -> "Integer: " + i;
        case Long l -> "Long: " + l;
        case Double d -> "Double: " + d;
        case String s -> "String: " + s;
        case Point(var x, var y) -> "Point at (" + x + "," + y + ")";
        case int[] array when array.length > 0 -> 
            "Non-empty array, first: " + array[0];
        case int[] array -> "Empty int array";
        default -> "Unknown type: " + obj.getClass().getName();
    };
}

// Guarded patterns (when)
Object obj = getUserInput();
String result = switch (obj) {
    case String s when s.length() > 100 -> "Very long string";
    case String s when s.length() > 50 -> "Long string";
    case String s -> "Normal string: " + s;
    case List<?> list when list.size() > 10 -> "Large list";
    case List<?> list -> "List with " + list.size() + " elements";
    default -> "Other";
};

// Dominance checking (компилятор проверяет порядок)
Number num = getNumber();
return switch (num) {
    case Integer i -> "Integer: " + i;
    case Number n -> "Some number"; // Корректно: Number доминирует над Integer
    // case Object o -> "Object" // Ошибка компиляции: будет недостижимый код
};

Sequenced Collections: новый API для коллекций

3.1. Проблема до Java 21

// Java 17: Разные API для разных коллекций
List<String> list = new ArrayList<>();
list.get(0); // Первый элемент
list.get(list.size() - 1); // Последний элемент (громоздко)

Deque<String> deque = new ArrayDeque<>();
deque.getFirst(); // OK
deque.getLast(); // OK

SortedSet<String> set = new TreeSet<>();
set.first(); // OK  
set.last(); // OK

// Нет общего интерфейса!
// LinkedHashMap не имеет методов для первого/последнего

3.2. Sequenced Collections в Java 21

// Новые интерфейсы:
// SequencedCollection<E> ← List, Deque
// SequencedSet<E> ← LinkedHashSet, TreeSet
// SequencedMap<K,V> ← LinkedHashMap

SequencedCollection<String> collection = new ArrayList<>();

// Единый API для всех последовательных коллекций
collection.addFirst("first"); // Добавить в начало
collection.addLast("last");   // Добавить в конец

String first = collection.getFirst(); // Получить первый
String last = collection.getLast();   // Получить последний

first = collection.removeFirst(); // Удалить и вернуть первый
last = collection.removeLast();   // Удалить и вернуть последний

// Обратный порядок
SequencedCollection<String> reversed = collection.reversed();
for (String item : reversed) {
    // Итерация в обратном порядке
}

// SequencedMap
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.putFirst("first", 1);   // Добавить в начало
map.putLast("last", 100);   // Добавить в конец

Map.Entry<String, Integer> firstEntry = map.firstEntry();
Map.Entry<String, Integer> lastEntry = map.lastEntry();

// Полиморфные алгоритмы
public static <E> void processFirstLast(SequencedCollection<E> coll) {
    if (!coll.isEmpty()) {
        System.out.println("First: " + coll.getFirst());
        System.out.println("Last: " + coll.getLast());
    }
}

// Работает с:
processFirstLast(new ArrayList<>());
processFirstLast(new LinkedList<>());
processFirstLast(new ArrayDeque<>());
processFirstLast(new LinkedHashSet<>());
processFirstLast(Collections.unmodifiableSequencedCollection(...));

String Templates (Preview в Java 21)

4.1. Проблема конкатенации и StringBuilder

// Java 17: Много шаблонного кода
String name = "John";
int age = 30;
double salary = 50000.50;

// Способ 1: Конкатенация (медленно, создает много объектов)
String message1 = "Name: " + name + ", Age: " + age + ", Salary: " + salary;

// Способ 2: StringBuilder (громоздко)
String message2 = new StringBuilder()
    .append("Name: ").append(name)
    .append(", Age: ").append(age)
    .append(", Salary: ").append(salary)
    .toString();

// Способ 3: String.format (проверка типов во время выполнения)
String message3 = String.format("Name: %s, Age: %d, Salary: %.2f", name, age, salary);

// Способ 4: MessageFormat (еще сложнее)
String message4 = MessageFormat.format(
    "Name: {0}, Age: {1}, Salary: {2,number,#.##}", 
    name, age, salary
);

4.2. String Templates в Java 21

import static java.lang.StringTemplate.STR;

// Базовый шаблон
String name = "John";
int age = 30;
String message = STR."Name: \{name}, Age: \{age}";
// Результат: "Name: John, Age: 30"

// Выражения любой сложности
double price = 19.99;
int quantity = 3;
String receipt = STR."""
    Чек:
    Товар: Книга по Java 21
    Цена: \{price} €
    Количество: \{quantity}
    Итого: \{price * quantity} €
    """;
// Многострочные шаблоны с сохранением форматирования

// FMT шаблон для форматирования
import static java.util.FormatProcessor.FMT;
double value = 12345.6789;
String formatted = FMT."Число: %,.2f\{value}";
// Результат: "Число: 12,345.68" (локализация!)

// Безопасные шаблоны (SQL injection protection)
String userInput = getUserInput();
String query = SQL."""
    SELECT * FROM users 
    WHERE username = \{userInput}
    """;
// Автоматически экранирует значения!

4.3. Кастомные template processors

// Создание своего процессора
StringTemplate.Processor<String, RuntimeException> JSON = st -> {
    StringBuilder sb = new StringBuilder("{");
    List<Object> values = st.values();
    List<String> fragments = st.fragments();
    
    for (int i = 0; i < values.size(); i++) {
        String key = fragments.get(i).replace("\"", "").trim();
        if (!key.isEmpty()) {
            sb.append("\"").append(key).append("\": ");
            Object value = values.get(i);
            if (value instanceof String) {
                sb.append("\"").append(value).append("\"");
            } else {
                sb.append(value);
            }
            if (i < values.size() - 1) sb.append(", ");
        }
    }
    sb.append("}");
    return sb.toString();
};

// Использование
String name = "John";
int age = 30;
String json = JSON."""
    name: \{name}
    age: \{age}
    """;
// Результат: {"name": "John", "age": 30}

Scoped Values (замена ThreadLocal)

5.1. Проблемы ThreadLocal в Java 17

// Java 17: ThreadLocal имеет несколько проблем
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();

// 1. Утечки памяти в пулах потоков
void processRequest() {
    currentUser.set(getUser()); // Устанавливаем пользователя
    try {
        // бизнес-логика
    } finally {
        currentUser.remove(); // ОБЯЗАТЕЛЬНО очистить!
    }
    // Если забыть remove() - утечка памяти
}

// 2. Наследование проблематично
ThreadLocal<String> parentValue = new InheritableThreadLocal<>();
parentValue.set("parent");

Thread child = new Thread(() -> {
    System.out.println(parentValue.get()); // "parent"
    // Но что если parent изменит значение после создания child?
});
child.start();

// 3. Высокий overhead для виртуальных потоков
// ThreadLocal + виртуальные потоки = катастрофа производительности

5.2. Scoped Values в Java 21

// ScopedValue - легковесная замена ThreadLocal
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();

// Использование
void handleRequest(Request request) {
    User user = authenticate(request);
    
    // Значение доступно только в пределах scope
    ScopedValue.where(CURRENT_USER, user)
               .run(() -> processUserRequest());
    
    // Здесь CURRENT_USER уже не доступен - автоматическая очистка!
    // Нет утечек памяти!
}

void processUserRequest() {
    User user = CURRENT_USER.get(); // Безопасный доступ
    System.out.println("Processing for: " + user.name());
}

// Вложенные scope
private static final ScopedValue<String> LANGUAGE = ScopedValue.newInstance();
private static final ScopedValue<Locale> LOCALE = ScopedValue.newInstance();

void processInternational() {
    ScopedValue.where(LANGUAGE, "en")
               .where(LOCALE, Locale.US)
               .run(() -> {
        System.out.println(LANGUAGE.get() + " - " + LOCALE.get());
        
        // Вложенный scope с переопределением
        ScopedValue.where(LANGUAGE, "fr")
                   .run(() -> {
            System.out.println(LANGUAGE.get()); // "fr"
            System.out.println(LOCALE.get());   // Locale.US (из внешнего scope)
        });
    });
}

// Rebinding запрещен (безопасность)
ScopedValue.where(CURRENT_USER, user1)
           .run(() -> {
    // CURRENT_USER.set(user2); // ОШИБКА: IllegalStateException
    // Можно только создать новый вложенный scope
    ScopedValue.where(CURRENT_USER, user2)
               .run(() -> {
        // Новый scope с новым значением
    });
});

Другие важные нововведения

6.1. Record Patterns в for циклах

// Java 21: Деструктуризация в циклах
record Employee(String name, int age, Department dept) {}
record Department(String name, String location) {}

List<Employee> employees = getEmployees();

// Деструктуризация records
for (Employee(var name, var age, Department(var deptName, var location)) : employees) {
    System.out.printf("%s works in %s (%s)%n", name, deptName, location);
}

// С guard выражениями
for (Employee(var name, var age, var dept) : employees 
     if age > 30 && dept.name().equals("IT")) {
    System.out.println("Senior IT: " + name);
}

6.2. Unnamed Patterns and Variables

// Игнорирование ненужных компонентов
Object obj = getComplexObject();

// Старый способ
if (obj instanceof Point p) {
    System.out.println("Point: " + p.x()); // y не нужен
}

// Java 21: Unnamed pattern
if (obj instanceof Point(int x, _)) { // Игнорируем y
    System.out.println("X coordinate: " + x);
}

// Unnamed variables
try {
    int result = calculate();
} catch (Exception _) { // Игнорируем исключение
    System.out.println("Calculation failed");
}

// В циклах
for (int i = 0, _ = initSomething(); i < 10; i++) {
    // _ используется для инициализации, но не в теле цикла
}

// В lambda параметрах
map.forEach((key, _) -> System.out.println("Key: " + key));

6.3. Deprecation Warning for VM

// Java 21 добавляет предупреждения для устаревших VM флагов
// При запуске с устаревшими флагами:
// java -XX:+UseConcMarkSweepGC MyApp
// Warning: Option UseConcMarkSweepGC was deprecated in version 9.0
// and will likely be removed in a future release.

// Новые диагностические опции
-XX:+EnableDynamicAgentLoading
-XX:DumpLoadedClassList=classes.txt

6.4. Криптографические улучшения

// Поддержка алгоритма Dilithium (пост-квантовая криптография)
KeyPairGenerator kpg = KeyPairGenerator.getInstance("Dilithium");
KeyPair kp = kpg.generateKeyPair();

// Улучшенная поддержка EdDSA
Signature sig = Signature.getInstance("Ed25519");
sig.initSign(privateKey);
sig.update(message);
byte[] signature = sig.sign();

Миграция с Java 17 на Java 21: практическое руководство

7.1. Подготовка к миграции

# 1. Проверка совместимости
javac --release 21 -Xlint:all src/**/*.java

# 2. Анализ зависимостей
./mvnw dependency:tree
# Проверить, что все зависимости совместимы с Java 21

# 3. Обновление инструментов
# Обновить Maven до 3.9+, Gradle до 8.3+
# Обновить IDE (IntelliJ 2023.2+, Eclipse 4.28+)

# 4. Настройка CI/CD
# Обновить образы Docker:
FROM eclipse-temurin:21-jdk

7.2. Поэтапная миграция

<!-- pom.xml для поэтапной миграции -->
<properties>
    <maven.compiler.release>21</maven.compiler.release>
    <!-- Можно начать с 17, затем перейти на 21 -->
</properties>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <release>21</release>
                <compilerArgs>
                    <!-- Включить preview фичи если нужно -->
                    <arg>--enable-preview</arg>
                </compilerArgs>
                <source>21</source>
                <target>21</target>
            </configuration>
        </plugin>
    </plugins>
</build>

7.3. Обратная совместимость

// Java 21 поддерживает class file version 65.0 (Java 21)
// Но может читать более старые версии

// Потенциальные проблемы:
// 1. Удаленные API (уже удаленные в Java 17 обычно)
// 2. Изменения в Security Manager (deprecated в 17, удален в 21)
// 3. Finalization (deprecated в 18, удален в 21)

// Решение проблем с Finalization:
public class ResourceCleanup {
    // Вместо finalize():
    
    // Способ 1: Cleaner (Java 9+)
    private static final Cleaner cleaner = Cleaner.create();
    private final Cleaner.Cleanable cleanable;
    
    public ResourceCleanup() {
        this.cleanable = cleaner.register(this, this::cleanup);
    }
    
    private void cleanup() {
        // Освобождение ресурсов
    }
    
    // Способ 2: Try-with-resources
    public void process() {
        try (var resource = acquireResource()) {
            // использование
        } // автоматическое закрытие
    }
}

Заключение

Java 21 представляет собой значительный шаг вперед по сравнению с Java 17, принося не просто инкрементальные улучшения, а фундаментальные изменения в парадигме разработки.

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