Руководство по завершению метода в Java

Руководство по завершению метода в Java

1. обзор

В этом руководстве мы сосредоточимся на основном аспекте языка Java - методеfinalize, предоставляемом корневым классомObject.

Проще говоря, это вызывается перед сборкой мусора для конкретного объекта.

2. Использование финализаторов

Методfinalize() называется финализатором.

Финализаторы вызываются, когда JVM выясняет, что этот конкретный экземпляр следует собирать мусором. Такой финализатор может выполнять любые операции, в том числе возвращать объект к жизни.

Однако основная цель финализатора - освободить ресурсы, используемые объектами, до того, как они будут удалены из памяти. Финализатор может работать в качестве основного механизма для операций очистки или в качестве защитной сети при сбое других методов.

Чтобы понять, как работает финализатор, давайте взглянем на объявление класса:

public class Finalizable {
    private BufferedReader reader;

    public Finalizable() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        this.reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    // other class members
}

КлассFinalizable имеет полеreader, которое ссылается на закрываемый ресурс. Когда объект создается из этого класса, он создает новый экземплярBufferedReader, читающий из файла в пути к классам.

Такой экземпляр используется в методеreadFirstLine для извлечения первой строки в данном файле. Notice that the reader isn’t closed in the given code.

Мы можем сделать это с помощью финализатора:

@Override
public void finalize() {
    try {
        reader.close();
        System.out.println("Closed BufferedReader in the finalizer");
    } catch (IOException e) {
        // ...
    }
}

Легко видеть, что финализатор объявляется так же, как любой обычный метод экземпляра.

В действительностиthe time at which the garbage collector calls finalizers is dependent on the JVM’s implementation and the system’s conditions, which are out of our control.

Чтобы сборка мусора происходила на месте, воспользуемся методомSystem.gc. В реальных системах мы никогда не должны вызывать это явно по ряду причин:

  1. Это дорого

  2. Сборка мусора запускается не сразу - это просто подсказка JVM для запуска сборки мусора.

  3. JVM лучше знает, когда нужно вызвать GC

Если нам нужно принудительно выполнить сборку мусора, мы можем использовать для этогоjconsole.

Ниже приведен тестовый пример, демонстрирующий работу финализатора:

@Test
public void whenGC_thenFinalizerExecuted() throws IOException {
    String firstLine = new Finalizable().readFirstLine();
    assertEquals("example.com", firstLine);
    System.gc();
}

В первом операторе создается объектFinalizable, затем вызывается его методreadFirstLine. Этот объект не привязан к какой-либо переменной, поэтому он имеет право на сборку мусора при вызове методаSystem.gc.

Утверждение в тесте проверяет содержимое входного файла и используется только для того, чтобы доказать, что наш пользовательский класс работает, как ожидалось.

Когда мы запустим предоставленный тест, на консоли будет напечатано сообщение о том, что буферизованный ридер закрыт в финализаторе. Это означает, что был вызван методfinalize, и он очистил ресурс.

До этого момента финализаторы выглядели как отличный способ для операций предварительного уничтожения. Однако это не совсем так.

В следующем разделе мы увидим, почему их следует избегать.

3. Избегайте финализаторов

Давайте посмотрим на несколько проблем, с которыми мы столкнемся при использовании финализаторов для выполнения критических действий.

Первой заметной проблемой, связанной с финализаторами, является отсутствие оперативности. Мы не можем знать, когда выполняется финализатор, так как сборка мусора может произойти в любое время.

Само по себе это не проблема, потому что самое главное, что финализатор все еще вызывается рано или поздно. Однако системные ресурсы ограничены. Thus, we may run out of those resources before they get a chance to be cleaned up, potentially resulting in system crashes.с

Финализаторы также влияют на переносимость программы. Поскольку алгоритм сборки мусора зависит от реализации JVM, программа может очень хорошо работать в одной системе, но вести себя по-разному во время выполнения в другой.

Еще одна важная проблема, связанная с финализаторами, - это производительность. В частности,JVM must perform much more operations when constructing and destroying objects containing a non-empty finalizer.

Детали зависят от реализации, но общие идеи одинаковы для всех JVM: необходимо выполнить дополнительные шаги, чтобы гарантировать выполнение финализаторов до того, как объекты будут отброшены. Эти шаги могут увеличить продолжительность создания и уничтожения объекта в сотни или даже тысячи раз.

Последняя проблема, о которой мы будем говорить, - это отсутствие обработки исключений во время финализации. If a finalizer throws an exception, the finalization process is canceled, and the exception is ignored, leaving the object in a corrupted state без уведомления.

4. Пример без финализатора

Давайте рассмотрим решение, обеспечивающее ту же функциональность, но без использования методаfinalize(). Обратите внимание, что приведенный ниже пример - не единственный способ заменить финализаторы.

Вместо этого он используется, чтобы продемонстрировать важный момент: всегда есть варианты, которые помогают избежать финализаторов.

Вот объявление нашего нового класса:

public class CloseableResource implements AutoCloseable {
    private BufferedReader reader;

    public CloseableResource() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    @Override
    public void close() {
        try {
            reader.close();
            System.out.println("Closed BufferedReader in the close method");
        } catch (IOException e) {
            // handle exception
        }
    }
}

Нетрудно заметить, что единственное различие между новым классомCloseableResource и нашим предыдущим классомFinalizable - это реализация интерфейсаAutoCloseable вместо определения финализатора.

Обратите внимание, что тело методаclose дляCloseableResource почти такое же, как тело финализатора в классеFinalizable.

Ниже приведен метод тестирования, который считывает входной файл и освобождает ресурс после завершения своей работы:

@Test
public void whenTryWResourcesExits_thenResourceClosed() throws IOException {
    try (CloseableResource resource = new CloseableResource()) {
        String firstLine = resource.readFirstLine();
        assertEquals("example.com", firstLine);
    }
}

В приведенном выше тесте экземплярCloseableResource создается в блокеtry оператора try-with-resources, поэтому этот ресурс автоматически закрывается, когда блок try-with-resources завершает выполнение.

Запустив данный тестовый метод, мы увидим сообщение, распечатанное из методаclose классаCloseableResource.

5. Заключение

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

Следует отметить один важный момент:finalize устарел, начиная с Java 9, и в конечном итоге будет удален.

Как всегда, исходный код этого руководства находится вover on GitHub.