واجهتا Path و Files
قدّمت Java 7 واجهة برمجة NIO.2 — الحزمة java.nio.file — لتحلّ محلّ الصنف القديم java.io.File. كان File متقلّبًا (فشل صامت، ورسائل خطأ شحيحة، ودعم ضعيف للوصلات الرمزية)، أمّا Path وFiles فهما دقيقان وقابلان للتركيب ويعملان عبر الاستثناءات. في Java الحديثة هذان هما الخياران الصحيحان لكل عملية على نظام الملفات.
Path: تمثيل موقع
Path واجهة تمثّل موقعًا في نظام الملفات — لا تنفّذ أي إدخال/إخراج بنفسها. تُنشئها بـ Path.of() (Java 11+) أو ما يعادلها Paths.get():
import java.nio.file.Path;
Path absolute = Path.of("/home/user/documents/report.txt");
Path relative = Path.of("data", "config.json"); // يُضاف فاصل المنصّة تلقائيًا
Path resolved = Path.of("/home/user").resolve("documents/report.txt");
System.out.println(absolute.getFileName()); // report.txt
System.out.println(absolute.getParent()); // /home/user/documents
System.out.println(absolute.getRoot()); // /
System.out.println(relative.toAbsolutePath()); // يُثبَّت على مجلّد عمل JVM
أبرز توابع التنقل في Path:
resolve(other) — يُلحق مقطع مسار آخر ويُعيد Path جديدًا.
relativize(other) — يحسب المسار النسبي بين مسارَين مطلقَين.
normalize() — يُزيل مقاطع . و.. الزائدة.
toAbsolutePath() — يحوّل المسار النسبي إلى مطلق استنادًا إلى مجلّد عمل JVM.
toRealPath() — كـ toAbsolutePath() لكنّه يحلّ الوصلات الرمزية أيضًا ويرمي IOException إن لم يوجد الملف.
Path base = Path.of("/var/app/logs");
Path target = Path.of("/var/app/logs/2024/march/error.log");
Path rel = base.relativize(target);
System.out.println(rel); // 2024/march/error.log
Path normalized = Path.of("/var/app/./logs/../logs/access.log").normalize();
System.out.println(normalized); // /var/app/logs/access.log
Path غير قابل للتغيير. كل تابع يبدو وكأنّه يُعدّل المسار — resolve وnormalize وrelativize — يُعيد كائن Path جديدًا. الأصل يبقى كما هو.
Files: صنف الأدوات الثابتة
Files صنف نهائي يحتوي على توابع ثابتة تنفّذ إدخال/إخراجًا فعليًا على Path. فكّر في Path كالعنوان وفي Files كمزوّد الخدمة الذي يؤدّي عملًا عند ذلك العنوان.
فحص الوجود والبيانات الوصفية
import java.nio.file.Files;
import java.nio.file.Path;
Path p = Path.of("data/report.csv");
boolean exists = Files.exists(p); // true / false
boolean isFile = Files.isRegularFile(p);
boolean isDir = Files.isDirectory(p);
boolean readable = Files.isReadable(p);
boolean writable = Files.isWritable(p);
long sizeBytes = Files.size(p); // يرمي IOException إن كان غائبًا
System.out.printf("exists=%b size=%d bytes%n", exists, sizeBytes);
خطر TOCTOU. فحص Files.exists() ثمّ التصرّف بناءً على نتيجته يُشكّل حالة سباق بين وقت الفحص ووقت الاستخدام في الشيفرة متعددة الخيوط أو العمليات. يُفضَّل محاولة العملية مباشرةً والتعامل مع IOException — مثلًا افتح الملف للكتابة وامسك FileAlreadyExistsException إن أردت الإنشاء الحصري.
نسخ الملفات
Files.copy(source, target, options...) ينسخ ملفًا أو إدخال دليل. المعامل الاختياري CopyOption يتحكّم في السلوك:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
Path src = Path.of("originals/photo.jpg");
Path dest = Path.of("backups/photo.jpg");
// افتراضيًا: يرمي FileAlreadyExistsException إن وُجد الهدف
Files.copy(src, dest);
// استبدل الهدف إن وُجد
Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
// انسخ سمات الملف أيضًا (الطوابع الزمنية والأذونات)
Files.copy(src, dest,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
نسخ دليل ينسخ إدخال الدليل فحسب، لا أبناءه. لنسخ شجرة أدلّة بالكامل، امشِ الشجرة بـ Files.walkFileTree() وانسخ كل إدخال على حدة — أو استعن بمكتبة مثل Apache Commons IO التي تُغلّف هذا النمط.
نقل الملفات وإعادة تسميتها
Files.move(source, target, options...) ينقل أو يُعيد تسمية. حين يكون المصدر والهدف على قسم نظام ملفات واحد تكون العملية عادةً إعادة تسمية ذريّة (رخيصة كـ mv في الصدفة)؛ وعبر الأقسام تنسخ ثمّ تحذف.
Path oldPath = Path.of("inbox/draft.txt");
Path newPath = Path.of("archive/2024/draft.txt");
// تأكّد أن دليل الهدف موجود أولًا
Files.createDirectories(newPath.getParent());
Files.move(oldPath, newPath, StandardCopyOption.REPLACE_EXISTING);
// oldPath لم يعد موجودًا؛ newPath يحمل الملف الآن
الخيار ATOMIC_MOVE يضمن — حين يدعمه نظام الملفات — أن النقل ذري تمامًا، لا يرى أي عملية أخرى حالةً جزئية:
Files.move(src, dest, StandardCopyOption.ATOMIC_MOVE);
// يرمي AtomicMoveNotSupportedException إن كان النظام لا يدعمه
حذف الملفات
توجد نسختان بدلالتَي فشل مختلفتَين:
Path file = Path.of("temp/cache.bin");
// يرمي NoSuchFileException إن لم يوجد الملف
Files.delete(file);
// يُعيد false (دون استثناء) إن لم يوجد الملف — "احذف إن وُجد"
boolean deleted = Files.deleteIfExists(file);
لا يمكنك حذف دليل غير فارغ بأيٍّ من الاستدعاءَين — كلاهما يرمي DirectoryNotEmptyException. لحذف شجرة كاملة، امشِها بـ Files.walkFileTree() واحذف الملفات قبل أدلّتها الأمّ.
إنشاء الأدلّة
// ينشئ دليلًا واحدًا — يفشل إن لم يوجد الأب
Files.createDirectory(Path.of("logs"));
// ينشئ المسار كاملًا بما يشمل أي أبٍّ ناقص (مثل mkdir -p)
Files.createDirectories(Path.of("logs/2024/march"));
// إنشاء ملف مؤقت / دليل مؤقت
Path tmpFile = Files.createTempFile("prefix-", ".log");
Path tmpDir = Files.createTempDirectory("work-");
قراءة حجم الملف بكفاءة
يستعلم Files.size(path) من بيانات نظام الملفات الوصفية ويُعيد الحجم بالبايتات دون قراءة المحتوى — إنّها استدعاء نظام O(1) لا قراءة O(n). استخدمها للتحقق من الصحة أو تتبع التقدّم أو توجيه الملفات الكبيرة إلى مسار البث:
Path upload = Path.of("uploads/video.mp4");
long maxBytes = 100L * 1024 * 1024; // 100 MB
if (Files.size(upload) > maxBytes) {
throw new IllegalArgumentException("File exceeds the 100 MB limit");
}
تجميع كل شيء معًا
import java.io.IOException;
import java.nio.file.*;
public class FileOps {
public static void archiveLog(Path source, Path archiveDir) throws IOException {
if (!Files.isRegularFile(source)) {
throw new IllegalArgumentException("Not a regular file: " + source);
}
Files.createDirectories(archiveDir);
Path dest = archiveDir.resolve(source.getFileName());
Files.copy(source, dest,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
System.out.printf("Archived %s (%,d bytes) -> %s%n",
source.getFileName(), Files.size(dest), dest);
Files.deleteIfExists(source);
}
public static void main(String[] args) throws IOException {
archiveLog(
Path.of("logs/app.log"),
Path.of("archive/2024/june")
);
}
}
الخلاصة
Path تمثيل غير قابل للتغيير وقابل للتركيب لموقع في نظام الملفات، وFiles الأداة الثابتة التي تتصرّف عليه. استخدم Files.copy() مع REPLACE_EXISTING أو COPY_ATTRIBUTES، وFiles.move() مع ATOMIC_MOVE لإعادة التسمية الآمنة، وFiles.delete() مقابل Files.deleteIfExists() حسب ما إذا كان الملف الغائب خطأً، وFiles.size() لاستعلامات البيانات الوصفية السريعة. دائمًا دع العمليات ترمي IOException وعالجها عند نقطة الاستدعاء بدلًا من ابتلاعها بصمت.