Подробные статьи о технологиях, определяющих будущее.

Java становится лучше, а этого почти никто не замечает

Полугодовой цикл релизов Java незаметно изменил язык. Pattern matching, виртуальные потоки и records делают его неузнаваемым.

Светящаяся чашка кофе, превращающая унылый старый офис в светлое современное пространство

Java — это Родни Дэнджерфилд среди языков программирования: её никто не уважает. Упомяните Java большинству разработчиков младше 30 лет, и они представят XML-конфигурации, enterprise beans и AbstractSingletonProxyFactoryBean. Репутация языка застыла в 2008 году, когда последней версией была Java 6, а писать на Java значило захлёбываться в шаблонном коде.

Но с тех пор, как в 2017 году Java перешла на шестимесячный цикл релизов, произошло нечто примечательное. Язык развивается темпами, которые были немыслимы в четырёхлетних промежутках между Java 6, 7 и 8. Records. Sealed-классы. Pattern matching. Виртуальные потоки. String-шаблоны. Структурированная конкурентность. Каждый релиз добавляет возможности, которые делают код на Java короче, выразительнее и приятнее в написании. Java 26 продолжает эту серию, и разрыв между «Java, какая она есть» и «Java, какой её себе представляют», никогда не был больше.

Функции, которые всё изменили

Если вы не писали на Java со времён Java 8, вы пропустили целое десятилетие улучшений. Вот как выглядит язык сейчас.

Records (Java 14) — неизменяемые классы данных. То, для чего раньше требовалось 50 строк шаблонного кода — поля, конструктор, геттеры, equals, hashCode, toString — теперь умещается в одну строку.

// Java 8: 50 lines of boilerplate
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() { return Objects.hash(x, y); }
@Override
public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}
// Java 14+: one line
record Point(int x, int y) {}

Pattern matching (Java 16–21, всё ещё развивается) позволяет деструктурировать объекты в switch-выражениях и проверках instanceof. Это устраняет каскадные цепочки if-else-instanceof, которые так досаждали Java-коду.

// Pattern matching with sealed types and records
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// Exhaustive pattern matching — compiler verifies all cases covered
double area(Shape shape) {
return switch (shape) {
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h) -> w * h;
case Triangle(var b, var h) -> 0.5 * b * h;
};
}

Виртуальные потоки (Java 21) — легковесные потоки, которыми управляет JVM, а не ОС. Можно создать миллионы таких потоков, не упираясь в память или дескрипторы потоков ОС. Благодаря этому модель «один поток на запрос» — самая простая модель конкурентности — становится практичной в больших масштабах.

// Create a million concurrent tasks — try this with OS threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1_000_000).forEach(i -> {
executor.submit(() -> {
// Each task gets its own virtual thread
// Blocking I/O (HTTP, DB, file) automatically yields
var response = httpClient.send(request, BodyHandlers.ofString());
return response.body();
});
});
}
// All done — executor auto-closes, waits for completion

Виртуальные потоки — ответ Java на горутины Go и корутины Kotlin. Отличие в том, что не нужен специальный синтаксис и не нужно раскрашивать функции. Обычный блокирующий код работает: JVM прозрачно приостанавливает и возобновляет виртуальные потоки в точках блокировки. Ваш существующий блокирующий HTTP-клиент, JDBC-драйвер и код файлового ввода-вывода получают выгоду без модификаций.

Почему этого никто не замечает

Траектория улучшений Java действительно впечатляет. Так почему восприятие всё ещё застряло в 2008 году?

Во-первых, корпоративная инерция. Многие организации работают на Java 8 или 11, потому что обновление большой кодовой базы дорого и рискованно. Когда разработчики говорят «Java», они имеют в виду версию, которую использует их компания, а не последний релиз. Разработчики Java 8 не пробовали records, pattern matching или виртуальные потоки.

Во-вторых, слой фреймворков. Spring Boot, Jakarta EE и другие корпоративные фреймворки добавляют свою сложность поверх языка. Разработчики винят «Java» в церемониях с аннотациями Spring, но виноват фреймворк, а не язык. Современная Java без тяжёлого фреймворка удивительно легковесна.

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

Преимущество JVM

Улучшения языка важны, но главное преимущество Java по-прежнему в JVM. После трёх десятилетий оптимизаций HotSpot JVM — одна из самых совершенных сред выполнения из когда-либо созданных.

  • JIT-компиляция. JVM профилирует выполняющийся код и компилирует горячие методы в оптимизированный нативный код. Для долгоживущих приложений (серверы, обработка данных) производительность JVM часто не уступает C++, а иногда и превосходит её, поскольку JIT может оптимизировать код под реальное поведение во время выполнения, а не только по результатам статического анализа.
  • Сборка мусора. Современные реализации GC (ZGC, Shenandoah) обеспечивают паузы менее миллисекунды даже при терабайтных кучах. Критика «паузы GC убивают латентность» перестала быть верной много лет назад — эти сборщики работают параллельно с приложением.
  • Наблюдаемость. JFR (Java Flight Recorder) предоставляет безопасное для продакшена профилирование с накладными расходами, близкими к нулю. Можно профилировать использование CPU, выделение памяти, конкуренцию за блокировки, задержки ввода-вывода и поведение GC, не замедляя приложение.
  • Зрелость экосистемы. В Maven Central — миллионы пакетов. Инструменты сборки (Gradle, Maven) хорошо изучены. Экосистема тестирования (JUnit, Mockito, Testcontainers) отличная. История развёртывания (контейнеры, нативные образы GraalVM) заметно созрела.

GraalVM и нативные образы

Слабость Java всегда была во времени запуска. JVM требуется сотни миллисекунд, чтобы инициализироваться, загрузить классы и прогреть JIT-компилятор. Для serverless-функций и CLI-утилит такие накладные расходы неприемлемы.

GraalVM Native Image решает эту проблему, компилируя Java-приложения заранее (AOT) в самостоятельные исполняемые файлы. JVM не нужна. Время запуска падает с сотен миллисекунд до однозначного числа миллисекунд. Потребление памяти снижается, потому что нет интерпретатора, JIT-компилятора и метаданных классов, которые нужно загрузить.

Компромисс: нативные образы теряют адаптивную оптимизацию JVM. Приложение с JIT-компиляцией со временем становится быстрее, так как JVM изучает горячие пути. Нативный образ стартует быстро, но не улучшается. Для короткоживущих процессов (CLI, serverless) нативные образы выигрывают. Для долгоживущих серверов JIT в конце концов даёт более быстрый код. Выбирайте инструмент под характер вашей среды выполнения.

Современная Java и альтернативы

Языки, которые «пришли на замену» Java — Go, Kotlin, Rust — отличные. Но современная Java конкурирует эффективнее, чем многие думают.

Простота Go привлекательна, но у Java теперь есть виртуальные потоки (аналог горутин) и гораздо более богатая система типов. Системы типов важны для больших кодовых баз, и система типов Java — особенно с sealed-типами и pattern matching — ловит ошибки, которые пропускает система интерфейсов Go.

Kotlin улучшает синтаксис Java, но работает на той же JVM. Большинство преимуществ Kotlin над Java 8 (null-безопасность, data-классы, корутины) имеют частичные или полные аналоги в современной Java (паттерны с Optional, records, виртуальные потоки). Kotlin всё ещё лаконичнее, но разрыв заметно сократился.

Rust занимает другую нишу — системное программирование без сборщика мусора. Java не стоит конкурировать там. Но для серверных приложений, обработки данных и корпоративного ПО сочетание производительности, безопасности и зрелости экосистемы Java трудно переиграть.

Полугодовой цикл — настоящее нововведение

Важнее любой отдельной функции оказался шестимесячный цикл релизов, который изменил траекторию Java. До 2017 года релизы Java были событиями раз в три-четыре года, в которых пытались выкатить всё сразу. Фичи задерживались, потому что не были готовы, что задерживало весь релиз, что создавало давление на то, чтобы втиснуть всё в следующий релиз, который тоже задерживался. Java 9 шла больше трёх лет отчасти из-за сложности модульной системы.

При шестимесячных релизах фичи выходят тогда, когда готовы. Pattern matching поставлялся постепенно в Java 16–24, и каждый релиз добавлял одну часть. Виртуальные потоки прошли два preview-релиза, прежде чем стали финальными. Такой инкрементальный подход позволяет команде Java собирать реальную обратную связь и корректировать дизайн до того, как принимать постоянные решения по API.

Урок выходит за рамки Java: частые небольшие релизы складываются в трансформирующие изменения. Каждый отдельный релиз Java начиная с 17 был скромным. Вместе же они переизобрели язык. Если вы пробовали Java пять лет назад и отпугнулись многословностью, попробуйте снова. Возможно, вас удивит то, что вы найдёте.