จำเป็นต้องปิด 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 ( บทช่วยสอน ) เพื่อให้ปัญหาใด ๆ ในการสร้างสตรีมที่ตามมาซึ่งมีข้อยกเว้นจะไม่ทำให้สตรีมก่อนหน้านี้หยุดทำงานดังนั้นคุณจึงไม่ต้องพึ่งพาการใช้งานสตรีมที่มีการเรียกให้ปิด สตรีมพื้นฐาน:

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

โปรดทราบว่าคุณไม่ได้โทรcloseเลย

หมายเหตุสำคัญ : หากต้องการปิด try-with-resources คุณต้องกำหนดสตรีมให้กับตัวแปรเมื่อคุณเปิดคุณไม่สามารถใช้การซ้อนกันได้ หากคุณใช้การซ้อนข้อยกเว้นในระหว่างการสร้างสตรีมในภายหลัง (พูด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
ในบล็อกจับ
ในที่สุดบล็อก
ในตอนท้ายของ 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สายที่เหมาะสม:

การสร้างตัวอย่าง $ InnerMost
การสร้างตัวอย่าง $ Middle
การสร้างตัวอย่าง $ OuterMost
ตัวอย่าง $ กลางปิด
ตัวอย่าง $ InnerMost ปิด
ตัวอย่าง $ InnerMost ปิด
ในบล็อกจับ
ในที่สุดบล็อก
ในตอนท้ายของ main

(ใช่สองการเรียกไปที่InnerMost#closeถูกต้องหนึ่งมาจากอีกสายหนึ่งมาจากMiddleการลองใช้กับทรัพยากร)


7
+1 สำหรับการสังเกตว่าอาจมีข้อยกเว้นในระหว่างการสร้างสตรีมแม้ว่าฉันจะทราบว่าในความเป็นจริงคุณจะได้รับข้อยกเว้นหน่วยความจำไม่เพียงพอหรือสิ่งที่ร้ายแรงพอ ๆ กัน (ซึ่งในจุดนี้มันไม่สำคัญจริงๆ หากคุณปิดสตรีมเนื่องจากแอปพลิเคชันของคุณกำลังจะออก) หรือจะเป็น GZIPOutputStream ที่พ่น IOException ตัวสร้างที่เหลือไม่มีข้อยกเว้นที่ตรวจสอบได้และไม่มีสถานการณ์อื่นใดที่มีแนวโน้มที่จะทำให้เกิดข้อยกเว้นรันไทม์
Jules

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-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 ทันทีซึ่งตามที่เราเห็นจะปิดไฟล์อยู่ดี


1
ดูคำตอบของ TJ Crowder สำหรับสถานการณ์ที่อาจล้มเหลว
TimK

@TimK คุณสามารถให้ตัวอย่างของตำแหน่งที่สร้างไฟล์ แต่สตรีมล้มเหลวในภายหลังและผลที่ตามมาคืออะไร ความเสี่ยงของความล้มเหลวต่ำมากและผลกระทบก็ไม่สำคัญ ไม่จำเป็นต้องทำให้ซับซ้อนมากขึ้นกว่าที่จำเป็น
Peter Lawrey

1
GZIPOutputStream(OutputStream)เอกสารIOExceptionและเมื่อดูที่มาแล้วจะเขียนส่วนหัว ดังนั้นจึงไม่ใช่ทฤษฎีที่ผู้สร้างสามารถโยนได้ คุณอาจรู้สึกว่ามันโอเคที่จะFileOutputStreamเปิดทิ้งไว้หลังจากเขียนไปแล้ว ฉันไม่.
TJ Crowder

1
@TJCrowder ใครก็ตามที่เป็นนักพัฒนา JavaScript มืออาชีพที่มีประสบการณ์ (และภาษาอื่น ๆ นอกจากนี้) ฉันถอดหมวกของฉันออกไป ฉันไม่สามารถทำได้ ;)
Peter Lawrey

1
เพื่อทบทวนสิ่งนี้อีกครั้งปัญหาอื่น ๆ ก็คือหากคุณใช้ GZIPOutputStream บนไฟล์และไม่เรียกให้เสร็จสิ้นอย่างชัดเจนจะถูกเรียกในการใช้งานอย่างใกล้ชิด นี่ไม่ได้อยู่ในการทดลอง ... ในที่สุดหากการเสร็จสิ้น / การล้างเกิดข้อยกเว้นการจัดการไฟล์ที่อยู่เบื้องหลังจะไม่ถูกปิด
robert_difalco

6

หากสตรีมทั้งหมดได้รับการสร้างอินสแตนซ์แล้วการปิดเฉพาะด้านนอกสุดก็ใช้ได้

เอกสารเกี่ยวกับCloseableอินเทอร์เฟซระบุว่าวิธีการปิด:

ปิดสตรีมนี้และเผยแพร่ทรัพยากรระบบที่เกี่ยวข้อง

รีซอร์สระบบรวมถึงการปิดสตรีม

นอกจากนี้ยังระบุว่า:

หากสตรีมถูกปิดไปแล้วการเรียกใช้วิธีนี้จะไม่มีผล

ดังนั้นหากคุณปิดอย่างชัดเจนในภายหลังจะไม่มีอะไรผิดปกติเกิดขึ้น


2
สิ่งนี้ถือว่าไม่มีข้อผิดพลาดในการสร้างสตรีมซึ่งอาจเป็นจริงหรือไม่เป็นจริงสำหรับรายการที่ระบุไว้ แต่โดยทั่วไปแล้วไม่น่าเชื่อถือ
TJ Crowder

6

ฉันควรใช้try(...)ไวยากรณ์ (Java 7) เช่น

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

4
ในขณะที่ฉันเห็นด้วยกับคุณคุณอาจต้องการเน้นถึงประโยชน์ของแนวทางนี้และตอบประเด็นหาก OP จำเป็นต้องปิดสตรีมเด็ก / ภายใน
MadProgrammer

5

จะเป็นการดีถ้าคุณปิดเฉพาะสตรีมสุดท้ายการโทรที่ปิดจะถูกส่งไปยังสตรีมที่อยู่เบื้องหลังด้วย


1
ดูความคิดเห็นเกี่ยวกับคำตอบของ Grzegorz Żur
TJ Crowder

5

ไม่ระดับบนสุดStreamหรือreaderจะทำให้แน่ใจว่าสตรีม / ผู้อ่านที่อยู่ภายใต้การปิดทั้งหมด

ตรวจสอบclose()วิธีการใช้งานสตรีมระดับสูงสุดของคุณ


5

ใน Java 7 จะมีคุณลักษณะลองกับทรัพยากร คุณไม่จำเป็นต้องปิดสตรีมของคุณอย่างชัดเจน แต่ก็จะดูแล

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.