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

الملفات والمسارات ونظام الملفات

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

الملفات والمسارات ونظام الملفات

تمتلك Java جيلين من واجهات برمجة نظام الملفات. صنف java.io.File الأصلي موجود منذ الإصدار Java 1.0، أما حزمة java.nio.file الحديثة — التي يُشار إليها عادةً بـ NIO.2 — فقد جاءت في Java 7 وتُعوّض الأول تمامًا. فهم الجيلين معًا، وإدراك أوجه القصور في الأول، هو الأساس الذي يرتكز عليه كل ما سيأتي في هذا البرنامج التعليمي.

صنف java.io.File القديم

قبل NIO.2، كانت كل عمليات نظام الملفات تمر عبر java.io.File. كائن File في جوهره مجرد غلاف نصي حول مسار — لا يتحقق مما إذا كان المسار موجودًا فعلًا عند إنشائه.

import java.io.File; File f = new File("/home/alice/notes.txt"); System.out.println(f.exists()); // false إذا لم يكن الملف موجودًا System.out.println(f.getAbsolutePath()); // /home/alice/notes.txt System.out.println(f.length()); // 0 إذا لم يكن الملف موجودًا System.out.println(f.isDirectory()); // false File dir = new File("/home/alice"); String[] names = dir.list(); // null إذا لم يكن مجلدًا أو حدث خطأ

تبدو الواجهة بسيطة، لكنها تحمل عدة مشكلات تصميمية جوهرية تعترض كل مطور يستخدمها بما يكفي:

  • الإبلاغ عن الأخطاء بقيم منطقية. تُعيد معظم التوابع التي تُجري تعديلات (mkdir() وdelete() وrenameTo()) قيمة false عند الفشل دون أي سبب. لا تعرف إن كانت العملية فشلت بسبب صلاحيات، أو مجلد أصل مفقود، أو حالة تسابق، أو أي شيء آخر.
  • فاصل المسار يعتمد على المنصة. استخدام "/" أو "\\" مباشرة يُفسد التوافق. يساعد File.separator لكن يسهل نسيانه.
  • لا وعي بالروابط الرمزية. يتبع File الروابط الرمزية بصمت بطرق قد تُفاجئك.
  • سرد المجلدات غير مكتمل. تُعيد list() مصفوفة String[] أو File[] — كافٍ للمجلدات الصغيرة، لكنه يُحمّل كل شيء في الذاكرة دفعة واحدة، مما يُشكّل مشكلة للمجلدات التي تحتوي ملايين المدخلات.
  • لا وصول للبيانات الوصفية. لا يمكنك قراءة صلاحيات الملف أو وقت إنشائه أو مالكه بطريقة متوافقة مع المنصات.
renameTo() غير موثوقة بشكل معروف. يمكنها الفشل بصمت حين يكون المصدر والوجهة على وحدات تخزين مختلفة، أو حين يكون الملف الوجهة موجودًا مسبقًا. في بعض أنظمة JVM تفشل أيضًا عبر محركات الأقراص المُركَّبة عبر الشبكة. تحقق دومًا من القيمة المُعادة، وفضّل استخدام Files.move() في NIO.2 بدلًا منها.

واجهة NIO.2 الحديثة: Path و Files

تُقسّم NIO.2 المفهوم إلى تجريدين واضحين:

  • java.nio.file.Path — كائن قيمة خالص يمثل سلسلة مسار. يعرف بنية نظام الملفات (الفواصل، الجذور، النسبي مقابل المطلق) لكنه لا ينفذ I/O بنفسه.
  • java.nio.file.Files — صنف مساعد من التوابع الساكنة التي تُنفّذ عمليات I/O الفعلية، وترمي استثناء IOException المتحقق (أو أنواعه الفرعية) عند الفشل حتى تعرف دومًا ما الذي حدث.

تحصل على Path من خلال التابع المصنع Path.of() (Java 11+) أو الأقدم Paths.get():

import java.nio.file.Path; // مسار مطلق Path absolute = Path.of("/home/alice/notes.txt"); // مسار نسبي Path relative = Path.of("docs", "readme.txt"); // docs/readme.txt // من URI Path fromUri = Path.of(URI.create("file:///tmp/data.csv")); System.out.println(absolute.getFileName()); // notes.txt System.out.println(absolute.getParent()); // /home/alice System.out.println(absolute.isAbsolute()); // true System.out.println(relative.isAbsolute()); // false
Path.of() مقابل Paths.get(): كلاهما يُعيد الكائن ذاته. أُضيف Path.of() في Java 11 كتابع مساعد مباشرة على الواجهة. في قواعد الكود الحديثة (Java 11+) فضّل Path.of() — فهو أوضح قراءةً ولا يتطلب استيراد صنف منفصل.

التنقل في المسارات والحل

من أبرز مزايا Path على السلاسل النصية الخام قدرتها المدمجة على حساب المسارات. يمكنك التنقل في شجرة نظام الملفات دون تسلسل نصي:

Path base = Path.of("/home/alice"); Path config = base.resolve("config/app.yaml"); // /home/alice/config/app.yaml Path parent = config.getParent(); // /home/alice/config Path name = config.getFileName(); // app.yaml Path root = config.getRoot(); // / // relativize — عكس resolve Path a = Path.of("/home/alice/docs/report.pdf"); Path b = Path.of("/home/alice"); System.out.println(b.relativize(a)); // docs/report.pdf // normalize تُزيل . و.. الزائدة Path messy = Path.of("/home/alice/../alice/./docs"); System.out.println(messy.normalize()); // /home/alice/docs // toAbsolutePath يحل المسار بالنسبة لمجلد العمل الحالي Path rel = Path.of("notes.txt"); System.out.println(rel.toAbsolutePath()); // مثلًا /home/alice/notes.txt

التحقق من الوجود وخصائص الملفات

يوفر صنف Files توابع شرطية تقابل توابع File القديمة، مع وصول أغنى للبيانات الوصفية:

import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.io.IOException; Path p = Path.of("/home/alice/notes.txt"); System.out.println(Files.exists(p)); // true / false System.out.println(Files.isDirectory(p)); // false System.out.println(Files.isReadable(p)); // true System.out.println(Files.isWritable(p)); // true System.out.println(Files.size(p)); // حجم الملف بالبايت BasicFileAttributes attrs = Files.readAttributes(p, BasicFileAttributes.class); System.out.println(attrs.creationTime()); // مثلًا 2024-03-15T10:22:00Z System.out.println(attrs.lastModifiedTime()); // وقت آخر كتابة System.out.println(attrs.isSymbolicLink()); // الوعي بالروابط الرمزية
فضّل Files.exists() على اصطياد الاستثناءات للتحكم في التدفق. إذا كنت تتوقع أن الملف قد لا يكون موجودًا، تحقق أولًا بـ Files.exists(). احتفظ بمعالجة IOException لحالات الفشل الاستثنائية الحقيقية — القرص ممتلئ، أخطاء الصلاحيات — لا للتحقق الروتيني من عدم وجود المسار.

التحويل بين File و Path

الكود القديم الذي يستخدم java.io.File منتشر في كل مكان — المكتبات القديمة، واجهات برمجة الطرف الثالث، وأغلفة Android قبل NIO. ستحتاج كثيرًا إلى الجسر بين العالمين:

import java.io.File; import java.nio.file.Path; // File -> Path File legacyFile = new File("/home/alice/notes.txt"); Path modern = legacyFile.toPath(); // Path -> File Path nioPath = Path.of("/home/alice/notes.txt"); File asFile = nioPath.toFile(); // يعمل فقط لمسارات نظام الملفات الافتراضي // كلاهما يشير إلى نفس مدخل نظام الملفات System.out.println(modern.equals(nioPath)); // true

FileSystem و FileSystems

خلف كل من Path وFiles يوجد java.nio.file.FileSystem — تجريد يتيح لك العمل مع أرشيفات ZIP وأنظمة ملفات في الذاكرة (للاختبار) وأنظمة ملفات بعيدة من خلال نفس الواجهة. يُحصل على نظام الملفات الافتراضي (قرص نظام التشغيل) عبر FileSystems.getDefault(). لن تحتاج هذا مباشرةً في معظم كود التطبيقات، لكنه يُوضّح سبب كون Path.of() اختصارًا لـ FileSystems.getDefault().getPath().

الخلاصة

صنف java.io.File القديم غلاف هش للمسارات مع أوضاع فشل صامتة، وبدون بيانات وصفية غنية أو تحكم في الروابط الرمزية. الثنائي الحديث في NIO.2 — Path (كائن قيمة للمسارات) وFiles (عمليات I/O مع استثناءات صحيحة) — يحل كل تلك المشكلات. في كل الكود الجديد استخدم Path.of() وFiles.*. عند صيانة كود قديم يعرض File، حوّل فورًا بـ file.toPath() وتعامل مع NIO.2 من تلك النقطة فصاعدًا.