Try-with-Resources في عمليات الإدخال والإخراج
Try-with-Resources في عمليات الإدخال والإخراج
كل تدفّق (stream) أو قارئ أو كاتب أو قناة (channel) تفتحه هو مورد محدود على مستوى نظام التشغيل: واصف ملف (file descriptor). إذا رمى الكود استثناءً قبل الوصول إلى استدعاء close() اليدوي، يتسرّب ذلك الواصف. وإذا تراكمت التسرّبات يبدأ JVM — أو نظام التشغيل بأكمله — في رفض فتح ملفات جديدة. try-with-resources، المُقدَّمة في Java 7 والمُحسَّنة في Java 9، هي الآلية اللغوية التي تضمن استدعاء close() دائمًا، بصرف النظر عمّا يحدث.
مشكلة إغلاق الموارد يدويًا
النمط الساذج يبدو بريئًا:
حتى اللجوء إلى try/finally مرهق وعرضة للأخطاء عند تعدّد الموارد:
لاحظ try المتداخلة داخل finally: إذا رمى جسم الكود الاستثناء A ثم رمى close() الاستثناء B، يُهمَل A بصمت. هذا بالضبط نوع الأخطاء الصعبة التشخيص التي صُمِّمت try-with-resources للتخلّص منها.
عقد AutoCloseable
تعمل try-with-resources مع أي صنف يُنفّذ java.lang.AutoCloseable (أو الواجهة الفرعية java.io.Closeable). الطريقة الوحيدة المطلوبة هي:
جميع أصناف الإدخال والإخراج القياسية — InputStream وOutputStream وReader وWriter وRandomAccessFile وقنوات NIO.2 وDirectoryStream وSeekableByteChannel — تُنفّذ AutoCloseable، وبالتالي تعمل جميعها تلقائيًا داخل كتلة try-with-resources.
الصياغة الأساسية
يُعرَّف المورد داخل الأقواس التي تلي try. عند الخروج من الكتلة — بشكل طبيعي أو بـ return أو بأي استثناء — يستدعي وقت التشغيل close() على كل مورد مُعرَّف بـترتيب عكسي.
موارد متعدّدة في كتلة واحدة
عرِّف الموارد مفصولةً بفاصلة منقوطة. تُغلَق بترتيب عكسي (آخر مُعرَّف يُغلَق أولًا)، محاكيًا الطريقة التي تستدعي فيها close يدويًا:
BufferedWriter البيانات في الذاكرة مؤقتًا؛ إغلاقه يفرغ تلك البيانات إلى القرص. تضمن try-with-resources حدوث عملية الفراغ والإغلاق حتى عند الفشل.
الاستثناءات المُكتَمَة (Suppressed Exceptions)
هذا الجزء الدقيق والمهم. إذا رمى الجسم الاستثناء A، ثم رمى close() الاستثناء B، لا تُهمِل Java الاستثناء A. بدلًا من ذلك، يُرفَق B بـA كـاستثناء مُكتَم:
يمكنك فحص الاستثناءات المُكتَمة عند تشخيص الأخطاء:
Java 9: المتغيّرات الفعليًا النهائية
تتيح Java 9 استخدام متغيّر مُعرَّف مسبقًا وفعليًا نهائي (effectively final) مباشرةً في قائمة الموارد دون إعادة تعريفه:
هذا مفيد حين يُنشأ المورد شرطيًا قبل كتلة try أو يُمرَّر إلى دالة.
تنفيذ AutoCloseable في أصنافك الخاصة
أي صنف يُغلّف موردًا خارجيًا يجب أن يُنفّذ AutoCloseable لتمكين المستدعين من استخدامه بأمان:
IOException وتجاهلها داخل close(). إذا فشل تفريغ المخزن المؤقت — قرص ممتلئ أو مشاركة شبكية منقطعة — يجب أن يعلم المستدعي بذلك.
close() الاعتيادية (Idempotent) والاستدعاء المزدوج
يشترط عقد Closeable (الواجهة الفرعية لعمليات الإدخال والإخراج) صراحةً أن يكون close() اعتياديًا: استدعاؤه مرّة ثانية يجب ألا يكون له أثر ولا يرمي استثناءً. لا يضمن AutoCloseable ذلك للموارد غير الإدخال/الإخراج، لذا إذا نفّذته بنفسك في سياق آخر، وثّق ما إذا كانت الاستدعاءات المتكرّرة آمنة.
موارد NIO.2 هي أيضًا AutoCloseable
أنواع NIO.2 تتكامل بسلاسة:
المقايضات وحالات الحافة
- فشل تهيئة المورد: إذا رمى مُنشئ المورد الثاني استثناءً، يُغلَق الأول مع ذلك. تُهيّئ 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.