Нужно ли закрывать каждый вложенный 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();

Ответ 1

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

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

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.)

Ответ 2

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

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

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

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

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, который, как мы видели, все равно закроет файл.

Ответ 3

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

Документация на Closeable указывает, что метод close:

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

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

В нем также указано, что:

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

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

Ответ 4

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

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

Ответ 5

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

Ответ 6

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

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

Ответ 7

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