Статьи

Общая идиома «попробуй с ресурсами» не уберется должным образом

Поэтому я перечитывал некоторые блоги об изменениях языка JDK 7 и заметил, что следующая идиома кажется популярной :

//
try (BufferedReader br = new BufferedReader(
        new FileReader("/tmp/click.xml"))) {
   System.out.println(br.readLine());
}

Это кажется все хорошо и хорошо; но подумайте, что произойдет, если что-то пойдет не так при создании BufferedReader. Контракт для try-with-resources будет пытаться закрыть и привести в порядок объявленное поле. Если что-то пойдет не так, FileReader никогда не будет закрыт.

Итак, рассмотрим этот пример, где конструктор BufferedReader выдает ошибку, это может быть OutOfMemoryError или любое количество других режимов сбоя.

//
package client;

import java.io.*;

public class BufferedFileExample {

    public static void main(String[] args) throws FileNotFoundException, 
                                                  IOException {

        try (BufferedReader br = new MyBufferedReader(
            new MyFileReader("/tmp/click.xml"))) {
            System.out.println(br.readLine());
        }
    }
    
    public static class MyFileReader extends FileReader 
    {

        public MyFileReader(String in) throws FileNotFoundException {
            super(in);
        }
        
        public void close() throws IOException {
            super.close();
            System.out.println("Close called");
        }
    }
    
    
    
    public static class MyBufferedReader extends BufferedReader 
    {

        public MyBufferedReader(Reader in) {
            super(in);
            throw new Error();
        }
    }
}

Это приводит нас к следующему выводу, и поэтому закрытие не выполняется в FileReader, который вызывается до финализатора. Вы застряли, пока gc не проснется в будущем, чтобы привести в порядок.

Exception in thread "main" java.lang.Error
 at client.BufferedFileExample$MyBufferedReader.(BufferedFileExample.java:38)
 at client.BufferedFileExample.main(BufferedFileExample.java:12)
Process exited with exit code 1.

К счастью, это легко исправить, изменение языка поддерживает несколько записей в инструкции try, поэтому вы можете изменить этот код так:

//
try (FileReader fr = new MyFileReader("/tmp/click.xml");
     BufferedReader br = new MyBufferedReader(
        fr)) {
   System.out.println(br.readLine());
}

Поэтому, если вы запустите код с указанной выше модификацией, вы обнаружите, что close теперь называется:

Close called
Exception in thread "main" java.lang.Error
 at client.BufferedFileExample$MyBufferedReader.(BufferedFileExample.java:38)
 at client.BufferedFileExample.main(BufferedFileExample.java:12)
Process exited with exit code 1.

Итак, проблема решена, но, к сожалению, здесь есть еще один недостаток: если мы удалим код, который выдает ошибку, то увидим следующий вывод:

Close called
Close called
Process exited with exit code 0.

Так что это хорошо для классов, которые реализуют существующий
интерфейс
java.io.Closeble, потому что вызов close во второй раз подойдет; но более
общий интерфейс, который используется для других закрываемых ресурсов, к сожалению, не имеет этого ограничения, поэтому процитируйте спецификацию


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

Таким образом, чтобы использовать идиому split-try-with-resources, сначала нужно убедиться, что у ресурсов есть идемпотентный метод close. Я подозреваю, что большинство API сделает это; но вам нужно сначала проверить и, возможно, вам нужно еще больше обернуть объекты, чтобы это было так.

Последний момент в try-with-resource — обработка исключений, когда закрытие завершается неудачно. JDK 7 представил концепцию подавленных исключений, поэтому вы заставляете разделенную версию кода генерировать исключение как в конструкторе, так и в MyBufferedReader, а в конце MyFileReader вы увидите следующую трассировку нового стека.

Close called
Exception in thread "main" java.lang.Error
 at client.BufferedFileExample$MyBufferedReader.(BufferedFileExample.java:38)
 at client.BufferedFileExample.main(BufferedFileExample.java:12)
 Suppressed: java.lang.Error: close
  at client.BufferedFileExample$MyFileReader.close(BufferedFileExample.java:27)
  at client.BufferedFileExample.main(BufferedFileExample.java:14)
Process exited with exit code 1.

Возможно, это не то, что вы называете кодом, ожидаемое — конечно, оно может немного отличаться от того, что будет производить код try / finally, который вы заменяете, стоит знать, что исключения обрабатываются немного по-другому. Например, если есть только ошибка, выдаваемая во время метода close, будет отображаться эта нечетная трассировка самоссылочного стека.

Close called
Close called
Exception in thread "main" java.lang.Error: close
 at client.BufferedFileExample$MyFileReader.close(BufferedFileExample.java:28)
 at java.io.BufferedReader.close(BufferedReader.java:517)
 at client.BufferedFileExample.main(BufferedFileExample.java:15)
 Suppressed: java.lang.Error: close
  at client.BufferedFileExample$MyFileReader.close(BufferedFileExample.java:28)
  ... 1 more
Process exited with exit code 1.

Таким образом, урок здесь заключается в том, что влияние некоторых новых конструкций JDK 7 будет немного более тонким, чем мы могли ожидать. Большое спасибо моему уважаемому коллеге Марку Уорнеру за то, что он рассказал мне об этом.