Java пополняется новыми модными механизмами, а вместе с ней и её усыпальница — место, куда отправляются устаревшие механизмы, такие как Vector, Finalization, NashornScriptEngine, SecurityManager и Unsafe. Взглянем на эти "реликвии" и посмотрим, что пришло им на замену.

Java живёт, растёт, обрастает фичами, API и модулями, но вместе с этим время от времени сбрасывает старую кожу. Некоторые идеи оказываются ненужными, другие — не лучшими решениями для задач, третьи — костылями. Всеми этими устаревшими или удалёнными механизмами тихо и стабильно наполняется усыпальница Java.
Взглянем на некоторые из них.
Ещё во времена, когда в Java не было List, Deque или Map, в коде бегали дикие Vector, Stack и Dictionary. В отличие от всех последующих "реликвий", этот устаревший API не помечен как Deprecated, но в то же время документация к каждому из классов кричит "НЕ ИСПОЛЬЗУЙ МЕНЯ!". На просторах интернета можно найти общее название для этих устаревших классов: "legacy collection classes".
Но почему они устарели?
У "legacy collections" есть два основных минуса:
Пойдём по порядку.
Первый минус: все точки взаимодействия с этими классами являются синхронизированными.
Вообще, это звучит больше как плюс, чем минус. Ведь безопасность в многопоточке — это всегда хорошо. Но что, если эти коллекции используются в однопоточном контексте? Мы просто теряем производительность из-за синхронизаций, которые по факту никак не повышают нам безопасность кода. Особенно это было бы заметно в тенденциях современной Java, где предпочтение отдается копированию коллекций, а не их модификации.
Второй минус: классы не имеют общих интерфейсов, и работать с каждым нужно по-своему.
Сталкиваемся с проблемой, которая стара как само программирование: не универсальный код. Представим следующую ситуацию: мы — разработчик на старенькой версии Java, и у нас есть код, завязанный на Stack. Например, вот такой:
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < 5; i++) {
stack.add(i);
}
System.out.println(stack.pop());
Сейчас код максимально тривиальный, но представим, что он разросся до 1000 строк и имеет 150 обращений к переменной stack.
Нам по факсу приходит сообщение от клиента, который пользуется этим кодом, где говорится, что он работает очень медленно (код, не клиент). Мы сидим, думаем и понимаем, что в Stack, имеющим под собой массив для хранения элементов, этот самый массив слишком часто пересоздаётся.
Думаем ещё немного и выдвигаем теорию: со связанным списком скорость приложения должна ускориться в 100 раз! Мы хотим проверить её и находим в интернете какой-нибудь LinkedList. Копируем себе и рефакторим эти 150 обращений к stack. Весело? Нет, но и другого выхода у нас не наблюдается.
Переносимся в настоящее время, где наш код выглядит уже следующим образом:
Deque<Integer> deque = new ArrayDeque<>();
IntStream.range(0, 5)
.forEach(deque::addLast);
System.out.println(deque.pollLast());
Код через какое-то время также разрастается в 150 обращений, но уже к deque. Проходим тот же путь и также выдвигаем теорию, что со связанным списком будет быстрее. И теперь... просто меняем объявление ArrayDeque на LinkedList, который тоже реализует Deque, и всё!
Таким образом, Collections Framework решил проблему отсутствия общих интерфейсов, позволив не завязываться на конкретных реализациях. Например, не завязываться на Vector, а использовать интерфейс List.
Когда-то финализаторы (#finalize()) казались отличной идеей: метод, который вызовется при уничтожении объекта и позволит освободить ресурсы системы.
Но, как выяснилось, идея эта оказалась не очень:
Зайдём издалека. Современная Java очень гибкая, вплоть до того, что сборщик мусора может быть любой и сконфигурирован по-разному. В таких условиях нельзя гарантировать, что экземпляр вообще будет когда-либо удалён.
И не забудем, что сборщик мусора гарантированно не соберёт экземпляр, если на него есть хоть одна сильная ссылка.
Именно по этим причинам логика освобождения системных ресурсов не должна зависеть от жизненного цикла объекта.
Если взглянуть на документацию метода #finalize(), то мы увидим, что вместо него рекомендуют пользоваться интерфейсом AutoCloseable. Он был создан именно для того, чтобы грамотно освобождать ресурсы, не завязываясь на сборщике мусора. Главное только не забывать это делать.
Что же касается отслеживания удаления объекта, то Java предоставила нам альтернативу в лице java.lang.ref.Cleaner, который решает вышеописанные проблемы.
Для наглядности поиграемся с памятью Java и напишем небезопасный код с использованием финализатора:
class ImmortalObject {
private static Collection<ImmortalObject> triedToDelete = new ArrayList<>();
@Override
protected void finalize() throws Throwable {
triedToDelete.add(this);
}
}
Здесь мы буквально воскрешаем каждый экземпляр ImmortalObject, который собирается удалить сборщик мусора, что не понравится ни вашей оперативной памяти, ни вам самим, когда упадёте с OutOfMemoryError.
А вот так выглядит аналогичный код с Cleaner:
static List<ImmortalObject> immortalObjects = new ArrayList<>();
....
var immortalObject = new ImmortalObject();
Cleaner.create()
.register(
immortalObject,
() -> immortalObjects.add(immortalObject)
);
Что здесь стоит отметить? Мы также можем слушать удаление абсолютно любого уже существующего объекта.
Стоп! Мы же буквально только что узнали, чем этот подход плох! Всё так, но он плох только при работе с ресурсами. Иногда Java-приложению всё-таки нужно знать о том, что сборщик мусора удаляет объект. Зачем? Например, для создания оптимизированного кеша.
Класс всё равно оправдает своё название, но уже не из-за того, что в момент удаления экземпляра мы добавляем его в статический список, а из-за того, что сам Runnable, который здесь является слушателем, держит ссылку на immortalObject. Теперь воскрешения объекта не будет, потому что сборщик мусора и не начнёт удалять экземпляр. Уже более Java-style и никакой некромантии.
Финализаторы часто использовались при работе с объектами, которые были созданы через JNI где-то не в Java коде, а, например, в C++. Но сам механизм был небезопасен, и финализаторам придумали вполне элегантную замену.
Немногие помнят, немногие знают, но когда-то Java шла в ногу с JavaScript, причём буквально: в JDK 8 появился встроенный JS-движок Nashorn, который позволял исполнять JavaScript прямо из Java кода.
И мы могли реализовать ту самую популярную задачку с бананом, не выходя из Java:
new NashornScriptEngineFactory()
.getScriptEngine()
.eval("('b' + 'a' + + 'a' + 'a').toLowerCase();");
Использовать это, конечно, прикольно, но ровно до тех пор, пока не осознаешь, что Java программистам приходилось поддерживать JavaScript движок.
Невозможность быстро эволюционировать вместе с JavaScript и появление полиглотных решений (например, GraalVM Polyglot) сделали Nashorn устаревшим уже через 4 версии языка (в Java 11), а через некоторое время (в Java 15) его окончательно выкинули за борт.
Но почему тот же GraalVM Polyglot оказался лучше? В то время как Nashorn реализовывал JavaScript поверх Java, Polyglot пошёл дальше и создал целую платформу на базе JVM, которая нативно поддерживала множество языков, включая Java и JavaScript.
Ещё один старожил, доживший до Java 17, — SecurityManager.
Во времена диких апплетов, когда Java-код мог выполняться прямо в браузере, SecurityManager был опорой безопасности Java-приложений.
Апплет — небольшая программа, которая запускалась прямо в браузере и могла показывать анимации, графику или интерактивные элементы, но работала с жёсткими ограничениями.
Но время шло, дикие апплеты окончательно перебирались из браузера на рабочий стол, а после вообще вымерли как подход. Но SecurityManager остался и часто использовался в enterprise-приложениях.

Но ничто не вечно, и смерть с косой пришла и за SecurityManager. В Java 17 всю его внутрянку выпилили и оставили лишь фасад без логики. Если взглянуть на точку входа для установки SecurityManager, можно найти лишь вот такую заглушку:
@Deprecated(since = "17", forRemoval = true)
public static void setSecurityManager(SecurityManager sm) {
throw new UnsupportedOperationException(
"Setting a Security Manager is not supported");
}
Как работал SecurityManager? Он выступал внутренним стражем JVM, который перехватывал потенциально опасную операцию и проверял, имеет ли каждый элемент стека право на такое действие.
Что конкретно перехватывал SecurityManager? Список огромен, поэтому предлагаю взглянуть только на основные моменты:
SecurityManager хорошо работал во времена браузерных апплетов, где нужно было удерживать недостоверный код в песочнице, но с развитием контейнеризации он устарел.
Пусть SecurityManager появился изначально для апплетов, использовался он не только там. Например, Tomcat поддерживал его использование в серверных приложениях. А в нашем статическом анализаторе PVS-Studio он использовался для решения забавной проблемы: если статический инициализатор класса в анализируемом коде вызывал System#exit, процесс анализа завершался :) SecurityManager позволил запретить такие операции при анализе статических полей, а позже мы перешли на использование ASM.
Кстати, пусть апплеты как подход и перестали использоваться раньше, чем SecurityManager, выпиливать их JDK начали только сейчас.
Мир меняется, подходы меняются, SecurityManager уходит.
Из Java удаляют небезопасность. Буквально.
sun.misc.Unsafe — это, пожалуй, самый легендарный обитатель усыпальницы.
Этот класс был потайным ходом, ведущим в глубины JVM, где перестают действовать привычные правила языка. С помощью Unsafe можно легко уронить Java-приложение с SIGSEGV.
Как? Например, вот так:
class Container {
Object value; // (1)
}
// ✨ some magic to get unsafe unsafely ✨
Unsafe unsafe = ....;
long Container_value =
unsafe.objectFieldOffset(Container.class.getDeclaredField("value"));
var container = new Container();
unsafe.getAndSetLong(container, Container_value, Long.MAX_VALUE); // (2)
System.out.println(container.value); // (3)
Здесь мы в поле value (1) вместо Object устанавливаем Long.MAX_VALUE (2) и пытаемся вывести это значение в System.out (3).
Виртуальной машине такой финт ушами не нравится, и она падает на последней строчке, потому что в value лежит мусор, а не настоящая ссылка на объект. И это пример только той ошибки, которую можно легко заметить.
Но зачем такой опасный механизм вообще добавили в Java?
Unsafe родился не как анархист, а как спаситель. Когда-то, во времена до VarHandle и MemorySegment (о них мы поговорим немногим позже), стандартным библиотекам нужно было выполнять низкоуровневые операции: работать с памятью, выполнять атомарные операции или блокировать потоки без потерь производительности.
Но как сделать всё вышеупомянутое, если язык запрещает лезть себе под капот? Правильно, сделать дырку. Именно здесь и появился Unsafe как внутренний инструмент для собственных нужд Java.
Он предназначался только для использования внутри Java и даже имел защиту от получения своего экземпляра извне. Но личное стало публичным, и нашёлся способ получить Unsafe в обход его защиты. Вот так выглядит та самая магия, чтобы получить небезопасность небезопасно:
var Unsafe_theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
Unsafe_theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) Unsafe_theUnsafe.get(null);
Имея на руках этот объект, можно мучить виртуальную машину сколько вздумается.
Только не спешите ругать разработчиков, которые захотели посягнуть на то, что Java всячески пыталась от них спрятать: доступ к Unsafe помог многим библиотекам стать максимально высокопроизводительными. Например, популярная для работы с сетью библиотека Netty была вынуждена использовать Unsafe, и когда вышла замена, перешла на неё.
Но что это за замена?
Она не одна, а целых две:
Эти API не ограничиваются лишь тем, что заменяют какие-то методы в Unsafe. Сами по себе это мощные инструменты для безопасной работы с низкоуровневыми операциями. Например, Foreign Function & Memory API позволяет вызывать нативные функции, минуя тяжёлый JNI!
Небезопасность из Java потихоньку уходит в небытие, оставляя вместо себя новые продуманные и мощные API, позволяющие делать то же самое, не боясь условного SIGSEGV.
Java взрослеет, отбрасывает устаревшие идеи, становится безопаснее, чище и проще. Каждый новый API — это не просто "добавили ещё один модуль", а закрытие старой дыры более элегантным способом.
При этом виртуальная усыпальница выполняет важную культурную функцию: она фиксирует эволюцию идей и решений, напоминая о пройденном пути и формируя контекст, необходимый для осмысленного развития платформы.
0