Чи потрібно закривати кожен вкладений 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
Зауважте, що у вашому прикладі є помилка, яка може спричинити втрату даних, оскільки ви закриваєте потоки не в тому порядку, коли ви їх відкрили. Закриваючи a, BufferedWriterможливо, знадобиться записати буферизовані дані в базовий потік, який у вашому прикладі вже закритий. Уникнення цих проблем - ще одна перевага спроб використання ресурсів, що відображаються у відповідях.
Jo23 23

Відповіді:


150

Якщо припустимо, що всі потоки будуть добре створені, так, просто реалізація цих потоківbw прекрасна ; але це велике припущення.

Я б використовував спробу-ресурси ( підручник ), щоб будь-які проблеми, що створювали наступні потоки, які викидають винятки, не залишали попередні потоки висячими, і тому вам не доведеться покладатися на реалізацію потоку, яка вимагає закрити основний потік:

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блоку та закриваються автоматично, у зворотному порядку, з якого вони були ініціалізовані, після виконання 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
Приклад $ Середня закрита
Приклад $ InnerMost закрито
Приклад $ InnerMost закрито
У блоці улову
Нарешті блок
В кінці головного

(Так, два дзвінки до InnerMost#close- це правильно; один - з Middle, інший - з "пробними ресурсами".)


7
+1 за те, що зауважують, що винятки можуть бути кинуті під час побудови потоків, хоча я зазначу, що реально ви збираєтесь отримати виняток із пам'яті або щось настільки ж серйозне (тоді це насправді не має значення якщо ви закриєте потоки, оскільки ваша програма збирається вийти), або GZIPOutputStream видасть IOException; решта конструкторів не мають перевірених винятків, і немає інших обставин, які, ймовірно, можуть спричинити виняток із виконання.
Жуль

5
@Jules: Так, справді для цих конкретних потоків. Йдеться більше про хороші звички.
TJ Crowder

2
@PeterLawrey: Я категорично не згоден із використанням шкідливих звичок або не залежно від реалізації потоку. :-) Це не відмінність від YAGNI / no-YAGNI, це зразки, які створюють надійний код.
TJ Crowder

2
@PeterLawrey: Немає нічого вище, про те, щоб не довіряти java.io. Деякі потоки - узагальнюючі, деякі ресурси - викидають з конструкторів. Тому переконайтеся, що кілька ресурсів відкриваються окремо, щоб їх можна було надійно закрити, якщо подальший викид ресурсів, на мій погляд, є лише хорошою звичкою. Ви можете не робити цього, якщо не погоджуєтесь, це добре.
TJ Crowder

2
@PeterLawrey: Отже, ви виступаєте за те, щоб витратити час на перегляд вихідного коду реалізації для того, щоб щось документувати виняток, у кожному конкретному випадку, а потім сказати: «О, ну, насправді це не кидається, так. .. "та збереження кількох символів набору тексту? Ми розлучаємося з компанією, великий час. :-) Більше того, я просто подивився, і це не теоретично: GZIPOutputStreamконструктор пише заголовку в потік. І так може кинути. Тож тепер позиція полягає в тому, чи я вважаю, що варто турбуватися, щоб спробувати закрити потік після написання кинутої. Так: я відкрив її, я повинен хоча б спробувати її закрити.
TJ Crowder

12

Ви можете закрити зовнішній самий потік, адже вам не потрібно зберігати всі потоки, обгорнуті, і ви можете використовувати пробні ресурси Java 7.

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


1
Дивіться відповідь TJ Crowder у випадках, коли це може вийти з ладу.
TimK

@TimK Ви можете навести приклад того, де файл створюється, але потік пізніше виходить з ладу і який наслідок цього. Ризик відмови вкрай низький, а вплив - тривіальний. Не потрібно робити складніше, ніж це повинно бути.
Пітер Лорі

1
GZIPOutputStream(OutputStream)документи IOExceptionі, дивлячись на джерело, насправді пише заголовок. Так що це не теоретично, що конструктор може кинути. Ви можете відчути, що добре залишити основу FileOutputStreamвідкритою після написання на неї кидків. Я не.
TJ Crowder

1
@TJCrowder Кожен, хто є досвідченим професійним розробником JavaScript (та іншими мовами крім), я знімаю шапку. Я не міг цього зробити. ;)
Пітер Лорі

1
Просто для перегляду цього питання, інша проблема полягає в тому, що якщо ви використовуєте GZIPOutputStream у файлі і не викликаєте явного завершення, він буде викликаний у його близькій реалізації. Це не в спробі ... нарешті, тому, якщо фініш / флеш викидає виняток, тоді нижня ручка файлу ніколи не буде закрита.
robert_difalco

6

Якщо всі потоки були створені миттєво, то закриття лише самого зовнішнього просто добре.

У документації на Closeableінтерфейс зазначено, що метод закриття:

Закриває цей потік і звільняє будь-які системні ресурси, пов'язані з ним.

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

Він також говорить, що:

Якщо потік уже закритий, то виклик цього методу не має ефекту.

Тож якщо явно закрити їх згодом, нічого поганого не станеться.


2
Це не передбачає помилок при побудові потоків, що може бути, а може бути істинним для перелічених, але в цілому не є достовірним.
TJ Crowder

6

Я вважаю за краще використовувати try(...)синтаксис (Java 7), наприклад

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

4
Хоча я погоджуюся з вами, ви, можливо, захочете підкреслити перевагу такого підходу та відповісти на питання, якщо ОП потребує закриття дитини / внутрішніх потоків
MadProgrammer

5

Це буде добре, якщо ви закриєте лише останній потік - закритий дзвінок також буде надісланий до базових потоків.


1
Дивіться коментар до відповіді Гжегожа Юра.
TJ Crowder

5

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

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


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.