Необходимо ли закрывать каждый вложенный OutputStream и Writer отдельно?

Я пишу кусок кода:

OutputStream outputStream = new FileOutputStream(createdFile);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(gzipOutputStream));

мне нужно закрыть каждый поток или писатель, как показано ниже?

gzipOutputStream.close();
bw.close();
outputStream.close();

или просто закрытие последнего потока будет в порядке?

bw.close();

7 ответов


предполагая, что все потоки будут созданы хорошо, да, просто закрывая bw нормально С этими реализациями потока; но это большое предположение.

Я хотел бы использовать try-with-resources (учебник) так что любые проблемы с построением последующих потоков, которые вызывают исключения, не оставляют предыдущие потоки висящими, и поэтому вам не нужно полагаться на реализацию потока, имеющую вызов для закрытия базового поток:

try (
    OutputStream outputStream = new FileOutputStream(createdFile);
    GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
    OutputStreamWriter osw = new OutputStreamWriter(gzipOutputStream);
    BufferedWriter bw = new BufferedWriter(osw)
    ) {
    // ...
}

обратите внимание, что вы больше не звоните close на всех.

важное замечание: чтобы попробовать с ресурсами закрыть их, вы должны назначьте потоки переменным по мере их открытия, вы не можете использовать вложенность. Если вы используете вложенность, исключение при построении одного из более поздних потоков (скажем,GZIPOutputStream) оставит открытым любой поток, построенный вложенными вызовами внутри него. От JLS §14.20.3:

оператор try-with-resources параметризован с помощью переменные (известные как ресурсы), которые инициализируются перед выполнением try блокируются и закрываются автоматически, в обратном порядке, из которого они были инициализированы, после выполнения try блок.

обратите внимание на слово "переменные" (Курсив мой).

Е. Г., не делай этого:

// DON'T DO THIS
try (BufferedWriter bw = new BufferedWriter(
        new OutputStreamWriter(
        new GZIPOutputStream(
        new FileOutputStream(createdFile))))) {
    // ...
}

...потому что исключение из GZIPOutputStream(OutputStream) конструктор (который говорит, что может бросить IOException и записывает заголовок в базовый поток) оставит FileOutputStream открыть. Поскольку у некоторых ресурсов есть конструкторы, которые могут бросать, а у других нет, хорошая привычка просто перечислять их отдельно.

мы можем дважды проверить нашу интерпретацию этого раздела JLS с помощью этой программы:

public class Example {

    private static class InnerMost implements AutoCloseable {
        public InnerMost() throws Exception {
            System.out.println("Constructing " + this.getClass().getName());
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
        }
    }

    private static class Middle implements AutoCloseable {
        private AutoCloseable c;

        public Middle(AutoCloseable c) {
            System.out.println("Constructing " + this.getClass().getName());
            this.c = c;
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
            c.close();
        }
    }

    private static class OuterMost implements AutoCloseable {
        private AutoCloseable c;

        public OuterMost(AutoCloseable c) throws Exception {
            System.out.println("Constructing " + this.getClass().getName());
            throw new Exception(this.getClass().getName() + " failed");
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
            c.close();
        }
    }

    public static final void main(String[] args) {
        // DON'T DO THIS
        try (OuterMost om = new OuterMost(
                new Middle(
                    new InnerMost()
                    )
                )
            ) {
            System.out.println("In try block");
        }
        catch (Exception e) {
            System.out.println("In catch block");
        }
        finally {
            System.out.println("In finally block");
        }
        System.out.println("At end of main");
    }
}

...который имеет выход:

Constructing Example$InnerMost
Constructing Example$Middle
Constructing Example$OuterMost
In catch block
In finally block
At end of main

обратите внимание, что нет вызовов close там.

если мы фиксируем main:

public static final void main(String[] args) {
    try (
        InnerMost im = new InnerMost();
        Middle m = new Middle(im);
        OuterMost om = new OuterMost(m)
        ) {
        System.out.println("In try block");
    }
    catch (Exception e) {
        System.out.println("In catch block");
    }
    finally {
        System.out.println("In finally block");
    }
    System.out.println("At end of main");
}

тогда мы получим соответствующую close вызовы:

Constructing Example$InnerMost
Constructing Example$Middle
Constructing Example$OuterMost
Example$Middle closed
Example$InnerMost closed
Example$InnerMost closed
In catch block
In finally block
At end of main

(да, два звонка в InnerMost#close правильно, один из Middle, другой из try-with-resources.)


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

try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
                     new GZIPOutputStream(new FileOutputStream(createdFile)))) {
     // write to the buffered writer
}

если вы подписались на YAGNI, или вы-aint-gonna-need-it, вы должны только добавлять код, который вам действительно нужен. Вы не должны добавлять код, который вам может понадобиться, но на самом деле ничего полезного не делает.

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

try (
    OutputStream outputStream = new FileOutputStream(createdFile);
    GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
    OutputStreamWriter osw = new OutputStreamWriter(gzipOutputStream);
    BufferedWriter bw = new BufferedWriter(osw)
    ) {
    // ...
}

давайте начнем с FileOutputStream, который вызывает open чтобы сделать всю реальную работу.

/**
 * Opens a file, with the specified name, for overwriting or appending.
 * @param name name of file to be opened
 * @param append whether the file is to be opened in append mode
 */
private native void open(String name, boolean append)
    throws FileNotFoundException;

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

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

давайте посмотрим на следующий поток GZIPOutputStream

есть код, который может выбросить исключение

private void writeHeader() throws IOException {
    out.write(new byte[] {
                  (byte) GZIP_MAGIC,        // Magic number (short)
                  (byte)(GZIP_MAGIC >> 8),  // Magic number (short)
                  Deflater.DEFLATED,        // Compression method (CM)
                  0,                        // Flags (FLG)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Extra flags (XFLG)
                  0                         // Operating system (OS)
              });
}

это записывает заголовок файла. Теперь было бы очень необычно, чтобы вы могли открыть файл для записи, но не могли записать в него даже 8 байт, но давайте представим, что это может произойти, и мы не закрываем файл после этого. Что происходит с файлом, если он не закрыт?

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

protected void finalize() throws IOException {
    if (fd != null) {
        if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
            flush();
        } else {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
    }
}

если вы вообще не закрываете файл, он все равно закрывается, просто не сразу (и, как я уже сказал, данные, которые остаются в буфере, будут потеряны таким образом, но на данный момент их нет)

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

оба OutputStreamWriter и BufferedWriter не бросают IOException в своих конструкторах, поэтому не ясно, какая проблема они вызовут. В случае BufferedWriter вы можете получить OutOfMemoryError. В этом случае он немедленно запустит GC, который, как мы видели, все равно закроет файл.


Если все потоки были созданы, то закрытие только самого внешнего просто отлично.

документация на Closeable интерфейс утверждает, что метод close:

закрывает этот поток и освобождает все связанные с ним системные ресурсы.

высвобождающие системные ресурсы включают в себя закрывающие потоки.

в нем также говорится, что:

Если поток уже закрыт, то вызов этот метод не имеет эффекта.

поэтому, если вы закроете их явно после этого, ничего плохого не произойдет.


Я бы предпочел использовать try(...) синтаксис (Java 7), например

try (OutputStream outputStream = new FileOutputStream(createdFile)) {
      ...
}

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


нет, самый верхний уровень Stream или reader гарантирует, что все базовый потоки / считыватели закрыты.

Регистрация close() метод реализация вашего потока верхнего уровня.


в Java 7, есть функция try-with-resources. Вам не нужно явно закрывать свои потоки, он позаботится об этом.