الإدخال والإخراج وNIO.2

Try-with-Resources في عمليات الإدخال والإخراج

15 دقيقة الدرس 8 من 13

Try-with-Resources في عمليات الإدخال والإخراج

كل تدفّق (stream) أو قارئ أو كاتب أو قناة (channel) تفتحه هو مورد محدود على مستوى نظام التشغيل: واصف ملف (file descriptor). إذا رمى الكود استثناءً قبل الوصول إلى استدعاء close() اليدوي، يتسرّب ذلك الواصف. وإذا تراكمت التسرّبات يبدأ JVM — أو نظام التشغيل بأكمله — في رفض فتح ملفات جديدة. try-with-resources، المُقدَّمة في Java 7 والمُحسَّنة في Java 9، هي الآلية اللغوية التي تضمن استدعاء close() دائمًا، بصرف النظر عمّا يحدث.

مشكلة إغلاق الموارد يدويًا

النمط الساذج يبدو بريئًا:

BufferedReader reader = new BufferedReader(new FileReader("data.txt")); String line = reader.readLine(); // ماذا لو رمى هذا استثناءً؟ reader.close(); // لن يُنفَّذ عند حدوث استثناء

حتى اللجوء إلى try/finally مرهق وعرضة للأخطاء عند تعدّد الموارد:

BufferedReader reader = null; try { reader = new BufferedReader(new FileReader("data.txt")); String line = reader.readLine(); } finally { if (reader != null) { try { reader.close(); // قد يرمي close() بنفسه — يبتلع الاستثناء الحقيقي } catch (IOException ignored) {} } }

لاحظ try المتداخلة داخل finally: إذا رمى جسم الكود الاستثناء A ثم رمى close() الاستثناء B، يُهمَل A بصمت. هذا بالضبط نوع الأخطاء الصعبة التشخيص التي صُمِّمت try-with-resources للتخلّص منها.

عقد AutoCloseable

تعمل try-with-resources مع أي صنف يُنفّذ java.lang.AutoCloseable (أو الواجهة الفرعية java.io.Closeable). الطريقة الوحيدة المطلوبة هي:

public void close() throws Exception;

جميع أصناف الإدخال والإخراج القياسية — InputStream وOutputStream وReader وWriter وRandomAccessFile وقنوات NIO.2 وDirectoryStream وSeekableByteChannel — تُنفّذ AutoCloseable، وبالتالي تعمل جميعها تلقائيًا داخل كتلة try-with-resources.

الصياغة الأساسية

try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } // يُستدعى reader.close() هنا دائمًا، حتى عند حدوث استثناء

يُعرَّف المورد داخل الأقواس التي تلي try. عند الخروج من الكتلة — بشكل طبيعي أو بـ return أو بأي استثناء — يستدعي وقت التشغيل close() على كل مورد مُعرَّف بـترتيب عكسي.

موارد متعدّدة في كتلة واحدة

عرِّف الموارد مفصولةً بفاصلة منقوطة. تُغلَق بترتيب عكسي (آخر مُعرَّف يُغلَق أولًا)، محاكيًا الطريقة التي تستدعي فيها close يدويًا:

Path source = Path.of("input.txt"); Path dest = Path.of("output.txt"); try ( BufferedReader reader = Files.newBufferedReader(source); BufferedWriter writer = Files.newBufferedWriter(dest) ) { String line; while ((line = reader.readLine()) != null) { writer.write(line); writer.newLine(); } } // يُغلَق writer أولًا، ثم reader
ترتيب الإغلاق مهم للصحة. يُخزّن BufferedWriter البيانات في الذاكرة مؤقتًا؛ إغلاقه يفرغ تلك البيانات إلى القرص. تضمن try-with-resources حدوث عملية الفراغ والإغلاق حتى عند الفشل.

الاستثناءات المُكتَمَة (Suppressed Exceptions)

هذا الجزء الدقيق والمهم. إذا رمى الجسم الاستثناء A، ثم رمى close() الاستثناء B، لا تُهمِل Java الاستثناء A. بدلًا من ذلك، يُرفَق B بـA كـاستثناء مُكتَم:

try (var stream = new FailingStream()) { stream.doWork(); // يرمي IOException("body failed") } // يرمي close() أيضًا IOException("close failed") // يتلقّى المستدعي IOException("body failed") // ex.getSuppressed()[0] هو IOException("close failed")

يمكنك فحص الاستثناءات المُكتَمة عند تشخيص الأخطاء:

try (var r = Files.newBufferedReader(Path.of("missing.txt"))) { r.readLine(); } catch (IOException ex) { System.err.println("الأساسي: " + ex.getMessage()); for (Throwable s : ex.getSuppressed()) { System.err.println("المُكتَم: " + s.getMessage()); } }
استخدم try-with-resources بدلًا من try/finally لكل AutoCloseable. آلية الاستثناءات المُكتَمة أصحّ من النمط القديم الذي كان فيه استثناء close يحلّ صامتًا محلّ استثناء الجسم.

Java 9: المتغيّرات الفعليًا النهائية

تتيح Java 9 استخدام متغيّر مُعرَّف مسبقًا وفعليًا نهائي (effectively final) مباشرةً في قائمة الموارد دون إعادة تعريفه:

BufferedReader reader = openReader(); // مُحصَل عليه في مكان آخر try (reader) { // Java 9+: مرجع المتغيّر لا تعريفه process(reader); } // لا يزال reader.close() مضمونًا

هذا مفيد حين يُنشأ المورد شرطيًا قبل كتلة try أو يُمرَّر إلى دالة.

تنفيذ AutoCloseable في أصنافك الخاصة

أي صنف يُغلّف موردًا خارجيًا يجب أن يُنفّذ AutoCloseable لتمكين المستدعين من استخدامه بأمان:

public final class CsvWriter implements AutoCloseable { private final BufferedWriter writer; public CsvWriter(Path path) throws IOException { this.writer = Files.newBufferedWriter(path); } public void writeRow(String... columns) throws IOException { writer.write(String.join(",", columns)); writer.newLine(); } @Override public void close() throws IOException { writer.close(); // يفوّض إلى المورد الأساسي } } // الاستخدام: try (var csv = new CsvWriter(Path.of("report.csv"))) { csv.writeRow("name", "score"); csv.writeRow("Alice", "98"); }
لا تبتلع الاستثناءات داخل close(). خطأ شائع هو التقاط IOException وتجاهلها داخل close(). إذا فشل تفريغ المخزن المؤقت — قرص ممتلئ أو مشاركة شبكية منقطعة — يجب أن يعلم المستدعي بذلك.

close() الاعتيادية (Idempotent) والاستدعاء المزدوج

يشترط عقد Closeable (الواجهة الفرعية لعمليات الإدخال والإخراج) صراحةً أن يكون close() اعتياديًا: استدعاؤه مرّة ثانية يجب ألا يكون له أثر ولا يرمي استثناءً. لا يضمن AutoCloseable ذلك للموارد غير الإدخال/الإخراج، لذا إذا نفّذته بنفسك في سياق آخر، وثّق ما إذا كانت الاستدعاءات المتكرّرة آمنة.

موارد NIO.2 هي أيضًا AutoCloseable

أنواع NIO.2 تتكامل بسلاسة:

Path dir = Path.of("/tmp/work"); try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) { for (Path entry : stream) { System.out.println(entry.getFileName()); } } try (SeekableByteChannel channel = Files.newByteChannel(Path.of("data.bin"))) { ByteBuffer buf = ByteBuffer.allocate(1024); while (channel.read(buf) > 0) { buf.flip(); // معالجة المخزن المؤقت buf.clear(); } }

المقايضات وحالات الحافة

  • فشل تهيئة المورد: إذا رمى مُنشئ المورد الثاني استثناءً، يُغلَق الأول مع ذلك. تُهيّئ Java كل مورد وتتتبّعه باستقلالية.
  • موارد null: يُتجاوَز null في قائمة الموارد بصمت — لا NPE ولا استدعاء close. مفيد حين لا يكون المورد مُنشأً بعد، لكن كن حذرًا إذ قد يخفي أخطاء عدم فتح المورد.
  • الاستثناءات المتحقَّق منها مقابل غير المتحقَّق منها: إذا أعلن close() عن throws Exception يُجبَر المستدعي على معالجته. إذا لم يرمي close() سوى استثناءات غير متحقَّق منها أو لا شيء، أعلن throws nothing لتخفيف العبء عن المستدعين.

الخلاصة

try-with-resources هي الطريقة الصحيحة والاصطلاحية لإدارة أي مورد AutoCloseable في Java. تضمن تشغيل close() بلا شروط، وتعالج الاستثناءات المُكتَمة بشكل صحيح بدلًا من تجاهلها، وتُزيل الشيفرة المكرَّرة لكتل try/finally المتداخلة. كل صنف I/O في المكتبة القياسية يُنفّذ AutoCloseable، وأصنافك الخاصة التي تحمل موارد يجب أن تفعل الأمر ذاته. حين تفتح ملفًا أو قناةً أو تدفّقًا، استخدم دائمًا كتلة try-with-resources.