นากกายต้นฉบับกำลังปิดวาล์วทรัพยากรสามตัวจากขวาไปซ้ายบนโต๊ะทำงานสว่าง

Java Try-with-resources: ปิด Resource ย้อนลำดับแม้กลางทางมี Exception

พี่โชว์Engineering

การเปิดไฟล์, connection หรือ lock สำเร็จเพียงครึ่งชุดก็ต้องปิดของที่เปิดแล้วครับ try-with-resources ผูกการได้ resource กับ cleanup ไว้ในโครงสร้างภาษา และปิดย้อนลำดับการประกาศแม้โค้ดใน block โยน exception

WHY: Cleanup เป็นส่วนหนึ่งของ Correctness

finally เขียนได้ แต่เมื่อมีหลาย resource โค้ดจะซ้อนและอาจกลบ exception ต้นเหตุด้วย exception ตอนปิด JLS กำหนดลำดับของ try-with-resources ชัดเจน พร้อมเก็บ exception ตอน close เป็น suppressed exception เมื่อมีปัญหาหลักเกิดก่อน

resource ต้องเป็น AutoCloseable หรือ Closeable หลังสร้างสำเร็จ Java จะรับผิดชอบเรียก close() ให้ หาก constructor ตัวถัดไปล้ม ตัวก่อนหน้าที่ยืนยันแล้วก็ยังถูกปิด

HOW: พิสูจน์ลำดับด้วย Event List

import java.util.ArrayList;
import java.util.List;

public class Main {
    static final class Resource implements AutoCloseable {
        private final String name;
        private final List<String> events;

        Resource(String name, List<String> events) {
            this.name = name;
            this.events = events;
            events.add("open:" + name);
        }

        @Override
        public void close() {
            events.add("close:" + name);
        }
    }

    public static void main(String[] args) {
        List<String> events = new ArrayList<>();
        try (Resource first = new Resource("first", events);
             Resource second = new Resource("second", events)) {
            events.add("use");
        }

        assert events.equals(List.of(
            "open:first", "open:second", "use",
            "close:second", "close:first"
        ));
    }
}

ลำดับย้อนกลับเหมือน stack: ตัวหลังมักพึ่งตัวก่อน จึงต้องปิดตัวหลังให้เสร็จก่อน เช่น stream wrapper ปิดก่อน file stream ด้านล่าง

ระวัง Ownership ที่ไม่ชัด

อย่าใส่ resource ที่ method อื่นยังต้องใช้หลัง block จบเข้า try นี้ ถ้า method รับ resource จากผู้เรียก ต้องตกลงว่าใครเป็นเจ้าของการปิด การปิดของที่ยืมมาทำให้ failure เกิดไกลจากต้นเหตุ

เมื่อ close() โยน exception ให้ตรวจ getSuppressed() ใน logging และ test แต่ไม่ควรออกแบบ cleanup ให้ล้มง่าย ปิดแบบ idempotent เมื่อทำได้ และอย่าทำ network call ยาวโดยไม่มี timeout ใน close

Recap

  • ประกาศ resource ใน try เพื่อผูก lifetime กับ scope
  • จำว่าปิดย้อนลำดับการประกาศ
  • กำหนด ownership ก่อนรับหรือส่ง resource ข้าม method
  • เก็บ suppressed exceptions โดยไม่กลบสาเหตุหลัก

พรุ่งนี้เลือก finally ที่มี close หลายตัวหนึ่งจุด แล้วเขียน test ลำดับ cleanup ก่อนย้ายมา try-with-resources ครับ