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

person Adon Smith    schedule 02.02.2015    source источник
comment
Относительно соответствующего устаревшего вопроса Java 6 см. stackoverflow.com/questions/884007/   -  person Raedwald    schedule 03.02.2015
comment
Обратите внимание, что в вашем примере есть ошибка, которая может вызвать потерю данных, потому что вы закрываете потоки не в том порядке, в котором вы их открывали. При закрытии BufferedWriter может потребоваться запись буферизованных данных в базовый поток, который в вашем примере уже закрыт. Избежание этих проблем - еще одно преимущество подходов try-with-resource, показанных в ответах.   -  person Joe23    schedule 06.02.2015


Ответы (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))))) {
    // ...
}

... потому что исключение из _ 8_ (который говорит, что он может выбросить 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.)

person T.J. Crowder    schedule 02.02.2015
comment
+1 за то, что во время построения потоков могут возникать исключения, хотя я отмечу, что на самом деле вы либо получите исключение нехватки памяти, либо что-то столь же серьезное (в этот момент это действительно не имеет значения если вы закрываете свои потоки, потому что ваше приложение вот-вот завершится), или это будет GZIPOutputStream, который выбрасывает IOException; у остальных конструкторов нет проверенных исключений, и нет других обстоятельств, которые могут вызвать исключение времени выполнения. - person Jules; 02.02.2015
comment
@Jules: Да, действительно, для этих конкретных потоков. Это больше о хороших привычках. - person T.J. Crowder; 02.02.2015
comment
Я категорически не согласен с идеей увеличения сложности кода для воображаемой проблемы, когда в этом нет реальной необходимости. Хотя вы продемонстрировали, что есть разница для теоретического случая, на самом деле вы не показали, какая разница будет в этом случае, о котором спрашивает OP. Смотрите мой ответ. - person Peter Lawrey; 04.02.2015
comment
@PeterLawrey: Я категорически не согласен с тем, чтобы использовать вредные привычки или нет, в зависимости от реализации потока. :-) Это не различие между YAGNI / no-YAGNI, это касается шаблонов, которые делают код надежным. - person T.J. Crowder; 04.02.2015
comment
@ T.J.Crowder правда, я имею в виду библиотеку java.io, которая идет вместе с JVM. - person Peter Lawrey; 04.02.2015
comment
@PeterLawrey: Нет ничего выше о том, чтобы не доверять java.io. Некоторые потоки - обобщающие, некоторые ресурсы - выбрасываются из конструкторов. Так что, на мой взгляд, обеспечение того, чтобы несколько ресурсов открывались индивидуально, чтобы их можно было надежно закрыть при последующих выбросах ресурсов, - это просто хорошая привычка. Вы можете выбрать не, если вы не согласны, это нормально. - person T.J. Crowder; 04.02.2015
comment
@PeterLawrey: Готово. Я смягчил то, что вы должны, если вы хотите, чтобы try-with-resources закрыл их, и переключил пример выброса конструктора с OutputStreamWriter (чьи конструкторы не документируют никаких исключений) на GZIPOutputStream (чьи конструкторы do < / я>). - person T.J. Crowder; 04.02.2015
comment
@ T.J.Crowder Разумная квалификация. Учитывая, что код для этих классов был доступен задолго до того, как OpenJDK стал открытым исходным кодом, и они не так уж сложны, поставщику потребовалось бы много творчества / смелости, чтобы создать JVM, которая работала бы значительно иначе. то есть даже DVM для Android такой же, и я не удивлюсь, обнаружив, что код для C # очень похож. Эти классы представляют собой Java-версию того, что вы могли бы сделать на C (но с добавлением использования finalize ()) - person Peter Lawrey; 04.02.2015
comment
@PeterLawrey: Итак, вы выступаете за то, чтобы найти время, чтобы посмотреть на исходный код реализации для чего-то, документирующего исключение, в каждом конкретном случае, а затем сказать: «Ну, на самом деле это не срабатывает, так что ... ... а сохранение нескольких символов набора текста? Мы расстаемся там, по большому счету. :-) Более того, я только что посмотрел, и это не теоретически: конструктор GZIPOutputStream записывает заголовок в поток. А так его можно и выкинуть. Итак, теперь вопрос заключается в том, стоит ли попытаться закрыть поток после записи throw. Ага: Я открыл, надо хотя бы попытаться закрыть. - person T.J. Crowder; 04.02.2015
comment
Вам не нужно «полагаться на реализацию» для любого потока, который расширяет отфильтрованный поток / средство чтения, для которого закрытие обернутого потока является определенным поведением. - person user207421; 14.02.2015
comment
@EJP: Дело в переменных не в закрытии, а в том, что не удалось открыть, и поэтому поток, о котором вы говорите, даже не существует и не имеет возможности закрыть свой основной поток. - person T.J. Crowder; 15.02.2015
comment
Пфф, если мы доверяем конструкторам не бросать, тогда большая часть причин для попытки с ресурсами: проверка на null не потребуется. Создайте переменные для всех потоков, блестящий совет (который я действительно искал). - person Maarten Bodewes; 23.11.2017
comment
Это фантастическая запись - person heez; 31.08.2018

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

person Peter Lawrey    schedule 02.02.2015
comment
См. T.J. Ответ Краудера для ситуаций, когда это могло не сработать. - person TimK; 04.02.2015
comment
@TimK, можете ли вы предоставить пример того, где файл создается, но потом поток не работает, и каковы последствия. Риск отказа чрезвычайно низок, а воздействие незначительно. Не нужно делать более сложное, чем нужно. - person Peter Lawrey; 04.02.2015
comment
@TimK Его ответ на самом деле не говорит, где он может выйти из строя, просто он отличается для другого случая. - person Peter Lawrey; 04.02.2015
comment
GZIPOutputStream(OutputStream) документы IOException и, глядя на источник, фактически пишет заголовок. Так что это не теоретически, что конструктор может выбросить. Вы можете подумать, что оставлять лежащий в основе FileOutputStream открытым после того, как вы написали его, это нормально. Я не. - person T.J. Crowder; 04.02.2015
comment
@ T.J.Crowder Я уже говорил об этом во второй половине своего ответа;) копните немного глубже, и вы поймете, почему вряд ли вам стоит беспокоиться об этом. - person Peter Lawrey; 04.02.2015
comment
@PeterLawrey: Я думаю, что лучше просто написать отдельную строку, чем останавливать то, что я делаю каждый раз, копаться в исходном коде, рыться в том, что он делает, и делать оценку вероятности. - person T.J. Crowder; 04.02.2015
comment
@ T.J.Crowder, это честный подход. В какой-то момент вы должны действительно написать код, чтобы делать то, что должна делать ваша программа. Для среднего разработчика это вполне разумно, и многие из ваших читателей будут средними разработчиками, поэтому это неплохой совет людям, которые не хотят знать, что на самом деле делает код, но я не считаю себя средним разработчиком. ;) - person Peter Lawrey; 04.02.2015
comment
@PeterLawrey: :-) В. вид. - person T.J. Crowder; 04.02.2015
comment
@ T.J.Crowder Я снимаю шляпу со всех, кто является опытным профессиональным разработчиком JavaScript (и других языков). Я не мог этого сделать. ;) - person Peter Lawrey; 04.02.2015
comment
Чтобы вернуться к этому, другая проблема заключается в том, что если вы используете GZIPOutputStream в файле и не вызываете явно finish, он будет вызываться в его закрытой реализации. Это не в попытке ... наконец, поэтому, если завершение / очистка выдает исключение, базовый дескриптор файла никогда не будет закрыт. - person robert_difalco; 31.08.2017
comment
@robert_difalco он закрывается после GC, скорее всего, а может и нет;) - person Peter Lawrey; 31.08.2017
comment
Видеть, что YAGNI используется как предлог, чтобы оставлять ошибки в коде, даже больше пугает, чем удручает. Тем более, когда это неуловимая ошибка, которую вы можете заметить при написании кода, но больше не можете вспомнить, как только ошибка сработает. - person hmijail mourns resignees; 27.11.2020

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

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

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

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

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

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

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

person Grzegorz Żur    schedule 02.02.2015
comment
Это предполагает отсутствие ошибок конструирования потоков, что может быть или не быть верным для перечисленных, но в целом неверно. - person T.J. Crowder; 02.02.2015

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

try (OutputStream outputStream = new FileOutputStream(createdFile)) {
      ...
}
person Dmitry Bychenko    schedule 02.02.2015
comment
Хотя я согласен с вами, вы можете выделить преимущество этого подхода и ответить на вопрос, нужно ли OP закрыть дочерние / внутренние потоки. - person MadProgrammer; 02.02.2015

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

person Codeversum    schedule 02.02.2015
comment
См. Комментарий к ответу Гжегожа Цура. - person T.J. Crowder; 02.02.2015

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

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

person TheLostMind    schedule 02.02.2015

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

person Sivakumar    schedule 02.02.2015