Введение
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 { 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 { 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 list = new ArrayList(); list.get(0); // Первый элемент list.get(list.size() - 1); // Последний элемент (громоздко) Deque deque = new ArrayDeque(); deque.getFirst(); // OK deque.getLast(); // OK SortedSet set = new TreeSet(); set.first(); // OK set.last(); // OK // Нет общего интерфейса! // LinkedHashMap не имеет методов для первого/последнего
3.2. Sequenced Collections в Java 21
// Новые интерфейсы: // SequencedCollection ← List, Deque // SequencedSet ← LinkedHashSet, TreeSet // SequencedMap ← LinkedHashMap SequencedCollection 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 reversed = collection.reversed(); for (String item : reversed) { // Итерация в обратном порядке } // SequencedMap SequencedMap map = new LinkedHashMap(); map.putFirst("first", 1); // Добавить в начало map.putLast("last", 100); // Добавить в конец Map.Entry firstEntry = map.firstEntry(); Map.Entry lastEntry = map.lastEntry(); // Полиморфные алгоритмы public static void processFirstLast(SequencedCollection 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 JSON = st -> { StringBuilder sb = new StringBuilder("{"); List values = st.values(); List fragments = st.fragments(); for (int i = 0; i
Scoped Values (замена ThreadLocal)
5.1. Проблемы ThreadLocal в Java 17
// Java 17: ThreadLocal имеет несколько проблем private static final ThreadLocal currentUser = new ThreadLocal(); // 1. Утечки памяти в пулах потоков void processRequest() { currentUser.set(getUser()); // Устанавливаем пользователя try { // бизнес-логика } finally { currentUser.remove(); // ОБЯЗАТЕЛЬНО очистить! } // Если забыть remove() - утечка памяти } // 2. Наследование проблематично ThreadLocal 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 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 LANGUAGE = ScopedValue.newInstance(); private static final ScopedValue 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 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 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. Поэтапная миграция
21 org.apache.maven.plugins maven-compiler-plugin 3.11.0 21 --enable-preview 21 21
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-разработки, где асинхронность становится простой, код — выразительным, а производительность — предсказуемой. Инвестиции в миграцию окупятся повышением производительности разработки и улучшением характеристик приложений.