Правильный способ закрытия вложенных потоков и писателей в Java

Примечание. Этот вопрос и большинство его ответов датируются до выпуска Java 7. Java 7 предоставляет функцию автоматического управления ресурсами для этого просто. Если вы используете Java 7 или более позднюю версию, вам следует перейти к ответу Росса Джонсона.


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

FileOutputStream fos = new FileOutputStream(...)
BufferedOS bos = new BufferedOS(fos);
ObjectOutputStream oos = new ObjectOutputStream(bos);

Я понимаю, что операция close должна быть застрахована (возможно, с помощью предложения finally). Что меня интересует, так это то, нужно ли явно удостовериться, что вложенные потоки закрыты, или достаточно просто закрыть внешний поток (oos)?

Одна вещь, которую я заметил, по крайней мере, имея дело с этим конкретным примером, состоит в том, что внутренние потоки, кажется, только генерируют FileNotFoundExceptions. Казалось бы, это означает, что технически не нужно беспокоиться о закрытии их, если они потерпят неудачу.

Вот что написал коллега:


Технически, если это было реализовано правильно, закрытия внешнего потока (oos) должно быть достаточно. Но реализация кажется ошибочной.

Пример: BufferedOutputStream наследует close() от FilterOutputStream, который определяет его как:

 155       public void close() throws IOException {
 156           try {
 157             flush();
 158           } catch (IOException ignored) {
 159           }
 160           out.close();
 161       }

Однако, если flush() по какой-то причине вызывает исключение во время выполнения, out.close() никогда не будет вызываться. Так что кажется наиболее безопасным (но некрасивым) беспокоиться о закрытии FOS, при котором файл остается открытым.


Каков наилучший, когда вам абсолютно необходимо быть уверенным, подход к закрытию вложенных потоков?

И есть ли какие-нибудь официальные документы по Java/Sun, которые подробно разбираются с этим?

10 ответов

Решение

Я обычно делаю следующее. Во-первых, определите класс на основе шаблона-метода, чтобы справиться с беспорядком try/catch

import java.io.Closeable;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

public abstract class AutoFileCloser {
    // the core action code that the implementer wants to run
    protected abstract void doWork() throws Throwable;

    // track a list of closeable thingies to close when finished
    private List<Closeable> closeables_ = new LinkedList<Closeable>();

    // give the implementer a way to track things to close
    // assumes this is called in order for nested closeables,
    // inner-most to outer-most
    protected final <T extends Closeable> T autoClose(T closeable) {
            closeables_.add(0, closeable);
            return closeable;
    }

    public AutoFileCloser() {
        // a variable to track a "meaningful" exception, in case
        // a close() throws an exception
        Throwable pending = null;

        try {
            doWork(); // do the real work

        } catch (Throwable throwable) {
            pending = throwable;

        } finally {
            // close the watched streams
            for (Closeable closeable : closeables_) {
                if (closeable != null) {
                    try {
                        closeable.close();
                    } catch (Throwable throwable) {
                        if (pending == null) {
                            pending = throwable;
                        }
                    }
                }
            }

            // if we had a pending exception, rethrow it
            // this is necessary b/c the close can throw an
            // exception, which would remove the pending
            // status of any exception thrown in the try block
            if (pending != null) {
                if (pending instanceof RuntimeException) {
                    throw (RuntimeException) pending;
                } else {
                    throw new RuntimeException(pending);
                }
            }
        }
    }
}

Note the "pending" exception -- this takes care of the case where an exception thrown during close would mask an exception we might really care about.

Наконец, в конце концов сначала пытается закрыться снаружи любого декорированного потока, поэтому, если у вас есть BufferedWriter, обертывающий FileWriter, мы сначала пытаемся закрыть BuffereredWriter, и если это не удается, все равно попробуйте закрыть сам FileWriter. (Note that the definition of Closeable calls for close() to ignore the call if the stream is already closed)

You can use the above class as follows:

try {
    // ...

    new AutoFileCloser() {
        @Override protected void doWork() throws Throwable {
            // declare variables for the readers and "watch" them
            FileReader fileReader = 
                    autoClose(fileReader = new FileReader("somefile"));
            BufferedReader bufferedReader = 
                    autoClose(bufferedReader = new BufferedReader(fileReader));

            // ... do something with bufferedReader

            // if you need more than one reader or writer
            FileWriter fileWriter = 
                    autoClose(fileWriter = new FileWriter("someOtherFile"));
            BufferedWriter bufferedWriter = 
                    autoClose(bufferedWriter = new BufferedWriter(fileWriter));

            // ... do something with bufferedWriter
        }
    };

    // .. other logic, maybe more AutoFileClosers

} catch (RuntimeException e) {
    // report or log the exception
}

Используя этот подход, вам никогда не придется беспокоиться о try/catch / finally, чтобы снова закрывать файлы.

Если это слишком тяжело для вашего использования, по крайней мере подумайте о том, чтобы придерживаться подхода try/catch и подхода "ожидающих" переменных, который он использует.

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

Обратитесь к Java I/O Streams для деталей.

Для решения проблемы

Однако, если flush() по какой-то причине вызывает исключение во время выполнения, out.close() никогда не будет вызываться.

Это не правильно. После того, как вы поймаете и проигнорируете это исключение, выполнение вернется после блока catch иout.close()заявление будет выполнено.

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

В эпоху Java 7 попробовать с ресурсами, безусловно, путь. Как упоминалось в нескольких предыдущих ответах, запрос на закрытие распространяется от самого внешнего потока к самому внутреннему потоку. Так что единственное закрытие - это все, что требуется.

try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f))) {
  // do something with ois
}

Однако есть проблема с этим шаблоном. Try-with-resources не знает о внутреннем FileInputStream, поэтому, если конструктор ObjectInputStream выдает исключение, FileInputStream никогда не закрывается (пока сборщик мусора не доберется до него). Решение...

try (FileInputStream fis = new FileInputStream(f); ObjectInputStream ois = new ObjectInputStream(fis)) {
  // do something with ois
}

Это не так элегантно, но более надежно. Будет ли это на самом деле проблемой, будет зависеть от того, какие исключения могут быть сгенерированы при создании внешнего объекта (объектов). ObjectInputStream может генерировать IOException, который вполне может обрабатываться приложением без завершения. Многие потоковые классы генерируют только непроверенные исключения, что может привести к завершению работы приложения.

Хорошей практикой является использование Apache Commons для обработки объектов, связанных с вводом-выводом.

в finally использование пункта IOUtils

IOUtils.closeQuietly (bWriter); IOUtils.closeQuietly (oWritter);

Фрагмент кода ниже.

BufferedWriter bWriter = null;
OutputStreamWriter oWritter = null;

try {
  oWritter  = new OutputStreamWriter( httpConnection.getOutputStream(), "utf-8" );
  bWriter = new BufferedWriter( oWritter );
  bWriter.write( xml );
}
finally {
  IOUtils.closeQuietly(bWriter);
  IOUtils.closeQuietly(oWritter);
}

Коллега поднимает интересный вопрос, и есть основания для споров в любом случае.

Лично я бы проигнорировал RuntimeException, потому что непроверенное исключение означает ошибку в программе. Если программа неверна, исправьте ее. Вы не можете "обработать" плохую программу во время выполнения.

Это удивительно неловкий вопрос. (Даже если предположить, acquire; try { use; } finally { release; } код правильный.)

Если конструкция декоратора терпит неудачу, вы не будете закрывать основной поток. Поэтому вам необходимо явно закрыть базовый поток, будь то в конечном счете после использования или, что еще сложнее, после успешной передачи ресурса декоратору).

Если исключение вызывает сбой выполнения, вы действительно хотите сбросить?

Некоторые декораторы сами имеют ресурсы. Текущая реализация Sun ZipInputStream например, выделена не куча памяти Java.

Утверждалось, что (IIRC) две трети ресурсов, используемых в библиотеке Java, реализованы явно некорректным образом.

в то время как BufferedOutputStream закрывается даже на IOException от flush, BufferedWriter закрывается правильно.

Мой совет: закройте ресурсы как можно более прямо и не позволяйте им портить другой код. OTOH, вы можете потратить слишком много времени на эту проблему - если OutOfMemoryError это хорошо, но другие аспекты вашей программы, вероятно, имеют более высокий приоритет, и библиотечный код, вероятно, в любом случае нарушается в этой ситуации. Но я бы всегда писал:

final FileOutputStream rawOut = new FileOutputStream(file);
try {
    OutputStream out = new BufferedOutputStream(rawOut);
    ... write stuff out ...
    out.flush();
} finally {
    rawOut.close();
}

(Смотри: не поймай!)

И, возможно, использовать идиому "Выполнить вокруг".

Java SE 7 try-with-resources не упоминается. Это избавляет от необходимости явно делать закрытие полностью, и мне очень нравится идея.

К сожалению, для разработки под Android эта конфета становится доступной только при использовании Android Studio (я думаю) и ориентации на Kitkat и выше.

Также вам не нужно закрывать все вложенные потоки.

проверить это http://ckarthik17.blogspot.com/2011/02/closing-nested-streams.html

Я использую, чтобы закрыть потоки, как это, не вкладывая try-catch в блоки finally

public class StreamTest {

public static void main(String[] args) {

    FileOutputStream fos = null;
    BufferedOutputStream bos = null;
    ObjectOutputStream oos = null;

    try {
        fos = new FileOutputStream(new File("..."));
        bos = new BufferedOutputStream(fos);
        oos = new ObjectOutputStream(bos);
    }
    catch (Exception e) {
    }
    finally {
        Stream.close(oos,bos,fos);
    }
  }   
}

class Stream {

public static void close(AutoCloseable... array) {
    for (AutoCloseable c : array) {
        try {c.close();}
        catch (IOException e) {}
        catch (Exception e) {}
    }
  } 
}

JavaDocs от Sun включают RuntimeException s в их документации, как показано методом read(byte[], int, int) InputStream; задокументировано как выбрасывание исключений NullPointerException и IndexOutOfBoundsException.

Flush () в FilterOutputStream задокументирован только как выбрасывающий IOException, поэтому он на самом деле не выбрасывает RuntimeException s. Любое, что может быть брошено, скорее всего, будет завернуто в IIOException,

Это все еще может бросить Error, но вы мало что можете с этим поделать; Sun рекомендует не пытаться их поймать.

Другие вопросы по тегам