معالجة الاستثناءات

try-with-resources وواجهة AutoCloseable

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

try-with-resources وواجهة AutoCloseable

في كل مرة يفتح فيها كودك ملفًا أو اتصال شبكة أو اتصال قاعدة بيانات أو أي مورد خارجي آخر، يجب إغلاق ذلك المورد عند الانتهاء منه. نسيان إغلاق الموارد من أكثر الأخطاء شيوعًا في Java: يتسبب في تسرب الذاكرة، واستنزاف مقابض الملفات، وقفل الملفات بحيث لا يستطيع أي كود آخر فتحها.

قدّم Java 7 تعليمة try-with-resources لحل هذه المشكلة تلقائيًا. بدلًا من كتابة منطق الإغلاق بنفسك، تُعلن عن المورد داخل قوسَي try ويضمن Java إغلاقه — حتى لو رُمي استثناء.

المشكلة: تنظيف الموارد يدويًا عرضة للأخطاء

قبل try-with-resources، كان يجب إغلاق الموارد في كتلة finally. حتى الكود الحذِر كان يحتوي على أخطاء خفية:

// الأسلوب القديم: خطير ومطوّل FileReader reader = null; try { reader = new FileReader("data.txt"); // قراءة البيانات... } catch (IOException e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); // قد يرمي close() استثناءً بدوره! } catch (IOException e) { e.printStackTrace(); } } }

هذا عشرة أسطر من الكود المكرر لإغلاق مورد واحد بأمان. إذا رمى close() استثناءً أيضًا، يُبتلع الاستثناء الأصلي — خطأ يصعب تتبعه جدًا.

الحل: try-with-resources

أعلِن عن المورد في القوسين بعد try. يستدعي Java تلقائيًا close() عند نهاية الكتلة، سواء خرجت بشكل طبيعي أو عبر استثناء:

// Java الحديث: نظيف وآمن try (FileReader reader = new FileReader("data.txt")) { int character; while ((character = reader.read()) != -1) { System.out.print((char) character); } } catch (IOException e) { System.err.println("تعذّرت قراءة الملف: " + e.getMessage()); } // reader مضمون إغلاقه هنا

يُعيد المُترجم كتابة هذا الكود بصورة تتعامل بشكل صحيح مع كل الحالات الطرفية، بما فيها الاستثناءات المكبوتة.

الضمان الأساسي: يُستدعى close() دائمًا — حتى عند رمي استثناء داخل كتلة try، وحتى إن لم تكن هناك كتلة catch على الإطلاق.

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

يمكنك الإعلان عن موارد متعددة مفصولة بفاصلة منقوطة. تُغلق بـترتيب عكسي للإعلان — آخر مورد فُتح هو أول من يُغلق:

try ( FileReader reader = new FileReader("input.txt"); FileWriter writer = new FileWriter("output.txt") ) { int ch; while ((ch = reader.read()) != -1) { writer.write(Character.toUpperCase(ch)); } } catch (IOException e) { System.err.println("خطأ IO: " + e.getMessage()); } // يُغلق writer أولًا، ثم reader

واجهة AutoCloseable

تعمل try-with-resources مع أي كلاس يُنفّذ AutoCloseable (أو الواجهة الفرعية Closeable). تحتوي الواجهة على طريقة واحدة فقط:

public interface AutoCloseable { void close() throws Exception; }

جميع كلاسات I/O القياسية في Java (InputStream، OutputStream، Reader، Writer، Scanner، Connection، PreparedStatement، إلخ) تُنفّذ هذه الواجهة بالفعل. لهذا تعمل جميعها مع try-with-resources من دون أي إعداد إضافي.

كتابة كلاس AutoCloseable خاص بك

يمكنك جعل أي كلاس يعمل مع try-with-resources بتنفيذ AutoCloseable. مثال شائع هو كلاس يُغلّف اتصال شبكة أو ملفًا مؤقتًا:

public class DatabaseConnection implements AutoCloseable { private final String url; private boolean open; public DatabaseConnection(String url) { this.url = url; this.open = true; System.out.println("فُتح الاتصال بـ " + url); } public void query(String sql) { if (!open) throw new IllegalStateException("الاتصال مغلق"); System.out.println("تنفيذ الاستعلام: " + sql); } @Override public void close() { if (open) { open = false; System.out.println("أُغلق الاتصال بـ " + url); } } }

الآن يمكنك استخدامه تمامًا مثل أي مورد مدمج:

try (DatabaseConnection conn = new DatabaseConnection("jdbc:mysql://localhost/mydb")) { conn.query("SELECT * FROM users"); conn.query("SELECT * FROM orders"); } // الإخراج: // فُتح الاتصال بـ jdbc:mysql://localhost/mydb // تنفيذ الاستعلام: SELECT * FROM users // تنفيذ الاستعلام: SELECT * FROM orders // أُغلق الاتصال بـ jdbc:mysql://localhost/mydb
اجعل close() ثابتة النتيجة عند الاستدعاء المتكرر (idempotent). من الممارسات الجيدة السماح باستدعاء close() أكثر من مرة دون رمي خطأ. استخدم علمًا منطقيًا (مثل open أعلاه) للحماية من الإغلاق المزدوج.

الاستثناءات المكبوتة

ماذا يحدث عندما ترمي كتلة try استثناءً ويرمي close() استثناءً أيضًا؟ في الكود القديم بكتلة finally، كان الاستثناء الثاني يُخفي الأول. تتعامل try-with-resources مع هذا بشكل صحيح: استثناء كتلة try هو الاستثناء الرئيسي، والاستثناء الصادر عن close() يُرفق به كـاستثناء مكبوت.

public class FaultyResource implements AutoCloseable { @Override public void close() throws Exception { throw new Exception("خطأ أثناء الإغلاق"); } } try (FaultyResource res = new FaultyResource()) { throw new RuntimeException("خطأ في الكتلة الرئيسية"); } catch (RuntimeException e) { System.out.println("الرئيسي: " + e.getMessage()); for (Throwable suppressed : e.getSuppressed()) { System.out.println("المكبوت: " + suppressed.getMessage()); } } // الرئيسي: خطأ في الكتلة الرئيسية // المكبوت: خطأ أثناء الإغلاق

يمكنك استرجاع الاستثناءات المكبوتة بـgetSuppressed() التي تُعيد Throwable[]. لا تضيع أي معلومات.

قراءة ملف سطرًا بسطر — مثال واقعي

عمليًا، الاستخدام الأكثر شيوعًا لـtry-with-resources هو قراءة ملفات نصية بـBufferedReader:

import java.io.*; import java.nio.file.*; // BufferedReader يُغلّف FileReader للقراءة الفعّالة سطرًا بسطر try (BufferedReader br = new BufferedReader(new FileReader("employees.csv"))) { String line; while ((line = br.readLine()) != null) { String[] parts = line.split(","); System.out.println("الاسم: " + parts[0] + "، الدور: " + parts[1]); } } catch (IOException e) { System.err.println("فشل قراءة الملف: " + e.getMessage()); }
يكفي إعلان المورد الخارجي فقط في try-with-resources. في المثال أعلاه، إغلاق BufferedReader يُغلق تلقائيًا FileReader المُغلَّف بداخله، لأن استدعاء close ينتقل عبر السلسلة. إعلان كليهما منفصلَين قد يُغلق القارئ الداخلي مرتين.

الخلاصة

try-with-resources هي الطريقة الحديثة والصحيحة لإدارة أي مورد يجب إغلاقه. أعلِن عن الموارد في قوسَي try()، ونفِّذ AutoCloseable في كلاساتك الخاصة، ودَع JVM يتولى التنظيف. ستحصل على كود أقصر وأخطاء أقل ومعالجة صحيحة للاستثناءات المكبوتة — كل ذلك مجانًا.