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

كتابة الملفات

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

كتابة الملفات

قراءة الملف ليست سوى نصف القصة. معرفة كيفية الكتابة — واختيار الأداة المناسبة — لا تقل أهمية. تمنحك Java عدة خيارات: الدوال الحديثة Files.write وFiles.writeString من NIO.2، والكلاسيكي BufferedWriter المبني على واجهة I/O القديمة. لكل منها مجاله المثالي، وكل منها يتيح لك الاختيار بين الكتابة فوق الملف الحالي والإلحاق بنهايته.

الطريق السريع: Files.write و Files.writeString

منذ Java 7 مع NIO.2، توفّر java.nio.file.Files دوالًا ساكنة تفتح الملف وتكتب فيه وتغلقه بنداء واحد، دون الحاجة إلى إدارة التدفقات يدويًا.

كتابة قائمة من الأسطر:

import java.nio.file.Files; import java.nio.file.Path; import java.nio.charset.StandardCharsets; import java.util.List; Path target = Path.of("notes.txt"); List<String> lines = List.of( "First line", "Second line", "Third line" ); // يكتب فوق الملف (ينشئه إذا لم يكن موجودًا) Files.write(target, lines, StandardCharsets.UTF_8);

كل عنصر في القائمة يصبح سطرًا واحدًا؛ تضيف الدالة فاصل السطر المناسب للنظام تلقائيًا بعد كل عنصر. يُنشأ الملف إذا لم يكن موجودًا شرط وجود المجلد الأب.

كتابة مصفوفة بايتات خام: Files.write مُحمَّلة أيضًا لقبول byte[]، وهو مفيد للبيانات الثنائية أو حين تمتلك البايتات مسبقًا:

byte[] data = "Hello, World!".getBytes(StandardCharsets.UTF_8); Files.write(Path.of("hello.bin"), data);

كتابة سلسلة نصية كاملة دفعة واحدة (Java 11+):

import java.nio.file.Files; import java.nio.file.Path; String content = """ # Shopping List - Milk - Bread - Eggs """; Files.writeString(Path.of("shopping.txt"), content);

أُضيفت Files.writeString في Java 11 كتسهيل للحالة الشائعة جدًا: كتابة String واحد. تعتمد UTF-8 افتراضيًا وهي أقصر طريق من قيمة نصية إلى ملف على القرص.

حدّد الترميز دائمًا بصراحة. تقبل كلتا الدالتين Files.write وFiles.writeString وسيطة Charset اختيارية. حذفها يعني UTF-8 (الافتراضي لدوال NIO.2) وهو آمن في الغالب — لكن التصريح به يوثّق نيّتك ويمنع المفاجآت على الأنظمة النادرة ذات الترميز الافتراضي المختلف.

الإلحاق مقابل الكتابة فوق: StandardOpenOption

بشكل افتراضي تُكتب Files.write وFiles.writeString فوق الملف بالكامل. مرّر StandardOpenOption لتغيير ذلك:

import java.nio.file.StandardOpenOption; // APPEND — يضيف إلى النهاية بدلًا من الاستبدال Files.writeString( Path.of("log.txt"), "New log entry\n", StandardOpenOption.APPEND ); // CREATE_NEW — يرمي استثناءً إذا كان الملف موجودًا بالفعل Files.writeString( Path.of("report.txt"), reportContent, StandardOpenOption.CREATE_NEW );

أبرز الخيارات التي ستستخدمها:

  • WRITE — فتح للكتابة (ضمني عند استدعاء دوال الكتابة).
  • CREATE — أنشئ الملف إن لم يكن موجودًا، افتحه إن كان (السلوك الافتراضي).
  • CREATE_NEW — أنشئ فقط؛ ارمِ FileAlreadyExistsException إن كان موجودًا. مفيد لضمان عدم الكتابة فوق ملف موجود.
  • APPEND — انقل مؤشر الكتابة إلى النهاية قبل كل عملية كتابة. استخدمه مع CREATE لبناء ملف سجلات بأمان.
  • TRUNCATE_EXISTING — اقطع الملف إلى الصفر عند الفتح (هذا ما يجعل السلوك الافتراضي كتابةً فوق الملف).
APPEND ليست آمنة للخيوط على مستوى التطبيق. إذا كتب خيطان (أو عمليتان) في الملف نفسه بـ APPEND في الوقت ذاته، فنداءات الكتابة الفردية ذرية على مستوى نظام التشغيل في معظم المنصات، لكن ترتيب الإدخالات غير محدد. للتسجيل متعدد الخيوط استخدم إطار تسجيل مخصصًا أو غلّف الكتابة بـ synchronized.

BufferedWriter: حين تحتاج إلى كتابة تدريجية

Files.write مثالية حين تمتلك البيانات كلها مسبقًا — فهي تجمّع المحتوى كله في الذاكرة قبل الكتابة. حين تحتاج إلى بناء الملف تدريجيًا (كتابة سطر سطر داخل حلقة مثلًا)، فـ BufferedWriter هي الأداة المناسبة. تتراكم الكتابات في مخزن مؤقت في الذاكرة (8 كيلوبايت افتراضيًا) وتُفرغ إلى القرص في قطع كبيرة كفؤة بدلًا من استدعاء النظام عند كل سطر.

import java.io.BufferedWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; Path out = Path.of("numbers.txt"); try (BufferedWriter writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) { for (int i = 1; i <= 10_000; i++) { writer.write("Line " + i); writer.newLine(); // فاصل سطر صحيح للمنصة } } // يُغلَق تلقائيًا؛ يُفرَّغ المخزن المؤقت ويُغلَق الملف هنا

ملاحظات مهمة:

  • Files.newBufferedWriter هو المصنع الخاص بـ NIO.2 — فضّله على new BufferedWriter(new FileWriter(...)) لأنه يقبل Path وCharset مباشرة.
  • writer.newLine() يكتب فاصل السطر الخاص بالمنصة (\r\n على Windows، \n على Unix). تجنّب ترميز "\n" مباشرة في الملفات التي ستُقرأ على منصات متعددة.
  • كتلة try-with-resources تضمن تفريغ المخزن المؤقت وتحرير مقبض الملف حتى لو رُمي استثناء داخل الحلقة.

الإلحاق مع BufferedWriter

للإلحاق بدلًا من الكتابة فوق الملف، مرّر StandardOpenOption.APPEND إلى المصنع:

try (BufferedWriter writer = Files.newBufferedWriter( Path.of("audit.log"), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { writer.write("[INFO] Application started"); writer.newLine(); }

تمرير كل من CREATE وAPPEND معًا هو النمط الاصطلاحي: أنشئ الملف إن لم يكن موجودًا، وألحق به إن كان موجودًا.

اختيار الأداة المناسبة

إليك قاعدة عملية للقرار:

  • محتوى صغير موجود في الذاكرة (سلسلة نصية، قائمة أسطر، مصفوفة بايتات) → استخدم Files.writeString أو Files.write. سطر واحد، لا تدفقات تُدار.
  • محتوى كبير أو توليد تدريجي (حلقة، نتائج متدفقة، بناء تقرير سطرًا سطرًا) → استخدم BufferedWriter عبر Files.newBufferedWriter. المخزن المؤقت يمنع استدعاءات النظام الزائدة.
  • بيانات ثنائية → استخدم Files.write(path, byte[]) أو Files.newOutputStream مغلّفًا بـ BufferedOutputStream.
لماذا يهم التخزين المؤقت للأداء؟ كل كتابة غير مخزّنة تصل إلى نظام التشغيل هي استدعاء نظام، وهذه الاستدعاءات مكلفة مقارنة بعمليات الذاكرة الداخلية. ملف بـ 10,000 سطر مكتوب سطرًا واحدًا دون تخزين مؤقت قد يُجري 10,000 استدعاء نظام. مع BufferedWriter (مخزن 8 كيلوبايت) تتقلص هذه الاستدعاءات إلى 5–10 عمليات تفريغ تقريبًا، وهو أسرع بمراتب على الأقراص الدوّارة وأسرع ملحوظًا على أقراص SSD.

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

تُرمي جميع دوال الكتابة في NIO.2 IOException (استثناء محسوب). يجب إما التقاطه أو الإعلان عنه في توقيع الدالة. نمط بسيط لدالة مساعدة:

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; public static void saveReport(Path path, String content) throws IOException { Files.writeString(path, content); }

في كود التطبيق حيث لا يمكن تمرير الاستثناء المحسوب (مثلًا داخل تعبير lambda)، غلّفه باستثناء غير محسوب:

import java.io.UncheckedIOException; paths.forEach(p -> { try { Files.writeString(p, generate(p)); } catch (IOException e) { throw new UncheckedIOException(e); } });

الخلاصة

استخدم Files.writeString للحالة الأبسط: سلسلة نصية واحدة، نداء واحد، UTF-8، انتهى. استخدم Files.write حين تمتلك قائمة أسطر أو مصفوفة بايتات. استخدم BufferedWriter من Files.newBufferedWriter حين تكتب تدريجيًا أو تهتم بالأداء على الحجم. تحكّم في الكتابة فوق الملف مقابل الإلحاق بـ StandardOpenOption. استخدم دائمًا try-with-resources لضمان إغلاق الملف وتفريغ المخزن المؤقت.