Почему finalize() устарел и почему это вообще было плохой идеей?
Если вы давно работаете с Java, наверняка встречали метод:
@Override
protected void finalize() throws Throwable {
// cleanup
}
И когда-то его действительно использовали для освобождения ресурсов, но сегодня это считается плохой практикой.
Более того, finalize() deprecated с Java 9, а начиная с Java 18 его использование особенно явно рассматривается как устаревший механизм.
Но почему?
Проблема №1. Мы не знаем, когда он будет вызван
Например:
class Resource {
@Override
protected void finalize() {
closeConnection();
}
}
Объект стал недостижим.
Можно подумать:
"Ну всё, GC его удалит и вызовет finalize()."
Но когда именно?
Через секунду?
Через минуту?
Через час?
И будет ли вызван вообще?
Никаких гарантий по времени нет.
Поэтому finalize() нельзя использовать для управления ресурсами, которые нужно освобождать вовремя.
Проблема №2. finalize() может вообще не выполниться
Например, JVM может завершиться раньше, чем объект будет финализирован.
Поэтому:
finalize()
не является надежным механизмом: "объект уничтожен → ресурс освобожден"
Проблема №3. Объект можно "воскресить"
Вот это уже интереснее.
class Zombie {
static Zombie instance;
@Override
protected void finalize() {
instance = this;
}
}
Объект должен быть удален, но во время finalize() он записывает this в статическое поле.
Теперь снова существует сильная ссылка:
GC
↓
объект
↓
finalize()
↓
Zombie.instance
↓
объект снова жив
Объект фактически воскрес.
Правда, второй раз finalize() для этого объекта JVM вызывать не обязана. Именно поэтому механизм настолько неприятный.
Проблема №4. finalize() может тормозить GC
Объекты с finalize() нельзя просто обработать как обычный мусор.
JVM должна дополнительно учитывать их финализацию. В результате сборка мусора становится сложнее, а освобождение памяти может откладываться.
А чем пользоваться вместо этого? Если ресурс должен быть освобожден явно:
try (InputStream input = ...) {
// работа
}
Используем: try-with-resources + AutoCloseable
class MyResource implements AutoCloseable {
@Override
public void close() {
// освобождение ресурса
}
}
И тогда Java гарантирует вызов close() при выходе из try.
Главное finalize() пытался решить проблему:
"Что делать, когда объект больше не нужен?"
Но GC отвечает только за память. Он не знает, когда вам нужно закрыть:
- файл;
- сокет;
- соединение с БД;
- native resource;
- другой внешний ресурс.
Поэтому правило простое:
GC управляет памятью. Разработчик должен явно управлять внешними ресурсами.
Именно поэтому finalize() оказался плохим архитектурным решением и в современных версиях Java от него постепенно отказались, а если на собеседовании вас спросят:
"Может ли finalize() гарантированно выполниться перед уничтожением объекта?"
Правильный ответ:
Нет.