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

العمل مع المجلدات

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

العمل مع المجلدات

المجلدات هي الهيكل العظمي لأي نظام ملفات. تمنحك واجهة NIO.2 في جافا — المرتكزة على java.nio.file.Files وjava.nio.file.Path — تحكمًا دقيقًا وتعبيريًا في إنشاء المجلدات وسرد محتوياتها والمشي عبر شجرة المجلدات بالكامل. في هذا الدرس ستتعلم بالضبط أي دالة تستخدم ولماذا اتُّخِذ كل قرار تصميمي.

إنشاء المجلدات

توفّر NIO.2 دالتَي بناء متمايزتَين، واختيار الخاطئة منهما خطأ شائع.

Files.createDirectory(Path) تُنشئ مجلدًا واحدًا بالضبط. تفشل الدالة بإطلاق NoSuchFileException إن لم يكن أيٌّ من المجلدات الأب موجودًا، وبـ FileAlreadyExistsException إن كان المجلد الهدف نفسه موجودًا.

import java.nio.file.*; Path dir = Path.of("output/reports"); Files.createDirectory(dir); // تُطلق استثناءً إن لم يكن 'output' موجودًا

Files.createDirectories(Path) تُنشئ المسار كاملًا — بما فيه المجلدات الأب — وهي idempotent: إن كان المجلد موجودًا بالفعل تعود بصمت بدلًا من إطلاق استثناء.

Path nested = Path.of("output/2024/reports/q1"); Files.createDirectories(nested); // تُنشئ كل جزء مفقود؛ لا تفعل شيئًا إن كل شيء موجود
افضّل createDirectories في كود التطبيق. نادرًا ما تعرف التطبيقات الحقيقية مسبقًا إن كان المجلد الأب موجودًا. استخدام createDirectories يُلغي الحاجة للتحقق المسبق من الوجود وهو آمن من ناحية سباق الخيوط — فالتحقق والإنشاء يحدثان ذريًا على مستوى نظام التشغيل.

يمكنك أيضًا إنشاء مجلد بصلاحيات POSIX محددة في استدعاء واحد:

import java.nio.file.attribute.*; import java.util.Set; Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-x---"); FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms); Files.createDirectories(Path.of("secure/vault"), attr); // تُطبَّق فقط على أنظمة POSIX (Linux، macOS). تُتجاهل بصمت على Windows.

سرد مدخلات المجلد

ثلاث دوال موجودة لسرد محتويات المجلد. تتباين في العمق، والكسل، والتحكم الذي تمنحه.

Files.list(Path) تُعيد Stream<Path> كسولًا للأبناء المباشرين فقط (عمق 1). لأن الدفق مدعوم بمكرّر مجلد يجب إغلاقه — استخدم دائمًا try-with-resources.

Path source = Path.of("src/main/java"); try (var entries = Files.list(source)) { entries .filter(p -> p.toString().endsWith(".java")) .map(Path::getFileName) .forEach(System.out::println); }
دفقات المجلدات غير المغلقة تُسرِّب واصفات ملفات OS. على Linux الحد الافتراضي للعملية 1024. استخدم دائمًا try-with-resources مع Files.list وFiles.walk وFiles.find.

Files.newDirectoryStream(Path) وصيغتها مع الـ glob تمنحانك DirectoryStream<Path> — مفيدة حين تريد فلترة نمط glob يعالجها نظام التشغيل بدلًا من جافا:

try (var stream = Files.newDirectoryStream(Path.of("logs"), "*.log")) { for (Path logFile : stream) { System.out.println(logFile.getFileName()); } }

المشي عبر شجرة المجلدات

Files.walk(Path, int maxDepth) تُعيد Stream<Path> كسولًا يُنفّذ اجتيازًا بالعمق أولًا. الجذر نفسه دائمًا هو أول عنصر يُنبعث.

Path root = Path.of("project"); try (var tree = Files.walk(root)) { long javaFileCount = tree .filter(Files::isRegularFile) .filter(p -> p.toString().endsWith(".java")) .count(); System.out.println("Java files found: " + javaFileCount); }

حذف maxDepth يمشي الشجرة بالكامل؛ تحديده يُحدّد العودية. عمق 1 يعادل Files.list.

Files.find(Path, int maxDepth, BiPredicate<Path, BasicFileAttributes>) هو الابن الأقوى. يستقبل المسند كلًا من المسار ولقطة BasicFileAttributes الخاصة به، فتستطيع الفلترة على البيانات الوصفية (الحجم، آخر تعديل، هل هو رابط رمزي) دون استدعاء نظام إضافي لكل مدخل:

import java.nio.file.attribute.BasicFileAttributes; import java.time.Instant; import java.time.temporal.ChronoUnit; Instant cutoff = Instant.now().minus(7, ChronoUnit.DAYS); try (var recent = Files.find( Path.of("uploads"), Integer.MAX_VALUE, (path, attrs) -> attrs.isRegularFile() && attrs.lastModifiedTime().toInstant().isAfter(cutoff))) { recent.forEach(System.out::println); }
لماذا Files.find أفضل من Files.walk مع Files.readAttributes. كل استدعاء لـ Files.readAttributes هو استدعاء نظام منفصل. Files.find تلتقط البيانات الوصفية في نفس استدعاء النظام الذي يقرأ مدخلة المجلد، فتدفع مرة واحدة لا مرتين.

حذف شجرة مجلدات

يجب أن تكون المجلدات فارغة قبل أن يقبلها Files.delete(Path). لحذف شجرة بشكل تكراري، امشِ فيها بترتيب من الأسفل إلى الأعلى عبر عكس دفق مُرتَّب:

import java.util.Comparator; try (var tree = Files.walk(Path.of("tmp/scratch"))) { tree.sorted(Comparator.reverseOrder()) // أعمق المدخلات أولًا .forEach(p -> { try { Files.delete(p); } catch (Exception e) { throw new RuntimeException(e); } }); }
لا توجد سلة محذوفات. Files.delete دائمة. في كود الإنتاج تحقق من مسار الجذر بعناية قبل التكرار. متغير في المكان الخطأ يمكنه حذف أدلة مشاريع كاملة.

نسخ وتحريك أشجار المجلدات

Files.copy تنسخ مدخلة واحدة؛ ولا تتكرر. لنسخ شجرة بأكملها، امشِ فيها وأعد إنتاج البنية يدويًا:

Path src = Path.of("templates/base"); Path dest = Path.of("projects/new-app"); try (var tree = Files.walk(src)) { tree.forEach(source -> { Path target = dest.resolve(src.relativize(source)); try { if (Files.isDirectory(source)) { Files.createDirectories(target); } else { Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); } } catch (Exception e) { throw new RuntimeException(e); } }); }

الأسلوب المفتاحي هنا هو dest.resolve(src.relativize(source)): يجرّد بادئة الجذر المصدري ويلصق كل جزء متبقٍّ بجذر الوجهة — منتجًا المسار المعكوس دون تلاعب بالنصوص.

الخلاصة

  • استخدم Files.createDirectories في معظم الحالات — فهي idempotent وتُنشئ المجلدات الأب.
  • استخدم Files.list للسرد الضحل والكسول؛ أغلق الدفق دائمًا.
  • استخدم Files.walk للاجتياز التكراري وFiles.find حين تحتاج فلترة مبنية على البيانات الوصفية في نفس التمريرة.
  • احذف الأشجار من الأسفل إلى الأعلى بعكس دفق مُرتَّب؛ انسخ الأشجار بالمشي وحل المسارات النسبية.