تدفقات البايت مقابل تدفقات المحارف
تدفقات البايت مقابل تدفقات المحارف
تُبنى مكتبة الإدخال والإخراج في Java على تسلسلَين هرميَّين متوازيَين: تدفقات البايت وتدفقات المحارف. اختيار التسلسل الخاطئ هو أحد أبرز أسباب تلف النصوص، وفساد الملفات الثنائية، وأخطاء الترميز التي لا تظهر إلا على أنظمة تشغيل بعينها. يشرح هذا الدرس ما يفعله كل تسلسل، ولماذا يوجد كلاهما، وكيف تختار بينهما.
نظرة سريعة على التسلسلَين الهرميَّين
كل صنف إدخال/إخراج في حزمة java.io ينحدر من أحد أربعة جذور مجردة:
InputStream/OutputStream— تسلسل تدفقات البايت.Reader/Writer— تسلسل تدفقات المحارف.
تتعامل تدفقات البايت مع البايتات الخام بحجم 8 بتات — أي البتات الفعلية المخزنة على القرص أو المُرسَلة عبر الشبكة. أما تدفقات المحارف فتُضيف طبقة معالجة Unicode فوقها، وتُترجم تلقائيًا بين البايتات وتمثيل Java الداخلي للمحارف (UTF-16) باستخدام Charset.
تدفقات البايت: InputStream وOutputStream
الدوال المحورية بسيطة جدًا:
لأن كل بايت يُعامَل كقيمة غامضة دون أي تفسير ترميزي، فإن تدفقات البايت هي الأداة المناسبة لـ:
- ملفات الصور والصوت والفيديو.
- أرشيفات ZIP/JAR وملفات الصنف المُجمَّعة وملفات PDF.
- مقابس الشبكة (بيانات TCP هي دائمًا بايتات).
- أي حالة تحتاج فيها إلى تحكم كامل في نمط البتات الخام.
مثال بسيط — نسخ ملف ثنائي بمصفوفة بايتات (التخزين المؤقت مغطى في الدرس السابع؛ هذا المثال يعرض الواجهة البرمجية الخام):
read() قيمة int تحمل بايتًا خامًا واحدًا. في ملف UTF-8 يحتوي على إشارة اليورو € (3 بايتات: 0xE2 0x82 0xAC)، ستحصل على ثلاث قيم صحيحة منفصلة — لا على المحرف. تدفقات المحارف تُعالج التسلسلات متعددة البايتات نيابةً عنك.
تدفقات المحارف: Reader وWriter
يُعكس تسلسل تدفقات المحارف تسلسل البايتات، لكنه يعمل على قيم char في Java (وحدات ترميز UTF-16):
الصنفان الجسريان الأساسيان هما InputStreamReader وOutputStreamWriter. يُغلّفان تدفق بايتات مع Charset، ويُنفذان التحويل بين البايتات والمحارف:
من الناحية العملية نادرًا ما تستخدم InputStreamReader وOutputStreamWriter مباشرةً — إذ تُغلّفهما في BufferedReader/BufferedWriter أو تستخدم المصانع الجاهزة في Files (مغطاة لاحقًا). لكن فهم الجسر ضروري لمعرفة أين يحدث تحويل الترميز بالضبط.
حدد مجموعة المحارف دائمًا — ولا تعتمد على الافتراضي المنصّي
إن استدعيت new FileReader("file.txt") دون تحديد مجموعة محارف، يستخدم Java الترميز الافتراضي للمنصة — وهو UTF-8 على Linux/macOS الحديثة لكنه كان تاريخيًا CP1252 على Windows. قد يكون ملف كُتب على جهاز غير قابل للقراءة على جهاز آخر.
Charset صريحًا. منذ Java 11 يقبل كلا المُنشئَين وسيطة Charset:
الأصناف الفعلية التي ستستخدمها
تطبيقات تدفقات البايت:
FileInputStream/FileOutputStream— قراءة وكتابة الملفات كبايتات خام.ByteArrayInputStream/ByteArrayOutputStream— مخازن بايتات في الذاكرة (مفيدة جدًا في الاختبارات).DataInputStream/DataOutputStream— قراءة وكتابة أنواع Java الأولية (int،double، إلخ) بصيغة ثنائية قابلة للنقل.
تطبيقات تدفقات المحارف:
FileReader/FileWriter— أغلفة رفيعة حولInputStreamReader/OutputStreamWriterللملفات.StringReader/StringWriter— مخازن محارف في الذاكرة مدعومة بـString.PrintWriter— يُغلّف أيWriterويُضيفprintln/printf؛ هو المكافئ الإخراجي لـSystem.out.
قاعدة الاختيار
اطرح على نفسك سؤالًا واحدًا: هل هذه البيانات نص يستطيع إنسان قراءته في محرر نصوص؟
- نعم (كود مصدري، CSV، JSON، ملفات السجلات، HTML) ← استخدم تدفق محارف، دائمًا مع
Charsetصريح. - لا (صور، ثنائيات مُجمَّعة، بيانات مشفرة، أرشيفات مضغوطة) ← استخدم تدفق بايتات.
Files.readString(path, charset) أو Files.writeString(path, text, charset) أو Files.newBufferedReader(path, charset) بدلًا من تجميع سلاسل التدفقات يدويًا. هذه الدوال تُعالج التخزين المؤقت والترميز والإغلاق في استدعاء واحد. تحت الغطاء لا تزال تستخدم نفس تسلسل Reader/Writer — ومعرفة الأساسيات يُمكّنك من تشخيص الأخطاء حين تظهر.
أخطاء الترميز في الواقع العملي
هذا مثال مكسور عمدًا يوضح ما يحدث حين تخلط تدفقات البايت والمحارف بتهور:
الخلاصة
استخدم تدفقات البايت (عائلة InputStream/OutputStream) لأي بيانات ذات طبيعة ثنائية. استخدم تدفقات المحارف (عائلة Reader/Writer) لجميع البيانات النصية، وحدد دائمًا Charset صريحًا — StandardCharsets.UTF_8 هو الافتراضي الصحيح للكود الجديد. الصنفان الجسريان InputStreamReader وOutputStreamWriter هما الموضع الذي يجري فيه تحويل الترميز فعليًا؛ فكل تدفق محارف يقرأ أو يكتب بايتات في نهاية المطاف عبرهما.