تُصبِح البرامج عديمة الفائدة إذا لم تكن قادرةً على التعامل مع العالم الخارجي بشكلٍ أو بآخر، حيث يُشار إلى تعامل البرامج مع العالم الخارجي باسم "الدْخَل والخرج أو I/O". يُعدّ توفير إمكانياتٍ جيدةٍ لعمليات الدْخَل والخرج واحدًا من أصعب التحديات التي تواجه مُصمِّمي اللغات البرمجية، حيث يَستطيِع الحاسوب الاتصال مع أنواعٍ مختلفةٍ كثيرة من أجهزة الدخل والخرج.
إذا اضطّرت لغة البرمجة للتعامل مع كل نوعٍ منها على حدة، لكان الأمر غايةً في التعقيد، ولهذا يُعدّ التمثيل المُجرّد لأجهزة الدخل والخرج واحدًا من أعظم الإنجازات بتاريخ البرمجة، ويُطلَق على ذلك التمثيل المُجرّد بلغة جافا اسم مجاري تدفق الدْخَل والخرج I/O streams. تتوفَّر تجريداتٌ أخرى، مثل الملفات والقنوات، ولكننا سنناقش مجاري التدفق فقط، حيث يُمثِّل كل مجرًى مصدرًا يُقرَأ منه الدْخَل أو مقصدًا يُرسَل إليه الخرج.
مجاري تدفق البايتات Byte Streams ومجاري تدفق المحارف Character Streams
عندما تتعامل مع المُدْخَلات والمخرجات، تذكَّر أن هناك نوعان من البيانات في العموم؛ بياناتٌ مُهيأةٌ للآلة؛ وبياناتٌ مهيأةٌ لنا بمعنى أنها قابلةٌ للقراءة. تُكتَب الأولى بالصيغة الثنائية binary بنفس الطريقة التي تُخزَّن بها البيانات داخل الحاسوب، أي بهيئة سلاسلٍ نصيةٍ مُكوَّنةٍ من "0" و "1"؛ بينما تُكتَب الثانية بهيئة محارف. فعندما تقرأ عددًا، مثل "3.141592654"، فأنت في الواقع تقرأ متتاليةً من المحارف، ولكنك تُفسِّرها عددًا؛ بينما يُمثِّل الحاسوب نفس ذلك العدد بهيئة سلسلةٍ نصيةٍ من البتات أي أنك لن تتمكَّن من تمييزها.
تُوفِّر جافا نوعين من مجاري التدفق streams للتعامل مع البيانات المُمثَلة بالصيغتين السابقتين: مجرى بايتات byte streams للبيانات المُهيأة للآلة، ومجرى محارف character streams للبيانات القابلة للقراءة. ستَجِد أصنافًا مُعرَّفةً مُسبقًا تُمثِّل المجاري من كلا النوعين.
تنتمي الكائنات المُرسِلة للبيانات إلى مجرى بايت إلى أحد الأصناف الفرعية subclasses المُشتقَّة من الصنف المُجرَّد OutputStream
؛ بينما تنتمي الكائنات القارئة للبيانات من هذا النوع من المجاري إلى أحد الأصناف الفرعية المُشتقَّة من الصنف المُجرَّد InputStream
. إذا أرسلت أعدادًا إلى كائنٍ من الصنف OutputStream
، لن تتمكَّن من قراءة البيانات الناتجة بنفسك. في المقابل، ما يزال بإمكان الحاسوب قرائتها مُجدَّدًا عبر كائنٍ من الصنف InputStream
. تعمَل عمليتي قراءة البيانات وكتابتها في تلك الحالة بكفاءة لعدم استخدامهما أي ترجمة؛ حيث تُنسَخ فقط البتات المُمثِلة للبيانات بالحاسوب من مجاري التدفق وإليها.
في المقابل، يتولَّى الصنفان المجرَّدان Reader
وWriter
قراءة البيانات القابلة للقراءة وكتابتها على الترتيب، فجميع أصناف مجارى المحرف هي مجرد أصنافٍ فرعيةٍ مُشتقَّةٍ من هذين الصنفين. إذا أرسلت عددًا إلى مجرًى من النوع Writer
، فيجب أن يُترجمها الحاسوب إلى متتاليةٍ من المحارف المُمثِلة لذلك العدد والقابلة للقراءة؛ بينما تنطوي عملية قراءة عددٍ من مجرًى من النوع Reader
، وتخزينها بمُتغيِّرٍ عددي على عملية ترجمةٍ من متتالية محارف إلى سلسلة بتاتٍ مناسبة. حتى لو كانت البيانات التي تتعامل معها مُكوَّنةً من محارفٍ بالأساس، مثل بعض الكلمات من برنامج معدِّل نصوص، من الممكن أن يتضمَّن الأمر بعضًا من الترجمة أيضًا.
يُخزِّن الحاسوب المحارف على انها قيم يونيكود Unicode من 16 بت، وتُخزّن حروف الأبجدية الإنجليزية عمومًا بملفات بشيفرة ASCII، التي تَستخدِم 8 بتات للمحرف الواحد. يتولى الصنفان Reader
وWriter
أمر تلك الترجمة، كما يمكنهما معالجة الحروف الأبجدية الأخرى، وكذلك المحارف من غير الحروف الأبجدية المكتوبة بلغاتٍ، مثل الصينية.
تُستَخدَم مجاري تدفق البايتات للاتصال المباشر بين الحواسيب، كما أنها تكون مفيدةً أحيانًا لتخزين البيانات ضمن ملفات، بالأخص عندما نحتاج إلى تخزين أحجامٍ هائلةٍ من البيانات بطريقةٍ فعالة؛ كما هو الحال مع قواعد البيانات الضخمة. ومع ذلك، تُعدّ البيانات الثنائية هشة نوعًا ما، فهي لا تعبُر بذاتها عن معناها.
عندما تتعامل مع سلسلةٍ طويلةٍ من العددين صفر وواحد، ينبغي أن تُعرِّف أولًا نوعية المعلومات المُفترَض لتلك السلسلة أن تُمثِّلها، وكذلك أن تَعرِّف الكيفية التي رُمزَّت بها المعلومات قبل أن تتمكَّن من تفسيرها. ينطبق الأمر نفسه بالطبع على البيانات المحرفية نوعًا ما؛ فالمحارف بالنهاية مثلها مثل أي نوعٍ من البيانات، وينبغي أن تُرمَّز مثل أعدادٍ ثنائية حتى يتمكَّن الحاسوب من تخزينها ومعالجتها، ولكن الترميز الثنائي للبيانات المحرفية على الأقل مُوحدٌ ومفهوم، بل حتى يُمكِننا أن نجعل البيانات بصيغتها المحرفية ذات معنًى للقارئ. يتجه التيار العام إلى استخدام البيانات المحرفية، وتمثيلها بطريقةٍ تجعلها مُفسَّرةً ذاتيًا قدر الإمكان، وسنناقش إحدى تلك الطرائق في مقال مقدمة مختصرة للغة XML.
لا يدعم الإصدار الأصلي من جافا مجاري المحارف، حيث يُمكِن لمجاري البايتات أن تحلّ محل مجاري المحارف عند التعامل مع البيانات المُرمزَّة بشيفرة ASCII. يُعدُّ مجريا الدخل القياسي System.in
والخرج القياسي System.out
مجاري بايتات، وليس مجاري محارف؛ ومع ذلك يُحبَّذ استخدام الصنفين Reader
وWriter
على الصنفين InputStream
وOutputStream
عند التعامل مع البيانات المحرفية، وحتى عند التعامل مع مجموعة محارف ASCII القياسية.
تقع أصناف مجاري الدخل والخرج القياسية -والتي سنناقشها ضمن هذا المقال - بحزمة java.io
بالإضافة إلى عددٍ من الأصناف الأخرى. يجب أن تستورد import أصناف تلك الحزمة إذا أردت اِستخدَامها ضمن البرنامج؛ أي إما أن تستورد الأصناف المطلوبة بصورةٍ فردية؛ أو أن تْكْتُب المُوجِّه import java.io.*;
في بداية الملف المصدري.
تُستخدَم مجاري الدخل والخرج عند التعامل مع الملفات، وعند الاتصال الشبكي، وكذلك للاتصال بين الخيوط المُتزامنة concurrent threads. تتوفَّر أيضًا أصناف مجاري لقراءة البيانات وكتابتها من وإلى ذاكرة الحاسوب.
اقتباسملاحظة: تُوفِّر واجهة برمجة تطبيقات جافا دعمًا إضافيًا لعمليات الدخل والخرج عبر حزمة
java.nio
وحزمها الفرعية subpackages، ولكننا لن نناقشها هنا. تُوفِّر تلك الحزمة بعضًا من التقنيات المُتقدِّمة لعمليات الدخل والخرج في العموم.
تَكْمُن فعالية المجاري وأناقتها بكونها تُجرِّد عملية كتابة البيانات؛ حيث تُصبِح عملياتٍ مثل كتابة بياناتٍ إلى ملف أو إرسالها عبر شبكةٍ بنفس سهولة طباعة تلك البيانات على الشاشة.
تُوفِّر أصناف الدخل والخرج Reader
وWriter
وInputStream
وOutputStream
العمليات الأساسية فقط، حيث يُصرِّح الصنف InputStream
مثلًا عن تابع النسخة instance method المُجرَّد التالي:
public int read() throws IOException
يقرأ هذا التابع بايتًا واحدًا من مجرى دْخَلٍ بهيئة عددٍ يقع بنطاقٍ يتراوح بين "0" و "255"، ويُعيد القيمة "-1" عند وصوله إلى نهاية المجرى. إذا حدث خطأٌ أثناء عملية الدخل، يقع استثناء exception من النوع IOException
، ونظرًا لكونه من الاستثناءات المُتحقَّق منها checked exceptions، لا بُدّ من استخدام التابع read()
ضمن تعليمة try
أو ببرنامجٍ فرعي subroutine يتضمَّن تصريحه عبارة throws IOException
. انظر مقال الاستثناءات exceptions وتعليمة try..catch في جافا للمزيد من المعلومات عن الاستثناءات المُتحقَّق منها والمعالجة الاجبارية للاستثناءات.
يُعرِّف الصنف InputStream
أيضًا توابعًا لقراءة عدة بايتات من البيانات ضمن خطوةٍ واحدة، وتخزينها بمصفوفة بايتات، وهو ما يُعدّ أكثر كفاءة بكثير من قرائتها بصورةٍ إفرادية؛ ولكنه -أي الصنف InputStream
- مع ذلك لا يُوفِّر أي توابعٍ لقراءة أنواعٍ أخرى من البيانات، مثل int
وdouble
من مجرى. لا يُمثِل ذلك مشكلة؛ حيث من النادر أن تستخدِم كائناتٍ من النوع InputStream
، وإنما ستعتمد على أصنافٍ فرعية منه. تُعرِّف تلك الأصناف توابع دْخَلٍ إضافية إلى جانب الإمكانيات الأساسية للصنف InputStream
، كما يُعرِّف بالمثل الصنف OutputStream
تابع الخرج التالي لكتابة بايت واحد إلى مجرى خرج:
public void write(int b) throws IOException
لاحِظ أن المعامل parameter من النوع int
، وليس من النوع byte
، ولكنه يُحوَّل type-cast إلى النوع byte
قبل كتابته، وهو ما يؤدي إلى إهمال جميع بتات المعامل b
باستثناء البتات الثمانية الأقل رتبة. عمليًا، ستَستخدِم دائمًا أصنافًا فرعية مُشتقَّة من الصنف OutputStream
، والتي تُعرِّف عمليات خرجٍ إضافية عالية المستوى.
يُوفِّر الصنفان Reader
وWriter
توابعًا منخفضة المستوى مشابهة لعمليتي read
وwrite
. وكما هو الحال مع أصناف مجاري البايتات، ينتمي كلٌ من معامل التابع write(c)
المُعرَّف بالصنف Writer
، والقيمة المعادة من التابع read()
المُعرَّف بالصنف Reader
إلى النوع int
، ولكن ما يزال هناك اختلاف؛ حيث تُجرَى بتلك الأصناف المُخصَّصة بالأساس للمحارف عمليتي الدخل والخرج على المحارف، وليس على البايتات. يعيد التابع read()
القيمة "-1" عند وصوله إلى نهاية المجرى، أما قبل ذلك، فيجب أن نُحوَّل القيمة المعادة منه إلى النوع char
لنَحصُل على المحرف المقروء. عمليًا، ستَستخدِم عادةً أصنافًا فرعيةً مُشتقَّةً من الصنفين Reader
وWriter
، والتي تُعرِّف عمليات دْخَل وخَرْج إضافية عالية المستوى، كما سنناقش فيما يلي.
الصنف PrintWriter
تُمكِّنك حزمة جافا للدخل والخرج من إضافة إمكانياتٍ جديدة إلى مجاري التدفق من خلال تغليفها wrapping ضمن كائنات مجاري تدفقٍ أخرى تُوفِّر تلك الإمكانيات. يكون الكائن المُغلِّف مجرًى أيضًا؛ أي يُمكِنك أن تقرأ منه أو تكتب به، ولكن عبر عملياتٍ أكثر فعالية من تلك المتاحة بمجاري التدفق الأصلية.
يُعدّ الصنف PrintWriter
على سبيل المثال صنفًا فرعيًا من الصنف Writer
، ويُوفِّر توابعًا لإخراج جميع أنواع البيانات الأساسية بلغة جافا بصيغة محارف مقروءة. إذا كان لديك كائنٌ منتميٌ إلى الصنف Writer
أو أيٍّ من أصنافه الفرعية، وأردت استخدام توابع الصنف PrintWriter
لعمليات الخرج الخاصة بذلك الكائن؛ فكل ما عليك فعله هو تغليف كائن الصنف Writer
بكائن الصنف PrintWriter
، وذلك بتمريره إلى باني الكائن constructor المُعرَّف بالصنف PrintWriter
. بفرض أن charSink
من النوع Writer
، يُمكِنك كتابة ما يَلِي:
PrintWriter printableCharSink = new PrintWriter(charSink);
يُمكِن للمعامل المُمَّرر إلى الباني أن يكون من النوع OutputStream
أو النوع File
، وهذا ما سنناقشه في المقال التالي؛ حيث يُنشِئ الباني في العموم كائنًا من النوع PrintWriter
، والذي يكون بإمكانه الكتابة إلى مقصد الخرج الخاص بالكائن المُمرَّر إليه. عندما تُرسِل بيانات خرجٍ إلى printableCharSink
عبر إحدى توابع الخرج عالية المستوى المُعرَّفة بالصنف PrintWriter
، فستُرسَل تلك البيانات إلى نفس المقصد الذي يُرسِل charSink
البيانات إليه؛ فكل ما فعلناه هو توفير واجهة أفضل لنفس مقصد الخرج، وهذا يَسمَح لنا باستخدام توابع الصنف PrintWriter
لإرسال البيانات إلى ملفٍ أو عبر اتصالٍ شبكي مثلًا.
إذا كان out
مُتغيّرًا من النوع PrintWriter
، فإنه إذًا يُعرِّف التوابع التالية:
-
out.print(x)
: يُرسِل قيمة المعاملx
بهيئة سلسلةٍ نصيةٍ من المحارف إلى مجرى الخرج، ويُمكِن للمعاملx
أن يكون تعبيرًا expression من أي نوع، بما في ذلك الأنواع الأساسية primitive types والأنواع الكائنية؛ حيث يُحوِّل التابع أي كائنٍ إلى سلسلةٍ نصيةٍ عبر تابعهtoString()
. تُمثَّل القيمة الفارغةnull
بالسلسلة النصية "null". -
out.println()
: يُرسِل مِحرف سطرٍ جديد إلى مجرى الخرج. -
out.println(x)
: يُرسِل قيمةx
متبوعةً بسطرٍ جديد، وهو ما يُكافِئ استدعاء التابعينout.print(x)
وout.println()
على التوالي. -
out.printf(formatString, x1, x2, ...)
: يُرسِل خرجًا مُنسَّقًا للمعاملات المُمرَّرةx1
وx2
و .. وهكذا إلى مجرى الخرج. يمثِّل المعامل الأول سلسلةً نصيةً تُخصِّص صيغة الخرج المطلوبة. إلى جانب ذلك، يَستقبِل التابع أي عددٍ من المعاملات الإضافية التي يُمكِنها أن تنتمي لأي نوع، بشرط أن تتوافق مع صيغة الخرج المُخصَّصة بالمعامل الأول. ألقِ نظرةً على قسم الخرج البسيط والخرج المنسق من مقال المدخلات والمخرجات النصية في جافا للمزيد من المعلومات عن الخرج المُنسَّق فيما يتعلَّق بمجرى الخرج القياسيSystem.out
، ويُوفِّر التابعout.printf
نفس الوظيفة. -
out.flush()
: يتأكَّد من كتابة المحارف المُرسلة عبر أيٍّ من التوابع السابقة إلى مقصدها بصورةٍ فعليّة. يكون استدعاء هذا التابع ضروريًا في بعض الحالات بالأخص عند إرسال الخرج إلى ملفٍ أو عبر شبكة، وذلك لضمان ظهور الخرج بالمقصد المُحدَّد.
لا تُبلِّغ أيٌ من التوابع السابقة عن استثناءٍ من النوع IOException
نهائيًا. بدلًا من ذلك، يتضمَّن الصنف PrintWriter
التابع التالي:
public boolean checkError()
يعيد هذا التابع القيمة true
في حالة حدوث خطأٍ أثناء عملية الكتابة بمجرى؛ حيث يلتقط الصنف PrintWriter
أي استثناءات من النوع IOException
، ثم يَضبُط قيمة رايةٍ flag داخليةٍ معينةٍ للإشارة إلى وجود خطأ. يُمكِنك إذًا استخدام التابع checkError()
لفحص قيمة تلك الراية، وذلك من خلال استخدام توابع الصنف PrintWriter
دون الحاجة لالتقاط أي استثناءات؛ ومع ذلك، إذا كنت تريد كتابة برنامج متين تمامًا، فيجب أن تستدعي التابع checkError()
عند استخدام أيٍّ من توابع الصنف PrintWriter
لتَتأكَّد من عدم وقوع أي أخطاءٍ مُحتمَلة.
مجاري تدفق البيانات Data Streams
عندما نَستخدِم الصنف PrintWriter
لإرسال بياناتٍ إلى مجرًى معيّن، فسيُحوِّل البيانات إلى متتاليةٍ مقروءةٍ من المحارف المُمثِّلة لتلك البيانات. ماذا لو أردنا إرسال البيانات بصيغةٍ ثنائيةٍ مهيأةٍ للآلة؟ في الواقع، تتضمَّن حزمة java.io
الصنف DataOutputStream
المُمثِّل لمجرى بايتات، والذي يُمكِننا استخدامه لإرسال البيانات إلى المجاري بهيئةٍ ثنائية.
تُعدّ العلاقة بين الصنفين DataOutputStream
وOutputStream
مشابهةً لتلك الموجودة بين الصنفين PrintWriter
وWriter
؛ فبينما يَملُك الصنف OutputStream
توابع الخرج المُخصَّصة للبايتات فقط؛ يملك الصنف DataOutputStream
التابع writeDouble(double x)
لقيم الخرج من النوع double
، والتابع writeInt(int x)
لقيم الخرج من النوع int
، وهكذا.
علاوةً على ذلك، من الممكن أيضًا تغليف أي كائنٍ من النوع OutputStream
ضمن كائنٍ من النوع DataOutputStream
؛ لنتمكَّن من استخدام توابع الخرج عالية المستوى المُعرَّفة به. إذا كان byteSink
من النوع OutputStream
مثلًا، يُمكِن كتابة ما يَلي لتغليفِه ضمن كائنٍ من النوع DataOutputStream
:
DataOutputStream dataSink = new DataOutputStream(byteSink);
تُوفِّر حزمة java.io
الصنف DataInputStream
بالنسبة للمُدْخلات المُهيأة للآلة، مثل تلك التي يُنشئها DataOutputStream
عند اِستخدَامه للكتابة. يُمكِنك تغليف كائنٍ من النوع InputStream
ضمن كائنٍ من النوع DataInputStream
؛ لتُمكِّنه من قراءة أي نوعٍ من البيانات من مجرى بايتات. أسماء توابع الصنف DataInputStream
المسؤولة عن قراءة البيانات الثنائية هي: readDouble()
وreadInt()
وهكذا.
يكْتُب الصنف DataOutputStream
البيانات بصيغةٍ يُمكِن للصنف DataInputStream
أن يقرأها بالضرورة، حتى لو أنشأ حاسوبٌ من نوعٍ معين المجرى، وكان المطلوب أن يقرأه حاسوبٌ من نوعٍ آخر. تُوفِّر البيانات الثنائية توافقًا compatibility عبر المنصات، ويُعدُّ هذا أحد الجوانب الأساسية لاستقلالية منصة جافا.
قد ترغب في بعض الحالات بقراءة محارفٍ من مجرًى من النوع InputStream
، أو كتابة محارفٍ إلى مجرًى من النوع OutputStream
، ولا يُمثِل ذلك مشكلةً لأن المحارف مثلها مثل جميع البيانات؛ فهي تُمثَّل بهيئة أعدادٍ ثنائيةٍ، على الرغم أنه من الأفضل في تلك الحالة استخدام الصنفين Reader
وWriter
، بدلًا من InputStream
وOutputStream
. مع ذلك، تستطيع فعل ذلك بتغليف مجرى البايتات ضمن مجرى محارف.
إذا كان byteSource
متغيرًا من النوع InputStream
وكان byteSink
مُتغيرًا من النوع OutputStream
، تُنشِئ التعليمات التالية مجاري محارف بإمكانها قراءة المحارف وكتابتها من وإلى مجاري بايتات.
Reader charSource = new InputStreamReader( byteSource ); Writer charSink = new OutputStreamWriter( byteSink );
يُمكِننا تحديدًا تغليف مجرى الدخل القياسي System.in
، المُنتمي إلى الصنف InputStream
لأسبابٍ تاريخية، ضمن كائنٍ من النوع Reader
، لتسهيل قراءة المحارف من الدخل القياسي كما يلي:
Reader charIn = new InputStreamReader( System.in );
لنأخذ مثالًا آخر؛ حيث تُعدّ مجاري الدخل والخرج المُرتبطِة باتصالٍ شبكي مجاري بايتات لا مجاري محارف، ويُمكننا مع ذلك تغليف مجاري البايتات بمجاري محارف للتسهيل من إرسال البيانات المحرفية واستقبالها عبر الشبكة. سنناقش عمليات الدخل والخرج عبر الشبكة لاحقًا.
تتوفَّر طرائقٌ مختلفة لترميز المحارف بهيئة بياناتٍ ثنائية، حيث يُطلَق مُصطلح "طقم محارف charset" على أي ترميز محارف، ويَملُك اسمًا قياسيًا، مثل "UTF-16" و "UTF-8" و "ISO-8859-1"؛ حيث يُرمِّز "UTF-16" المحارف بهيئة قيم يونيكود Unicode مُكوَّنةٍ من "16 بت"، وهو الترميز المُستخدَم داخليًا بجافا؛ بينما يُعدّ "UTF-8" أسلوبًا لترميز محارف اليونيكود بتخصيص "8 بت" لمحارف ASCII الشائعة في مقابل عدد بتاتٍ أكثر للمحارف الأخرى؛ أما ترميز "ISO-8859-1" المعروف أيضًا باسم "Latin-1"، فهو مكوَّنٌ من "8 بت"، ويتضمَّن محارف ASCII إلى جانب محارفٍ أخرى مُستخدَمةٍ ضمن عدة لغاتٍ أوروبية.
يَعتمِد الصنفان Reader
وWriter
على طقم المحارف الافتراضي ضمن الحاسوب المُشّغلان عليه، إلا إذا خصَّصت طقم محارفٍ معين بتمريره عبر الباني على النحو التالي:
Writer charSink = new OutputStreamWriter( byteSink, "ISO-8859-1" );
يؤدي اختلاف ترميزات أطقم المحارف وكثرتها إلى تعقيد عملية معالجة النصوص، وهو ما يُعدّ أمرًا سيئًا للمتحدثين بالإنجليزية، ولكنه ضروري لغيرهم ممن يَستخدِمون أطقم محارفٍ مختلفة. لا حاجة للقلق عمومًا بشأن أيٍّ من ذلك، إنما عليك فقط أن تتذكَّر أن هناك أطقم محارفٍ مختلفة إذا واجهت بياناتٍ نصيةٍ مُرمزَّة بطريقةٍ غير اعتيادية.
قراءة النصوص
تُجرَى كثيرٌ من عمليات الدخل والخرج على محارفٍ مقروءة، ومع ذلك، لا توفِّر جافا صنفًا قياسيًا يُمكِنه قراءة المحارف بإمكانياتٍ متكافئة مع ما يُوفِّره الصنف PrintWriter
لإخراج المحارف. قد يَكون الصنف Scanner
-الذي تعرَّضنا له في مقال المدخلات والمخرجات النصية في جافا، والذي سنناقشه تفصيليًا فيما يلي مكافئًا نوعًا ما، ولكنه ليس صنفًا فرعيًا من أي صنف مجرى؛ ما يعني أنه لا يتناسب مع إطار عمل مجاري التدفق. هناك مع ذلك حالةٌ بسيطةٌ بإمكان الصنف القياسي BufferedReader
معالجتها بسهولة. يتضمَّن هذا الصنف التابع التالي:
public String readLine() throws IOException
يقرأ هذا التابع سطرًا نصيًا واحدًا من المُدْخلات، ويقرأ خلال ذلك مؤشر نهاية السطر أيضًا، ولكن لا يكون هذا المؤشر جزءًا من السلسلة النصية التي يعيدها التابع؛ بينما يُعيد التابع القيمة null
عند وصوله إلى نهاية المجرى. تَستخدِم الأنواع المختلفة من مجاري الدْخَل محارفًا مختلفةً للإشارة إلى نهاية السطر، ولكن يُمكِن للتابع readLine
التعامُل مع أغلب الحالات الشائعة. تَستخدِم حواسيب Unix، بما في ذلك Linux و Mac OS X عادةً محرف سطرٍ جديد '\n' للإشارة إلى نهاية السطر؛ بينما يستخدم Macintosh محرف العودة إلى بداية السطر '\r'؛ أما Windows فيَستخدِم المحرفين "\r\n". تستطيع الحواسيب العصرية عمومًا التعامل مع كل تلك الاحتمالات.
يُعرِّف الصنف BufferedReader
إضافةً إلى ذلك تابع النسخة lines()
، والذي يعيد قيمةً من النوع Stream<String>
يُمكِن استخدامها مع واجهة برمجة تطبيقات stream API -انظر مقال مقدمة إلى واجهة برمجة التطبيقات Stream API في جافا-. بفرض أن reader
متغيرٌ من النوع BufferedReader
، ستكون الطريقة الأمثل لمعالجة جميع الأسطر التي قرأها بتطبيق العامل forEach()
على مجرى الأسطر على النحو التالي:
reader.lines().forEachOrdered(action)
حيث تمثِّل action
مُستهلِك سلاسلٍ نصية، والذي يُكتَب عادةً بصيغة تعبيرات لامدا lambda expression.
تشيع معالجة الأسطر واحدًا تلو الآخر، لذلك يُمكِننا تغليف wrap أي كائنٍ من النوع Reader
ضمن كائنٍ من النوع BufferedReader
لتسهيل قراءة الأسطر النصية بالكامل. بفرض أن reader
من النوع Reader
، يُمكِننا تغليفه باستخدام كائنٍ من النوع BufferedReader
على النحو التالي:
BufferedReader in = new BufferedReader( reader );
كما يُمكِننا مثلًا استخدامه مع الصنف InputStreamReader
المذكور بالأعلى لقراءة أسطرٍ نصيةٍ من كائنٍ من النوع InputStream
، أو قد نُطبقه على System.in
على النحو التالي:
BufferedReader in; // BufferedReader for reading from standard input. in = new BufferedReader( new InputStreamReader( System.in ) ); try { String line = in.readLine(); while ( line != null ) { processOneLineOfInput( line ); line = in.readLine(); } } catch (IOException e) { }
تقرأ الشيفرة السابقة أسطرًا من الدخل القياسي، وتعالجها حتى الوصول إلى نهاية المجرى. تَعمَل مؤشرات نهاية المجرى حتى مع المُْدْخَلات التفاعلية، حيث يُولِّد النقر على زر Control-D
ببعض الحواسيب على الأقل مثلًا مؤشر نهاية مجرى بمجرى الدخل القياسي. تُعدُّ معالجة الاستثناءات إلزامية نظرًا لإمكانية تبليغ التابع readLine
عن استثناءاتٍ exception من النوع IOException
، ولهذا كان من الضروري إحاطة التابع بتعليمة try..catch
. يُمكِننا بدلًا من ذلك كتابة عبارة throws IOException
بتصريح التابع المُتضمِّن للشيفرة بالأعلى. يجب أن تُستورد الأصناف الآتية من حزمة java.io
:
-
BufferedReader
. -
InputStreamReader
. -
IOException
.
على الرغم من تسهيل الصنف BufferedReader
عملية قراءة الأسطر النصية، فإن هذا ليس الغرض الأساسي من وجوده، حيث تعمَل بعض أجهزة الدخل والخرج بأعلى كفائتها عند قراءة أو كتابة قدرٍ كبيرٍ من البيانات دفعةً واحدةً بدلًا من مجرد قراءة بايتاتٍ أو محارفٍ مفردة. يُوفِّر الصنف BufferedReader
تلك الإمكانية، حيث يُمكِنه قراءة دفعةٍ من البيانات، وتخزينها ضمن ذاكرةٍ داخلية، تُعرَف باسم المخزن المؤقت buffer.
عندما تقرأ من كائنٍ من الصنف BufferedReader
، فإنه في الواقع يستعيد البيانات من المخزن المؤقت إذا كان ذلك ممكنًا، أي إذا لم يَكن المخزن فارغًا؛ حيث يضطّر تلك الحالة فقط من التعامل مع مصدر الدْخل مرةً أخرى لجلب المزيد من البيانات. يتوفَّر أيضًا الصنف المكافئ BufferedWriter
، بالإضافة إلى وجود أصناف مجاري تدفق في مخزنٍ مؤقت للعمل مع مجاري البايتات.
اِستخدمنا الصنف غير القياسي TextIO
سابقًا لقراءة المُدْخَلات من المُستخدِمين والملفات؛ حيث يتميز ذلك الصنف بسهولة قراءة البيانات المنتمية لأي نوعٍ من الأنواع الأساسية primitive types، ولكنه لا يستطيع مع ذلك القراءة من أكثر من مصدر دخلٍ واحد بنفس الوقت، وهو بذلك لا يَتّبِع نفس نمط أصناف جافا القياسية المبنية مُسبقًا للدْخَل والخَرْج.
إذا أعجبك أسلوب الصنف TextIO
في التعامل مع المُدْخَلات، يُمكِنك إلقاء نظرةٍ على الصنف TextReader.java، الذي يُنفِّذ implement أسلوبًا مشابهًا بطريقةٍ أكثر كائنية object-oriented. لم نَستخدِم الصنف TextReader
ضمن هذا الإصدار من الكتاب، ولكننا أشرنا إليه ضمن بعض الإصدارات السابقة.
الصنف Scanner
لم تُوفِّر جافا بإصداراتها الأولى دعمًا مبنيًا مسبقًا للمُدْخلات البسيطة، حيث اعتمد الدعم الذي وفِّرته على بعض التقنيات المتقدمة نوعًا ما، ووفَّرت بعد ذلك الصنف Scanner
المُعرَّف بحزمة java.util
لتسهيل قراءة المُدْخَلات من الأنواع البسيطة، وهو ما يُعدّ تَحسُنًا كبيرًا، ولكنه لم يَحِل المشكلة بالكامل. تعرَّضنا للصنف Scanner
في المقال مقدمة إلى واجهة برمجة التطبيقات Stream API في جافا، ولكننا لم نَستخدِمه بعدها، ولهذا سنعتمد بغالبية الأمثلة التالية على الصنف Scanner
بدلًا من TextIO
.
يُعرِّف الصنف البرامج المسؤولة عن عمليات الدْخَل على هيئة توابع نسخ instance methods؛ أي ينبغي أن نُنشِئ كائنًا منه إذا أردنا أن نَستخدِمها. يَستقبِل باني الصنف constructor المصدر الذي ينبغي أن تُقرَأ منه المحارف؛ أي أنه يَعمَل مثل مُغلِّف لذلك المصدر. يُمكِن للمصدر أن يكون من الصنف Reader
، أوInputStream
، أوString
، أوFile
، أو غيرها من الاحتمالات الأخرى.
إذا اِستخدَمنا النوع String
مصدرًا للمُدْخَلات، فسيقرأ الصنف Scanner
محارف السلسلة النصية ببساطة من بدايتها إلى نهايتها بنفس الكيفية التي كان سيتعامل بها مع متتالية محارفٍ مصدرها مجرى، حيث يُمكِننا مثلًا استخدام كائنٍ من النوع Scanner
للقراءة من الدْخَل القياسي بكتابة ما يلي:
Scanner standardInputScanner = new Scanner( System.in );
وبفرض أن charSource
من النوع Reader
، يُمكِننا بالمثل أن نُنشِئ كائنًا من الصنف Scanner
للقراءة منه بكتابة ما يَلي:
Scanner scanner = new Scanner( charSource );
يُعالِج الصنف Scanner
المُدَْخَلات عادةً وحدةً token تلو الأخرى؛ حيث يُقصَد بالوحدة سلسلةً نصيةً من المحارف لا يُمكِن تقسيمها إلى وحداتٍ أصغر، وإلا ستفقد معناها وفقًا للمهمة المعنية بها. يُمكِن للوحدة أن تكون كلمةً مفردةً مثلًا أو سلسلةً نصيةً مُمثِّلةً لقيمةٍ من النوع double
.
يحتاج الصنف Scanner
أيضًا لوجود "فاصلٍ delimiter" بين تلك الوحدات، والذي يُمثَّل عادةً ببضعة فراغات، مثل محارف الفراغ، أو محارف tab، أو مؤشرات نهاية السطر. يُهمِل الصنف Scanner
تلك الفراغات، حيث يقتصر الهدف من وجودها على الفصل بين الوحدات. يتضمَّن الصنف توابع نسخٍ لقراءة مختلف أنواع الوحدات. لنفترض أن scanner
كائنٌ من النوع Scanner
، يكون لدينا التوابع التالية:
-
scanner.next()
: يقرأ الوحدة التالية من مصدر المُدْخَلات، ويعيد قيمةً من النوعString
. -
scanner.nextInt()
وscanner.nextDouble()
وغيرها: يقرأون الوحدة التالية من مصدر المُدْخَلات، ويحاولون تحويلها إلى قيمةٍ من النوعint
وdouble
وغيرها. تتوفَّر توابعٌ لقراءة جميع الأنواع الأساسية. -
scanner.nextLine()
: يقرأ سطرًا كاملًا من المُدْخَلات حتى يَصِل إلى مؤشر نهاية السطر، ثم يعيد السطر على أنه قيمةٌ من النوعString
. في حين يقرأ التابع مؤشر نهاية السطر، فإنه لا يُضمُّنه بالقيمة التي يُعيدها، كما أنه لا يعتمد على مفهوم الوحدات؛ فهو يعيد سطرًا كاملًا بما قد يحتويه من أية فراغات. يُمكِن أن تكون القيمة المُعادة من التابع مجرد سلسلةٍ نصيةٍ فارغة.
يُمكِن للتوابع السابقة أن تُبلِّغ عن بعض أنواع الاستثناءات، حيث يمكنها على سبيل المثال التبليغ عن استثناءٍ من النوع NoSuchElementException
عند محاولتها القراءة من مصدرٍ تجاوزت نهايته بالفعل. كما تُبلِّغ توابعٌ، مثل scanner.getInt()
عن حدوث استثناءٍ من النوع InputMismatchException
، إذا لم تكُن الوحدة token التالية من النوع المطلوب. لا تُعدّ معالجة الاستثناءات التي تُبلِّغ عنها تلك التوابع إلزامية.
يتمتع الصنف Scanner
بإمكانياتٍ جيدة لفحص المُدْخَلات دون قرائتها؛ حيث يُمكِنه مثلًا أن يُحدِّد فيما إذا كان هناك المزيد من الوحدات للقراءة، أو إذا كانت الوحدة التالية من نوعٍ معين. إذا كان scanner
كائنًا من النوع Scanner
، يُمكِننا استخدام التوابع التالية:
-
scanner.hasNext()
: يُعيد القيمة المنطقيةtrue
في حالة وجود وحدةٍ واحدةٍ على الأقل بمصدر المُدْخَلات. -
scanner.hasNextInt()
وscanner.hasNextDouble()
، وهكذا: يعيدون القيمة المنطقيةtrue
إذا كان هناك وحدةً واحدةً على الأقل بمصدر المُدْخَلات، وكانت تلك الوحدة قيمةً من النوع المطلوب. -
scanner.hasNextLine()
: يعيد القيمة المنطقيةtrue
في حالة وجود سطرٍ واحدٍ على الأقل بمصدر المُدْخَلات.
تَحِد ضرورة اِستخدَام فاصلٍ بين الوحدات من فعالية الصنف Scanner
نوعًا ما، لكنه رغم ذلك سهل الاستخدام، ومناسبٌ للعديد من التطبيقات المختلفة. نظرًا لوجود الكثير من الأصناف المسؤولة عن عمليات الدْخَل، مثل BufferedReader
وTextIO
وScanner
، قد تُصيبك الحيرة لإختيار الأنسب للاستخدام. يُفضَّل عمومًا اِستخدام الصنف Scanner
إلا إذا كان هناك سببٌ واضحٌ يدفعك لتفضيل أسلوب الصنف TextIO
. في المقابل، يُعدّ الصنف BufferedReader
بديلًا بسيطًا، إذا كان كل ما تحتاجه هو مجرد قراءة أسطرٍ نصيةٍ كاملةٍ من مصدر المُدْخَلات.
لاحِظ أنه من الممكن تغيير الفاصل الذي يَعتمِد عليه الصنف Scanner
للفصل بين الوحدات tokens، ولكن يتطلَّب ذلك التعامل مع ما يُعرَف باسم التعبيرات النمطية regular expression، والتي قد تكون معقدةً بعض الشيء، وهي عمومًا ليست ضمن أهداف هذا الكتاب، ولكن سنأخذ مثالًا بسيطًا عنها؛ ولنفترض مثلًا أننا نريد وحداتٍ مؤلفةً من كلماتٍ مُكوَّنةٍ فقط من أحرف الأبجدية الإنجليزية. يُمكِن في تلك الحالة للفاصل أن يَكون أي محرفٍ من غير تلك الأحرف؛ فإذا كان لدينا كائنٌ من الصنف Scanner
اسمه scnr
، فإننا نستطيع كتابة scnr.useDelimiter("[^a-zA-Z]+")
لجعله يَستخدِم هذا النوع من الفواصل، وستكون بذلك الوحدات المعادة من scnr.next()
مُكوَّنةً بالكامل من أحرف الأبجدية الإنجليزية. تُعدّ السلسلة النصية [^a-zA-Z]+
تعبيرًا نمطيًا، وهي في الواقع أداةً مهمةً لأي مبرمج، وعليك أن تشرُع بتعلُّمها إذا واتتك الفرصة لذلك.
إدخال وإخراج الكائنات المسلسلة Serialized
تَسمَح لنا الأصناف الآتية:
-
PrintWriter
. -
Scanner
. -
DataInputStream
. -
DataOutputStream
.
بمعالجة الدْخَل والخَرْج من جميع أنواع جافا الأساسية، ولكن ماذا لو أردنا أن نقرأ أو نكتب كائنات؟ سنحتاج بالضرورة إلى العثور على طريقةٍ ما لترميز الكائنات، وتحويلها إلى متتاليةٍ من القيم المنتمية لأي من الأنواع الأساسية، والتي يُمكِن بعد ذلك إرسالها على أنها خَرْجٌ بهيئة بايتات أو محارف. يُطلَق على تلك العملية اسم سَلسَلة serialize الكائن. سنضطّر من الناحية الأخرى لقراءة البيانات المُسَلسَلة، ثم اِستخدَامها لإعادة بناء الكائن الأصلي.
إذا كان الكائن معقدًا بعض الشيء، فسيضطّرنا ذلك إلى الكثير من العمل الذي هو في أساسه مجرد عمل روتيني. تُوفِّر جافا لحسن الحظ الصنفين ObjectInputStream
وObjectOutputStream
؛ لتحمُّل عبء غالبية ذلك العمل. لاحِظ أنهما صنفان فرعيان من الصنفين InputStream
وOutputStream
، ويُمكِنهما العمل مع الكائنات المُسَلسَلة.
يُعدّ الصنفان ObjectInputStream
وObjectOutputStream
أصنافًا مغلِّفة؛ أي يُمكِنها أن تُغلِّف مجارٍ streams من النوعين InputStream
وOutputStream
على الترتيب، وهو ما يَسمَح بإدْخال وإخراج الكائنات عبر أي مجرى بايتات؛ حيث يتضمَّن الصنف ObjectInputStream
التابع readObject()
؛ بينما يتضمَّن الصنف ObjectOutputStream
التابع writeObject(Object obj)
، ويُمكِنهما التبليغ عن استثناءاتٍ من النوع IOException
.
اقتباسملاحظة: يُعيد التابع
readObject()
قيمةً من النوعObject
، والتي يجب أن نُحوِّلها type-cast إلى نوع الكائن الفعلي المُفترَض قراءته.
يتضمَّن الصنف ObjectOutputStream
التوابع writeInt()
وwriteDouble()
وما يُشبهها، لإرسال قيمٍ منتميةٍ لأي من الأنواع الأساسية إلى مجرى الخرج، كما يتضمَّن الصنف ObjectInputStream
توابعًا مكافئةً لقراءة قيمٍ منتميةٍ لأي من الأنواع الأساسية. لاحِظ أنه من الممكن إرسال كائناتٍ أثناء إرسال قيم تنتمي لأي من الأنواع الأساسية، حيث تُمثَّل القيم المنتمية للأنواع الأساسية بصيغتها الثنائية binary الداخلية عند تخزينها بملف.
تُعدّ مجاري الكائنات بمثابة مجاري بايتات؛ حيث تُمثَّل الكائنات بصيغةٍ ثنائيةٍ مهيأة للآلة. في حين يُعزز ذلك من كفائتها، فإنه يتسبَّب بنفس الهشاشة التي تُعاني منها البيانات الثنائية في العموم. ونظرًا لأن الصيغة الثنائية للكائنات مُهيأةٌ للغة جافا، لا يكون من السهل إتاحة بيانات مجاري الكائنات للبرامج المكتوبة بلغاتٍ برمجيةٍ مختلفة. بناءً على ذلك، يُفضَّل اِستخدَام مجاري الكائنات فقط عند الحاجة إلى تخزينها تخزينًا مؤقتًا، أو إلى نَقْلها عبر اتصالٍ شبكي بين برنامجي جافا؛ أما بالنسبة للتخزين طويل الأمد أو الاتصال مع برامج مكتوبة بلغات آخرى، فهناك طرائقٌ بديلةٌ أفضل لسَلسَلة الكائنات (ألقِ نظرةً على المقال مقدمة مختصرة للغة XML لطريقةٍ معتمدةٍ على المحارف).
يَعمَل الصنفان ObjectInputStream
وObjectOutputStream
مع الكائنات التي تُنفِّذ الواجهة Serializable
فقط، كما يجب أن تكون جميع متغيرات النسخ المُضمَّنة بتلك الكائنات قابلةً للسَلسَلة. لا تنطوي عملية جعل كائنٍ معينٍ قابلًا للسَلسَلة على أي عملٍ تقريبًا؛ حيث لا تُصرّح الواجهة Serializable
حقيقةً عن أي توابع، وإنما هي موجودةٌ فقط مثل إشارةٍ للمُصرِّف على أن الكائن المُنفِّذ لها قابل للكتابة والقراءة. يَعنِي ذلك أن كل ما علينا فعله هو إضافة الكلمات implements Serializable
إلى تعريف الصنف. لاحِظ أن الكثير من أصناف جافا القياسية مُصرَّح عنها بحيث تكون قابلة للسَلسَلة بالفعل.
تنبيه عن استخدام الصنف ObjectOutputStream
: أُعدَّت مجاري ذلك الصنف لتجنَّبنا إعادة كتابة نفس الكائن أكثر من مرة، ولهذا، إذا واجه المجرى كائنًا معينًا للمرة الثانية، فإنه في الواقع يَستخدِم مرجعًا reference إلى كائن المرة الأولى أثناء الكتابة. يَعنِي ذلك أنه في حالة كان الكائن قد عُدّل بين المرتين الأولى والثانية، فإننا لن نَحصُل على البيانات الجديدة؛ لأن القيمة المُعدَّلة لا تُرسَل بصورةٍ صحيحة إلى المجرى.
يَرجِع ذلك إلى أن مجاري الصنف ObjectOutputStream
قد أُعدّت بالأساس للعمل مع الكائنات الثابتة immutable التي لا يُمكِن تعديلها بعد إنشائها، مثل السلاسل النصية من النوع String
. ومع ذلك، إذا أردت حقًا أن تُرسِل كائنًا مُتغيرًا mutable إلى هذا النوع من المجاري، وكان من المحتمل أن تُرسِل نفس الكائن أكثر من مرة، فيُمكِنك في تلك الحالة أن تضمَن إرسال النسخة الصحيحة من الكائن باستدعاء تابع المجرى reset()
قبل إرسال الكائن إليه.
ترجمة -بتصرّف- للقسم Section 1: I/O Streams, Readers, and Writers من فصل Chapter 11: Input/Output Streams, Files, and Networking من كتاب Introduction to Programming Using Java.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.