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

127

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

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
Для соответствующего устаревшего Java 6 вопроса см stackoverflow.com/questions/884007/...
Raedwald
2
Обратите внимание, что в вашем примере есть ошибка, которая может вызвать потерю данных, потому что вы закрываете потоки не в том порядке, в котором вы их открывали. При закрытии BufferedWriterможет потребоваться записать буферизованные данные в базовый поток, который в вашем примере уже закрыт. Избежание этих проблем является еще одним преимуществом подходов « попробуйте с ресурсами», показанных в ответах.
Joe23

Ответы:

150

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

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

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");
    }
}

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

Пример создания $ InnerMost
Пример построения $ Middle
Пример создания $ OuterMost
В блоке захвата
В конечном блоке
В конце основного

Обратите внимание, что звонков туда 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вызовы:

Пример создания $ InnerMost
Пример построения $ Middle
Пример создания $ OuterMost
Пример $ Средний закрыт
Пример $ InnerСамый закрытый
Пример $ InnerСамый закрытый
В блоке захвата
В конечном блоке
В конце основного

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

TJ Crowder
источник
7
+1 за то, что при построении потоков могут возникать исключения, хотя я отмечу, что на самом деле вы либо получите исключение нехватки памяти, либо что-то не менее серьезное (в этот момент это действительно не имеет значения если вы закрываете свои потоки, потому что ваше приложение вот-вот завершится), или это будет GZIPOutputStream, который выбрасывает IOException; у остальных конструкторов нет проверенных исключений, и нет других обстоятельств, которые могли бы вызвать исключение времени выполнения.
Жюль
5
@Jules: Да, действительно, для этих конкретных потоков. Это больше о хороших привычках.
TJ Crowder
2
@PeterLawrey: Я категорически не согласен с тем, чтобы использовать вредные привычки или нет, в зависимости от реализации потока. :-) Это не различие между YAGNI / no-YAGNI, это касается шаблонов, которые делают код надежным.
TJ Crowder
2
@PeterLawrey: Нет ничего выше о недоверии java.io. Некоторые потоки - обобщающие, некоторые ресурсы - выбрасываются из конструкторов. Так что, на мой взгляд, обеспечение того, чтобы несколько ресурсов открывались индивидуально, чтобы их можно было надежно закрыть при последующих выбросах ресурсов, - это просто хорошая привычка. Вы можете не делать этого, если не согласны, это нормально.
TJ Crowder
2
@PeterLawrey: Итак, вы выступаете за то, чтобы найти время, чтобы посмотреть на исходный код реализации чего-то, документирующего исключение, в каждом конкретном случае, а затем сказать: «О, ну, на самом деле это не срабатывает, так что. .. "и сэкономить несколько символов набора? Мы расстаемся там, большое время. :-) Более того, я только что посмотрел, и это не теоретически: GZIPOutputStreamконструктор записывает заголовок в поток. А так его можно и выкинуть. Итак, теперь вопрос в том, стоит ли, по-моему, попытаться закрыть поток после записи throw. Ага: Я открыл, надо хотя бы попытаться закрыть.
TJ Crowder
12

Вы можете закрыть самый внешний поток, на самом деле вам не нужно сохранять все потоки в оболочке, и вы можете использовать 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. В этом случае он немедленно запустит сборщик мусора, который, как мы видели, в любом случае закроет файл.

Питер Лоури
источник
1
См. Ответ TJ Crowder для ситуаций, когда это может не сработать.
TimK
@TimK, можете ли вы предоставить пример того, где файл создается, но поток позже не работает, и каковы последствия. Риск отказа чрезвычайно низок, а воздействие незначительно. Не нужно делать более сложное, чем нужно.
Питер Лоури
1
GZIPOutputStream(OutputStream)документы IOExceptionи, глядя на источник, фактически пишет заголовок. Так что это не теоретически, что конструктор может выбросить. Вы можете подумать, что оставлять базовый объект FileOutputStreamоткрытым после того, как написали в него, это нормально . Я не.
TJ Crowder
1
@TJCrowder Я снимаю шляпу со всех, кто является опытным профессиональным разработчиком JavaScript (и других языков). Я не мог этого сделать. ;)
Питер Лоури
1
Чтобы вернуться к этому, другая проблема заключается в том, что если вы используете GZIPOutputStream в файле и не вызываете явно finish, он будет вызываться в его закрытой реализации. Это не в попытке ... наконец, поэтому, если завершение / очистка выдает исключение, базовый дескриптор файла никогда не будет закрыт.
robert_difalco
6

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

В документации по Closeableинтерфейсу указан метод закрытия:

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

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

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

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

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

Гжегож Дюр
источник
2
Это предполагает отсутствие ошибок при построении потоков, что может быть или не быть верным для перечисленных, но в целом не является достоверным.
TJ Crowder
6

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

try (OutputStream outputStream = new FileOutputStream(createdFile)) {
      ...
}
Дмитрий Быченко
источник
4
Хотя я согласен с вами, вы можете выделить преимущество этого подхода и ответить на вопрос, нужно ли OP закрыть дочерние / внутренние потоки
MadProgrammer
5

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

Codeversum
источник
1
См. Комментарий к ответу Гжегожа Цура.
TJ Crowder
5

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

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

TheLostMind
источник