Có nhất thiết phải đóng riêng từng OutputStream và Writer không?


127

Tôi đang viết một đoạn mã:

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

Tôi có cần phải đóng mọi luồng hoặc nhà văn như sau không?

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

Hoặc sẽ chỉ đóng luồng cuối cùng là tốt?

bw.close();

1
Đối với câu hỏi Java 6 lỗi thời tương ứng, hãy xem stackoverflow.com/questions/884007/NH
Raedwald

2
Lưu ý rằng ví dụ của bạn có một lỗi có thể gây mất dữ liệu, vì bạn đang đóng các luồng không theo thứ tự bạn đã mở chúng. Khi đóng, BufferedWriternó có thể cần ghi dữ liệu đệm vào luồng bên dưới, trong ví dụ của bạn đã bị đóng. Tránh những vấn đề này là một lợi thế khác của phương pháp thử tài nguyên được thể hiện trong các câu trả lời.
Joe23

Câu trả lời:


150

Giả sử tất cả các luồng được tạo đều ổn, vâng, chỉ cần đóng bwlà ổn với các triển khai luồng đó ; nhưng đó là một giả định lớn.

Tôi đã sử dụng tài nguyên thử ( hướng dẫn ) để mọi vấn đề khi xây dựng các luồng tiếp theo ném ngoại lệ không để các luồng trước đó bị treo và do đó bạn không phải phụ thuộc vào việc triển khai luồng có lệnh gọi đóng luồng cơ bản:

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

Lưu ý bạn không còn gọi nữa close.

Lưu ý quan trọng : Để có tài nguyên thử với chúng, bạn phải gán các luồng cho các biến khi bạn mở chúng, bạn không thể sử dụng lồng nhau. Nếu bạn sử dụng lồng nhau, một ngoại lệ trong quá trình xây dựng một trong các luồng sau (giả sử GZIPOutputStream) sẽ để lại bất kỳ luồng nào được tạo bởi các lệnh gọi lồng nhau bên trong nó. Từ JLS §14.20.3 :

Một câu lệnh try-with-resource được tham số hóa với các biến (được gọi là tài nguyên) được khởi tạo trước khi thực hiện trykhối và tự động đóng, theo thứ tự ngược lại từ đó chúng được khởi tạo, sau khi thực hiện trykhối.

Lưu ý từ "biến" (nhấn mạnh của tôi) .

Ví dụ, đừng làm điều này:

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

... bởi vì một ngoại lệ từ hàm GZIPOutputStream(OutputStream)tạo (có nghĩa là nó có thể ném IOExceptionvà viết tiêu đề vào luồng bên dưới) sẽ bỏ FileOutputStreamngỏ. Vì một số tài nguyên có các hàm tạo có thể ném còn các tài nguyên khác thì không, nên chỉ liệt kê riêng chúng.

Chúng tôi có thể kiểm tra lại cách diễn giải của chúng tôi về phần JLS đó với chương trình này:

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

... có đầu ra:

Xây dựng ví dụ $ InternalMost
Xây dựng ví dụ $ Trung
Xây dựng ví dụ $ OuterMost
Trong khối bắt
Trong khối cuối cùng
Cuối chính

Lưu ý rằng không có cuộc gọi đến closeđó.

Nếu chúng tôi sửa chữa 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");
}

sau đó chúng tôi nhận được các closecuộc gọi thích hợp :

Xây dựng ví dụ $ InternalMost
Xây dựng ví dụ $ Trung
Xây dựng ví dụ $ OuterMost
Ví dụ $ Trung đóng
Ví dụ $ InsideMost đã đóng
Ví dụ $ InsideMost đã đóng
Trong khối bắt
Trong khối cuối cùng
Cuối chính

(Có, hai cuộc gọi đến InnerMost#closelà chính xác; một cuộc gọi đến từ cuộc gọi Middlethử với tài nguyên.)


7
+1 để lưu ý rằng các ngoại lệ có thể được đưa ra trong quá trình xây dựng các luồng, mặc dù tôi sẽ lưu ý rằng thực tế bạn sẽ có một ngoại lệ ngoài bộ nhớ hoặc một cái gì đó không kém phần nghiêm trọng (tại thời điểm đó thực sự không quan trọng nếu bạn đóng luồng của mình, vì ứng dụng của bạn sắp thoát ra) hoặc đó sẽ là GZIPOutputStream ném IOException; phần còn lại của các hàm tạo không có ngoại lệ được kiểm tra và không có trường hợp nào khác có khả năng tạo ra ngoại lệ thời gian chạy.
Jules

5
@Jules: Vâng, đối với những luồng cụ thể này, thực sự. Đó là nhiều hơn về những thói quen tốt.
TJ Crowder

2
@PeterLawrey: Tôi hoàn toàn không đồng ý với việc sử dụng các thói quen xấu hay không phụ thuộc vào việc thực hiện luồng. :-) Đây không phải là điểm khác biệt của YAGNI / no-YAGNI, đó là về các mẫu tạo mã đáng tin cậy.
TJ Crowder

2
@PeterLawrey: Không có gì ở trên về việc không tin tưởng java.iocả. Một số luồng - khái quát hóa, một số tài nguyên - ném từ các nhà xây dựng. Vì vậy, đảm bảo nhiều tài nguyên được mở riêng lẻ để chúng có thể được đóng một cách đáng tin cậy nếu một tài nguyên tiếp theo ném chỉ là một thói quen tốt, theo quan điểm của tôi. Bạn có thể chọn không làm điều đó nếu bạn không đồng ý, điều đó tốt.
TJ Crowder

2
@PeterLawrey: Vì vậy, bạn ủng hộ việc dành thời gian để xem mã nguồn của một triển khai cho một cái gì đó ghi lại một ngoại lệ, trên cơ sở từng trường hợp, và sau đó nói "Ồ, tốt, nó không thực sự ném, vì vậy. .. "và lưu một vài ký tự gõ? Chúng tôi một phần công ty ở đó, thời gian lớn. :-) Hơn nữa, tôi chỉ nhìn, và đây không phải là lý thuyết: nhà GZIPOutputStreamxây dựng của nó viết một tiêu đề cho luồng. Và vì vậy nó có thể ném. Vì vậy, bây giờ vị trí là liệu tôi có đáng bận tâm để cố gắng đóng luồng sau khi viết không. Vâng: Tôi đã mở nó, ít nhất tôi nên cố gắng đóng nó.
TJ Crowder

12

Bạn có thể đóng luồng ngoài cùng, trên thực tế, bạn không cần giữ lại tất cả các luồng được gói và bạn có thể sử dụng thử tài nguyên Java 7.

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

Nếu bạn đăng ký YAGNI hoặc bạn không cần nó, bạn chỉ nên thêm mã bạn thực sự cần. Bạn không nên thêm mã mà bạn tưởng tượng bạn có thể cần nhưng thực tế không làm gì hữu ích.

Lấy ví dụ này và tưởng tượng những gì có thể xảy ra nếu bạn không làm điều này và tác động sẽ là gì?

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

Hãy bắt đầu với FileOutputStream để gọi opentất cả công việc thực sự.

/**
 * 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;

Nếu không tìm thấy tệp, không có tài nguyên cơ bản nào để đóng, vì vậy việc đóng tệp sẽ không tạo ra bất kỳ sự khác biệt nào. Nếu tệp tồn tại, nó sẽ ném FileNotFoundException. Vì vậy, không có gì đạt được bằng cách cố gắng đóng tài nguyên từ dòng này một mình.

Lý do bạn cần đóng tệp là khi tệp được mở thành công, nhưng sau đó bạn gặp lỗi.

Hãy nhìn vào luồng tiếp theo GZIPOutputStream

Có mã có thể ném ngoại lệ

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

Điều này viết tiêu đề của tập tin. Bây giờ sẽ rất bất thường khi bạn có thể mở một tệp để viết nhưng không thể ghi thậm chí 8 byte vào nó, nhưng hãy tưởng tượng điều này có thể xảy ra và chúng tôi không đóng tệp sau đó. Điều gì xảy ra với một tập tin nếu nó không được đóng lại?

Bạn không nhận được bất kỳ ghi không rõ ràng nào, chúng bị loại bỏ và trong trường hợp này, không có byte nào được ghi thành công vào luồng không được đệm vào thời điểm này. Nhưng một tệp không được đóng sẽ không tồn tại mãi mãi, thay vào đó FileOutputStream có

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

Nếu bạn hoàn toàn không đóng một tệp, dù sao nó cũng bị đóng, nhưng không phải ngay lập tức (và như tôi đã nói, dữ liệu còn lại trong bộ đệm sẽ bị mất theo cách này, nhưng không có gì tại thời điểm này)

Hậu quả của việc không đóng tập tin ngay lập tức là gì? Trong điều kiện bình thường, bạn có khả năng mất một số dữ liệu và bạn có khả năng hết các mô tả tệp. Nhưng nếu bạn có một hệ thống nơi bạn có thể tạo tệp nhưng bạn không thể viết bất cứ điều gì cho họ, bạn sẽ gặp vấn đề lớn hơn. tức là khó có thể tưởng tượng được tại sao bạn liên tục cố gắng tạo tập tin này mặc dù thực tế bạn đang thất bại.

Cả OutputStreamWriter và BufferedWriter đều không ném IOException vào các hàm tạo của chúng, vì vậy không rõ chúng sẽ gây ra vấn đề gì. Trong trường hợp của BufferedWriter, bạn có thể nhận được OutOfMemoryError. Trong trường hợp này, nó sẽ ngay lập tức kích hoạt một GC, như chúng ta đã thấy sẽ đóng tệp bằng mọi cách.


1
Xem câu trả lời của TJ Crowder cho các tình huống có thể thất bại.
TimK

@TimK bạn có thể cung cấp một ví dụ về nơi tệp được tạo nhưng luồng sau đó không thành công và hậu quả là gì. Nguy cơ thất bại là cực kỳ thấp và tác động là không đáng kể. Không cần phải làm cho phức tạp hơn nó cần phải được.
Peter Lawrey 4/2/2015

1
GZIPOutputStream(OutputStream)tài liệu IOExceptionvà, nhìn vào nguồn, thực sự viết một tiêu đề. Vì vậy, nó không phải là lý thuyết, mà nhà xây dựng có thể ném. Bạn có thể cảm thấy ổn khi để FileOutputStreammở bên dưới sau khi viết cho nó ném. Tôi không.
TJ Crowder

1
@TJCrowder Bất kỳ ai là nhà phát triển JavaScript chuyên nghiệp có kinh nghiệm (và các ngôn ngữ khác bên cạnh) tôi đều ngả mũ. Tôi không thể làm điều đó. ;)
Peter Lawrey 4/2/2015

1
Chỉ cần xem lại điều này, vấn đề khác là nếu bạn đang sử dụng GZIPOutputStream trên một tệp và không gọi kết thúc một cách rõ ràng thì nó sẽ được gọi trong triển khai chặt chẽ. Đây không phải là một thử ... cuối cùng vì vậy nếu kết thúc / tuôn ra một ngoại lệ thì xử lý tệp bên dưới sẽ không bao giờ bị đóng.
robert_difalco

6

Nếu tất cả các luồng đã được khởi tạo thì chỉ đóng ngoài cùng là ổn.

Các tài liệu về Closeablegiao diện nói rằng phương thức đóng:

Đóng luồng này và giải phóng bất kỳ tài nguyên hệ thống nào được liên kết với nó.

Các tài nguyên hệ thống phát hành bao gồm các luồng đóng.

Nó cũng nói rằng:

Nếu luồng đã bị đóng thì việc gọi phương thức này không có hiệu lực.

Vì vậy, nếu bạn đóng chúng một cách rõ ràng sau đó, sẽ không có gì sai xảy ra.


2
Điều này giả định rằng không có lỗi khi xây dựng các luồng, điều này có thể đúng hoặc không đúng với các luồng được liệt kê, nhưng nói chung là không đáng tin cậy.
TJ Crowder

6

Tôi muốn sử dụng try(...)cú pháp (Java 7), vd

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

4
Mặc dù tôi đồng ý với bạn, bạn có thể muốn nêu bật lợi ích của phương pháp này và trả lời điểm nếu OP cần đóng luồng con / luồng bên trong
MadProgrammer 2/215

5

Sẽ ổn thôi nếu bạn chỉ đóng luồng cuối cùng - cuộc gọi gần cũng sẽ được gửi đến các luồng bên dưới.


1
Xem bình luận về câu trả lời của Grzegorz.
TJ Crowder

5

Không, mức cao nhất Streamhoặc readersẽ đảm bảo rằng tất cả các luồng / trình đọc cơ bản được đóng lại.

Kiểm tra việc thực hiệnclose() phương pháp của luồng cấp cao nhất của bạn.


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.