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

قراءة الملفات

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

قراءة الملفات

تتيح Java عدة طرق لقراءة الملف، واختيار الطريقة المناسبة أمر جوهري. الاختيار الخاطئ قد يُحمّل جيجابايتات في الذاكرة الكومية، أو يُخلّف تدفقات مفتوحة مبعثرة في الكود. يتناول هذا الدرس المقاربات الثلاث الرئيسية — Files.readString وFiles.readAllLines وBufferedReader — ويشرح متى تلجأ إلى كل منها.

طريقتا الراحة: readString وreadAllLines

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

Files.readString تقرأ الملف بأكمله إلى سلسلة نصية واحدة من نوع String:

import java.nio.file.Files; import java.nio.file.Path; import java.nio.charset.StandardCharsets; import java.io.IOException; public class ReadStringDemo { public static void main(String[] args) throws IOException { Path path = Path.of("config.txt"); String content = Files.readString(path, StandardCharsets.UTF_8); System.out.println(content); } }

الوسيطة الثانية — وهي Charset — اختيارية؛ إن أغفلتها استخدمت Java الترميز الافتراضي للمنصة. مرّر دائمًا StandardCharsets.UTF_8 صراحةً كي يتصرف كودك بشكل متطابق على Windows وLinux وmacOS.

Files.readAllLines تقرأ كل سطر إلى قائمة List<String>، محذوفةً منها فواصل الأسطر (\n و\r\n و\r):

import java.nio.file.Files; import java.nio.file.Path; import java.nio.charset.StandardCharsets; import java.io.IOException; import java.util.List; public class ReadAllLinesDemo { public static void main(String[] args) throws IOException { Path path = Path.of("names.txt"); List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8); for (String line : lines) { System.out.println(line); } // أو باستخدام Stream API الذي تعرفه: lines.stream() .filter(l -> !l.isBlank()) .map(String::trim) .forEach(System.out::println); } }
فكرة جوهرية — ما تحتويه القائمة فعلًا: تُعيد readAllLines قائمة ArrayList عادية. تُحذف فواصل الأسطر، وتصبح الأسطر الفارغة سلاسل نصية خالية، ويُدرج السطر الأخير حتى لو لم ينتهِ الملف بسطر جديد. تكون القائمة كاملةً في الذاكرة قبل معالجة أي سطر.

المقايضة: الراحة مقابل الذاكرة

كلتا الطريقتين مريحتان، لكنهما تشتركان في خاصية جوهرية: يُحمَّل الملف بأكمله في الذاكرة قبل أن تتمكن من لمس بايت واحد. بالنسبة لملف إعداد بحجم 5 كيلوبايت أو ملف CSV بحجم 200 كيلوبايت فهذا مقبول تمامًا، أما بالنسبة لملف سجلات بحجم 2 جيجابايت فذلك انفجار للذاكرة ينتظر الحدوث.

  • Files.readString — الأنسب للنصوص البنيوية الصغيرة (الإعدادات، JSON، XML، القوالب) التي تريدها كتلةً واحدة.
  • Files.readAllLines — الأنسب للملفات السطرية المتوسطة الحجم عند الحاجة للوصول العشوائي إلى أي سطر أو تمرير القائمة الكاملة لدالة أخرى.
  • لا تُناسب أيٌّ منهما الملفات الكبيرة. استخدم BufferedReader عوضًا عنها.

BufferedReader للملفات الكبيرة

يقرأ BufferedReader جزءًا قابلًا للضبط (8 كيلوبايت افتراضيًا) من القرص في المرة الواحدة ويُسلّمك الأسطر سطرًا بسطر. يظل استخدام ذاكرة تطبيقك ثابتًا تقريبًا بغض النظر عن حجم الملف.

import java.io.BufferedReader; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; public class BufferedReaderDemo { public static void main(String[] args) throws IOException { Path path = Path.of("access.log"); // Files.newBufferedReader هو المصنع المفضل بأسلوب NIO.2 try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { String line; long errorCount = 0; while ((line = reader.readLine()) != null) { if (line.contains("ERROR")) { errorCount++; System.out.println(line); } } System.out.println("Total errors: " + errorCount); } // try-with-resources يضمن إغلاق القارئ هنا } }

ملاحظات جديرة بالانتباه:

  • Files.newBufferedReader(path, charset) هو المصنع المفضل على new BufferedReader(new FileReader(path)) — فالأخير يستخدم الترميز الافتراضي للمنصة ويتطلب مزيدًا من الشفرة المعيارية.
  • تُعيد readLine() القيمة null عند نهاية الملف لا استثناءً. نمط while (... != null) هو الأسلوب المعهود في Java.
  • يُغلق كتلة try-with-resources القارئَ حتى لو أُطلق استثناء في منتصف الملف — وهذا أمر جوهري للصحة عند التعامل مع الملفات الكبيرة.

بث الأسطر باستخدام Files.lines

إن كنت مرتاحًا للـ Stream API، فإن Files.lines (Java 8+) يمنحك Stream<String> كسولًا يتناسق جيدًا مع filter وmap وcollect:

import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.stream.Collectors; public class FilesLinesDemo { public static void main(String[] args) throws IOException { Path path = Path.of("access.log"); // try-with-resources إلزامي — التدفق يلفّ قارئًا مفتوحًا try (var lines = Files.lines(path, StandardCharsets.UTF_8)) { List<String> errors = lines .filter(l -> l.contains("ERROR")) .collect(Collectors.toList()); System.out.println("Errors found: " + errors.size()); } } }
لا تنسَ try-with-resources مع Files.lines. على خلاف readAllLines، يحتفظ التدفق بمعالج ملف مفتوح. إن أغفلت كتلة try-with-resources، يتسرّب المعالج حتى يُنهي مجمّع البيانات المهملة (GC) التدفقَ — وهو ما قد لا يحدث أبدًا في تطبيق طويل الأمد، مما قد يُنضب حد واصف ملفات نظام التشغيل.

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

  • ملف صغير، تحتاج النص كاملًاFiles.readString
  • ملف صغير، تحتاج كل سطر عنصرًا في قائمةFiles.readAllLines
  • ملف كبير، معالجة سطر بسطر بأسلوب إجرائيBufferedReader + readLine()
  • ملف كبير، معالجة بخط أنابيب StreamFiles.lines داخل try-with-resources
قاعدة عملية لحجم الملف: إن كان الملف يُحتمل أن يتجاوز بضع مئات من الميجابايت في بيئة الإنتاج، فعامله باعتباره "كبيرًا" وتجنّب طريقتَي الراحة. مهمة معالجة سجلات خادم ويب في الخلفية، أو خط أنابيب استيراد بيانات، أو أداة بناء تُجمّع ملفات مصدر كثيرة — كلها سيناريوهات ملفات كبيرة يكون فيها BufferedReader أو Files.lines هو الاختيار الصحيح.

الخلاصة

تُقدّم Files.readString وFiles.readAllLines شفرةً مختصرة ونظيفة للملفات الصغيرة على حساب تحميل كل شيء في الذاكرة. يمنحك BufferedReader عبر Files.newBufferedReader معالجةً سطر بسطر بذاكرة ثابتة لملفات ضخمة تعسفيًا. يردم Files.lines الفجوة حين تريد البث الكسول مع كامل قدرة Stream API. حدّد دائمًا مجموعة المحارف صراحةً، وأغلق دائمًا موارد الإدخال/الإخراج باستخدام try-with-resources.