القائمة الرئيسية

الصفحات

تعلم البرمجه تعلم لغه البرمجه Java الدرس السابع 7

تعلم البرمجه من الصفر حتى الختراف


تعلم البرمجه  تعلم لغه البرمجه الجافا الدرس السابع 7



Java
التعامل مع الملفات في جافا



مقدمة
غالباً ما تحتاج البرامج إلى تبادل البيانات مع مصادر خارجية, سواء كان ذلك لإستقبال البيانات من مصدر ما أو لإرسال البيانات إلى مستقبل ما.

المرسل و المستقبل في عملية تبادل البيانات قد تكون لهم أشكال مختلفة أمثلة:
يمكن تبادل البيانات بين البرنامج و الملف, أو بين البرنامج و الشبكة, أو بين البرنامج و قاعدة بيانات, أو بين البرنامج و برنامج إلخ...

و بالنسبة لأنواع البيانات التي يمكن نقلها. يمكنك تبادل أي نوع من أنواع البيانات أمثلة:
نصوص, صور, ملفات صوتية, برامج, إلخ..

لغة جافا توفر لك الحزمة java.io و التي تحتوي على جميع الكلاسات التي تحتاجها للتعامل مع الملفات, سواء لإدخال بيانات جديدة في الملف Input Operation أو لإستخراج بيانات موجودة في الملف Output Operation, و حتى لإنشاء أو حذف الملفات. و من هنا جاء الإسم I/O.

عملية إدخال البيانات أو إستخراجها من الملفات تسمى Streaming data, و هذه الكلمة تعني تدفق البيانات في اللغة العربية.

سميت هذه العملية ( تدفق البيانات ) لأنك عندما تتعامل مع الملفات سواء للقراءة أو الكتابة فأنت تقوم بذلك على دفعات صغيرة, بمعنى أنك تبرمج على سبيل المثال برنامج يفتح ملف معين و يقرأ منه حرف واحد أو مجموعة أحرف في كل دفعة حتى نقل كامل بيانات الملف. سترى ذلك لاحقاً بتفصيل.

Streams
الكلمة Streams تعني سلسلة من البيانات, يتم إستخراجها أو إدخالها في ملف معين.

هناك نوعين من الـ Streams و هما: input stream و output stream.


لذلك تم بناء الكلاسات التالية للتعامل مع الـ Streams:

الكلاس InputStream يستخدم لقراءة البيانات من ملف معين يسمى Source.
الكلاس OutputStream يستخدم لكتابة البيانات في ملف معين يسمى Destination.




في هذا الدرس سنشرح لك كيف تستخدم الحزمة I/O لقراءة أي نوع من الملفات و سنشرح فقط الكلاسات الأكثر استخداماً فيها.

طريقة التعامل مع الملفات
عندما تتعامل مع الملفات عليك إتباع نمط معين حتى تتجنب حدوث أي خطأ في البرنامج, و لقد قمنا بوضع خطوات ستساعدك كثيراً في فهم عملية نقل البيانات, و وضعنا أيضاً بعض الملاحظات التي يجب عليك الإنتباه لها أثناء تحديد مكان وجود الملفات التي سيتعامل البرنامج معها.


الخطوات التي يجب إتباعها لقراءة البيانات من ملف و كتابتها في ملف آخر
إنشاء كائن يمثل الملف الذي تريد قراءة البيانات منه.
إنشاء كائن يمثل الملف الذي تريد كتابة البيانات فيه.
عليك وضع جميع الأوامر التي تتعامل مع الملفات بداخل الجملة try لأن البرنامج قد يتعرض لعدة مشاكل عندما يتعامل مع الملفات و التي سنذكرها لك لاحقاً.
تحديد مسار و إسم و نوع الملف للكائن الذي يمثل الملف الذي سيتم قراءة البيانات منه.
تحديد مسار و إسم و نوع الملف للكائن الذي يمثل الملف الذي سيتم كتابة البيانات فيه.
تعريف متغير من النوع int.
إستخدام حلقة بشكل متوازي بين الملفين, أي في كل دورة من دورات هذه الحلقة نقرأ حرف من الملف الأول و نضعه في الملف الثاني.
إستخدام الدالة read() لقراءة حرف من الملف الأول, بعدها يجب تخزين هذا الحرف في المتغير.
إستخدام الدالة write() لكتابة الحرف الموجود في المتغير في الملف الثاني.
في الأخير عليك التأكد من إغلاق الملفات التي حاولت الإتصال معها سواء نجحت عملية النسخ أم لم تنجح, لذلك عليك إستدعاء الدالة close() في الجملة finally لإغلاق أي كائن متصل بالملفات حتى لا تعرض الملفات لأي خطر.

هذه الصورة توضح لك كيف تتم عملية نقل المحتوى من ملف لآخر, و هذا السيناريو هو نفسه تقريباً في جميع عمليات نقل البيانات و هو يستمر في نقل الأحرف الواحد تلو الآخر حتى آخر حرف موجود في الملف عندما نكتبه بالشكل الصحيح بداخل حلقة.



الدالة read() كما هو موضح في الصورة تقرأ حرف واحد من الملف و ترجعه كلما تم إستدعائها.
في حال تم قراءة جميع الأحرف الموجودة في الملف, ثم حاولت الدالة read() قرائة حرف من جديد, ستقوم بإرجاع 1-.
إذاً عندما ترجع الدالة read() القيمة 1- فهذا يعني أنه تم تخطي آخر حرف موجود في الملف.
من هنا جائت فكرة أنه يمكنك إستخدام الدالة read() و القيمة 1- كشرط أساسي لإيقاف عملية قرائة البيانات بعد آخر حرف يتم قرائته.


تحديد مسار الملفات التي تحاول التعامل معها
من الأشياء المهمة التي عليك الإنتباه لها عندما تتعامل مع الملفات هي معرفة كيف يتعامل نظام التشغيل الذي تبني برنامجك لأجله مع المسارات.

لنفترض أنه عندنا ملف موجود بداخل 3 مجلدات و نريد الوصول إليه كما في الصورة التالية:



الآن سترى كيف يتم تحديد المسار الموجود فيه الملف على أشهر أنظمة التشغيل.

على نظامي Mac و Windows نضع \\ بعد إسم كل مجلد عند تحديد المسار الموجود فيه الملف test.
على نظامي Unix و Linux نضع / بعد إسم كل مجلد عند تحديد المسار الموجود فيه الملف test.

لاحظ كيف أننا نضع نفس أسماء المجلدات و نفس إسم الملف لكي نصل للملف text. الإختلاف الوحيد هو الرمز الذي نضعه بعد كل مجلد.

A\\B\\C\\test.txt       // هكذا نصل للملف Mac و Windows على

A/B/C/test.txt          // هكذا نصل للملف Linux و Unix على

في جافا يمكنك إستخدام الأسلوب الذي تريده مهما كان نظام التشغيل, ثم تقوم بتحويل المسار عنك ليتناسب مع نظام التشغيل.

Character Streams
الكلاسات التي تصنف كـ Character Streams مصممة للتعامل مع الملفات النصية العادية التي تعتمد على الترميز unicode, أي كل حرف في الملف يتم تمثيله بـ 2 Bytes.

هناك العديد من الكلاسات التي تنتمي للـ Character Streams لكن الكلاسات الأكثر إستخداماً هما : FileReader و FileWriter.


تقنياً
الكلاسات FileReader و FileWriter يعتمدون على الكلاسات FileInputStream و FileOutputStream.
الفرق الوحيد بينهم هو أن الكلاس FileReader يقرأ 2 Bytes في كل مرة و الكلاس FileWriter يكتب 2 Bytes في كل مرة.

الترميز unicode يمثل كل حرف موجود في الملف بـ 2 Bytes.
إذاً الكلاس FileReader يقرأ حرف من الملف في كل مرة و الكلاس FileWriter يكتب حرف في الملف في كل مرة.


مثال
في حال كان عندنا ملف إسمه input.txt موجود في المسار C:\\MyFiles\\ كما في الصورة التالية:



و نريد إنشاء نسخة ثانية منه في نفس المسار إسمها output.txt كما في الصورة التالية:



نكتب الكود التالي.

مثال
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CopyFileExample {

    public static void main(String[] args) throws IOException {

        FileReader in = null;
        FileWriter out = null;

        try {
            in = new FileReader("C:\\MyFiles\\input.txt");
            out = new FileWriter("C:\\MyFiles\\ouput.txt");
            int c;

            while ((c = in.read()) != -1) {
                out.write(c);
            }
        }
        catch(IOException e) {
            System.out.println("There is IOException!");
        }
        finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }

    }
}
شرح الكود
هنا قمنا باستدعاء الكلاسات التي سنحتاجها في البرنامج.
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

هنا قمنا بوضع الجملة throws IOException لأن الدالة close() التي قمنا باستدعائها لاحقاً في الكود قد ترمي إستثناء في الدالة main()
    public static void main(String[] args) throws IOException {

هنا قمنا بإنشاء الكائن in لتمثيل الملف الذي سيتم فتحه لاحقاً.
و قمنا بإنشاء الكائن out لتمثيل الملف الذي سيتم خلقه لاحقاً.
        FileReader in = null;
        FileWriter out = null;

هنا قمنا بتحديد الملف الذي سيمثله الكائن in
الملف input.txt الموجود في المسار C:\\MyFiles\\ أصبح متمثلاً بالكائن in
و قمنا بخلق ملف جديد على إسمه output.txt و يمثله الكائن out.
الملف output.txt حتى الآن عبارة عن ملف فارغ موجود في المسار C:\\MyFiles\\
المتغير c سنستخدمه كوسيط بين الملفين عند نقل البيانات.
            in = new FileReader("C:\\MyFiles\\input.txt");
            out = new FileWriter("C:\\MyFiles\\ouput.txt");
            int c;

في الحلقة while سيحدث التالي في كل دورة:
ستقوم الدالة read() بقراءة 2 Bytes من الملف الذي يمثله الكائن in ثم تخزنهم في المتغير c
إذا كانت قيمة المتغير c لا تساوي 1- , سيتم إدخال الـ 2 Bytes الموجودة في المتغير c في الكائن out بواسطة الدالة write()
            while ((c = in.read()) != -1) {
                out.write(c);
            }
الكلاسات المصممة للتعامل مع الـ Character Streams.
في جافا تم بناء الكلاسات التي تتعامل مع الـ Character Streams بشكل متناسق كما في الصورة التالية.



الكلاس Reader هو الكلاس الأساسي لقراءة الأحرف من الملفات. و الكلاس Writer هو الكلاس الأساسي لكتابة الأحرف في الملفات.

أهم كلاسَين للتعامل مع الـ Character Streams هما الكلاس BufferedReader و الكلاس BufferedWriter بحيث يوفران لك كثير من الدوال التي تمكنك من التعامل مع الملفات بسهولة و مرونة و استخدام الـ buffer للحصول على أفضل أداء ممكن.


طريقة التعامل مع هذه الكلاسات
عندما تستخدم هذه الكلاسات ستستخدم مبدأ يسمى Upcasting.
و المقصود من هذا أنك مثلاً تنشئ كائن من الكلاس InputStream ثم تضع فيه أي كائن من الكلاسات التي ترث منه, مثل InputStreamReader.
بمعنى آخر تنشئ كائن من الكلاس InputStream ثم تحدد أنك تريد إستخدام الكلاس FileInputStream.


فكرة الـ Buffer و أهميتها عند التعامل مع الملفات
إفترض أن برنامجك سيقرأ على سبيل المثال ملف حجمه 10 MB, و التي تساوي 10,485,760 Bytes.

هل ستستدعي الدالة read() أكثر من 10 ملايين مرة حتى تقرأ هذا الملف؟!
حتماً لا لأنك إن فعلت ذلك ستهلك المعالج و الذاكرة و القرص الصلب لقراءة هذا الملف البسيط, و من هنا جائت فكرة الـ Buffer.


مفهوم الـ Buffer
الـ Buffer عبارة عن مساحة مؤقتة للتخزين يتم إنشاءها في الذاكرة من أجل قراءة كمية كبيرة من المعلومات, ثم يتم التخلص منها عند الإنتهاء.
عندما تستخدم كائن يتعامل مع الـ Buffer فهذا الكائن سينشئ هذه المساحة المؤقت في الذاكرة.

الآن إذا رجعنا للمثال السابق, كان يمكنك قراءة 1000 Bytes أو أكثر على سبيلا المثال في كل مرة تستدعي فيها الدالة read() بدل إستدعاءها لقراءة 1 Byte في كل مرة. و لو كان هذا مثال حي للاحظت الفارق في السرعة بين الأسلوبين.


في الجدول التالي وضعنا بعض كلاسات الـ Character Streams التي تستخدم للقراءة من الملفات.

الكلاس مع تعريفه
1
class InputStreamReader
الكلاس InputStreamReader يقرأ بيانات الملف كـ bytes و يحولهم لأحرف حسب الترميز الذي نقوم بتحديده في الكونستركتور.
تابع القراءة »
2
class FileReader
الكلاس FileReader يرث من الكلاسات InputStreamReader و Reader و هو يستخدم لقراءة Streams من الأحرف.
تابع القراءة »
3
class BufferedReader
الكلاس BufferedReader يقرأ أحرف كائن الـ InputStreamReader و يضعهم في الـ buffer. و هذا يوفر لك طرق عديدة لقراءة المحتوى من الـ buffer. مثل قراءته حرفاً حرفاً, أو تخزينه في مصفوفة, أو قراءته سطراً سطراً.
يمكنك تحديد حجم الـ buffer, أو استخدام الحجم الإفتراضي لها و الذي يعتبر جيداً في معظم الحالات.
تابع القراءة »

في الجدول التالي وضعنا بعض كلاسات الـ Character Streams التي تستخدم للكتابة في الملفات.

الكلاس مع تعريفه
1
class OutputStreamWriter
الكلاس OutputStreamWriter يكتب الأحرف في الملف كـ bytes حسب الترميز الذي نقوم بتحديده في الكونستركتور.
تابع القراءة »
2
class FileWriter
الكلاس FileWriter يرث من الكلاسات OutputStreamWriter و Writer و هو يستخدم لكتابة نص في الملف.
تابع القراءة »
3
class BufferedWriter
الكلاس BufferedWriter يضع أحرف كائن الـ OutputStreamWirter في الـ buffer. و هذا يوفر لك طرق عديدة لكتابة محتوى الـ buffer في الملف. مثل كتابته حرفاً حرفاً, أو تخزينه في مصفوفة, أو كتابته سطراً سطراً.
يمكنك تحديد حجم الـ buffer, أو استخدام الحجم الإفتراضي لها و الذي يعتبر جيداً في معظم الحالات.
تابع القراءة »
Byte Streams
الكلاسات التي تصنف كـ Byte Streams مصممة للتعامل الملفات الغير نصية التي تخزن المحتوى بشكل سلسلة بيانات من 1 Byte مثل ( PNG, MP4, MP3.. ) و يمكنها نقل الأحرف العادية شرط أن تكون الأحرف المستخدمة تتألف من 1 Byte حتى تنقل بشكل صحيح.

هناك العديد من الكلاسات التي تنتمي للـ Byte Streams لكن الكلاسات الأكثر إستخداماً هما : FileInputStream و FileOutputStream.


مثال
في حال كان عندنا ملف إسمه input.MP4 موجود في المسار C:\\MyFiles\\ كما في الصورة التالية:



و نريد إنشاء نسخة ثانية منه في نفس المسار إسمها output.MP4 كما في الصورة التالية:



نكتب الكود التالي.

مثال
package files_io;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyFileExample {

    public static void main(String[] args) throws IOException {

        FileInputStream in = null;
        FileOutputStream out = null;

        try {
            in = new FileInputStream("C:\\MyFiles\\input.MP4");
            out = new FileOutputStream("C:\\MyFiles\\ouput.MP4");
            int c;

            while ((c = in.read()) != -1) {
                out.write(c);
            }
        }
        catch(IOException e) {
            System.out.println("There is IOException!");
        }
        finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }

    }

}
شرح الكود
هنا قمنا باستدعاء الكلاسات التي سنحتاجها في البرنامج.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

هنا قمنا بوضع الجملة throws IOException لأن الدالة close() التي قمنا باستدعائها لاحقاً في الكود قد ترمي إستثناء في الدالة main().
    public static void main(String[] args) throws IOException {

هنا قمنا بإنشاء الكائن in لتمثيل الملف الذي سيتم فتحه لاحقاً.
و قمنا بإنشاء الكائن out لتمثيل الملف الذي سيتم خلقه لاحقاً.
        FileInputStream in = null;
        FileOutputStream out = null;

هنا قمنا بتحديد الملف الذي سيمثله الكائن in.
الملف input.mp4 الموجود في المسار C:\\MyFiles\\ أصبح متمثلاً بالكائن in.
و قمنا بخلق ملف جديد على إسمه output.MP4 و يمثله الكائن out.
الملف output.MP4 حتى الآن عبارة عن ملف فارغ موجود في المسار C:\\MyFiles\\
المتغير c سنستخدمه كوسيط بين الملفين عند نقل البيانات.
            in = new FileInputStream("C:\\MyFiles\\input.MP4");
            out = new FileOutputStream("C:\\MyFiles\\ouput.MP4");
            int c;

في الحلقة while سيحدث التالي في كل دورة:
ستقوم الدالة read() بقراءة 1 Byte من الملف الذي يمثله الكائن in ثم تخزنهم في المتغير c
إذا كانت قيمة المتغير c لا تساوي 1- , سيتم إدخال الـ 1 Byte الموجودة في المتغير c في الكائن out بواسطة الدالة write()
            while ((c = in.read()) != -1) {
                out.write(c);
            }

ملاحظة
عندما تقوم بتشغيل هذا البرنامج. سيحتاج إلى بعض الوقت حتى ينتهي من عملية النسخ بشكل كامل (أي لا تظنه معلق), و في حال قمت بإيقاف البرنامج أثناء عمله, إفتح الملف Output.MP4 و ستجد أن الجزء الأخير في الفيديو مفقود.

الكلاسات المصممة للتعامل مع الـ Byte Streams
في جافا تم بناء الكلاسات التي تتعامل مع الـ Byte Streams بشكل متناسق كما في الصورة التالية.



الكلاس InputStream هو الكلاس الأساسي لقراءة البيانات من ملف. و الكلاس OutputStream هو الكلاس الأساسي لكتابة البيانات في ملف.

أهم كلاسَين للتعامل مع الـ Streams هما الكلاس FileInputStream و الكلاس FileOutputStream بحيث يوفران لك كثير من الدوال التي تمكنك من التعامل مع الملفات بسهولة و مرونة.


في الجدول التالي وضعنا بعض كلاسات الـ Byte Streams التي تستخدم للكتابة في الملفات.

الكلاس مع تعريفه
1
class FileInputStream
الكلاس FileInputStream يستخدم لقراءة البيانات من الملفات.
تابع القراءة »
2
class ByteArrayInputStream
الكلاس ByteArrayInputStream يسمح باستخدام الـ buffer في الذاكرة كـ InputStream. و هو يقرأ البيانات من المصدر كمصفوفة نوعها byte.
تابع القراءة »
3
class DataInputStream
الكلاس DataInputStream يستخدم لقراءة البيانات البدائية primitive data types من ملف معين.
تابع القراءة »


في الجدول التالي وضعنا بعض كلاسات الـ Byte Streams التي تستخدم للقراءة من الملفات.

الكلاس مع تعريفه
1
class FileOutputStream
الكلاس FileOutputStream يستخدم لإنشاء ملف جديد و كتابة البيانات فيه.
في حال وجود ملف يحمل نفس الإسم و النوع في نفس المسار الموضوع للـ OutputStream, سيتم تبديل الملف القديم بملف الـ OutputStreamالجديد.
تابع القراءة »

2
class ByteArrayOutputStream
الكلاس ByteArrayOutputStream ينشئ buffer في الذاكرة لتخزين بيانات الـ Stream فيها قبل إرسالها للـ OutputStream.
تابع القراءة »
3
class DataOutputStream
الكلاس DataOutputStream يستخدم لكتابة البيانات البدائية primitive data types في ملف معين.
تابع القراءة »
الكلاس File
تم إعداد الكلاس File لمعالجة الملفات و المجلدات.
لا يوجد كلاس خاص لمعالجة المجلدات لأن المجلدات عبارة عن ملفات فارغة يمكنها إحتواء ملفات و مجلدات أخرى.


يستخدم الكلاس File للأغراض التالية:

لخلق ملف أو مجلد جديد.
لحذف ملف أو مجلد موجود.
للبحث عن ملف أو مجلد.
لعرض جميع الملفات و المجلدات الموجودة في المجلد.
لقراءة صلاحيات الملف أو المجلد.
لتغيير صلاحيات الملف أو المجلد.

وضعنا لك هنا أهم المعلومات التي قد تحتاجها حول الكلاس File.

تابع القراءة »

التعامل مع المجلدات
المجلد يسمى directory في البرمجة, و هو عبارة عن ملف يمكنه تخزين ملفات و مجلدات أخرى.

باستخدام كائن من الكلاس File يمكنك إنشاء العدد الذي تريده من المجلدات, و معرفة عدد الملفات الموجودة في مجلد ما.
لتعرف تفاصيل أكثر عن علاقة الكلاس File بالمجلدات, أدرس الكلاس File و ستتعرف على بعض الدوال الموجودة فيه المخصصة للتعامل مع المجلدات.


إنشاء المجلدات
يوجد دالتين في الكلاس File يمكن إستخدامها لإنشاء مجلدات.

الدالة mkdir() تستخدم لإنشاء مجلد واحد, و ترجع true إذا تم إنشاءه بنجاح و false إذا لم فشلة عملية إنشاءه.
إذا فشلت علية إنشاءه فذلك يعني أن المسار الموضوع في كائن الـ File موجود في الأصل, أو أنه لا يمكن إنشاء المجلد لإن المسار الموضوع قبل إسم الملف الذي سيتم إنشائه غير موجود.
الدالة mkdirs() تستخدم لإنشاء مجلد أو أكثر.
هنا في حال كان المسار الموضوع غير موجود ستقوم هي بإنشاءه. أي ستنشئ جميع المجلدات الموضوعة في المسار.

عرض محتوى المجلدات
يمكنك استخدام الدالة list() الموجودة في الكلاس File لعرض جميع الملفات الموجودة في المجلد.


مثال
•في هذا المثال سنقوم بإنشاء مجلد إسمه harmash بداخله سننشئ مجلدين images و videos, و سننشئ فيه أيضاً ملف إسمه index.php.

•بعدها سنقوم بعرض جميع الأشياء الموجودة في المجلد harmash

import java.io.File;
import java.io.IOException;

public class PrintFileInfo {

    public static void main(String[] args) throws IOException {

        try {
            // harmash قمنا بإنشاء هذا المتغير للإشارة إلى المسار الأساسي الذي سيوضع فيه المجلد
            String websitePath = "C:\\projects\\harmash\\";

            File harmashFolder = new File(websitePath);                 // الذي سننشئه لاحقاً harmash هنا قمنا بإنشاء كائن يشير لمسار المشروع
            File imagesFolder  = new File(websitePath + "images");      // harmash الذي سننشئه لاحقاً بداخل المجلد images هنا قمنا بإنشاء كائن يشير للمجلد
            File videosFolder  = new File(websitePath + "videos");      // harmash الذي سننشئه لاحقاً بداخل المجلد videos هنا قمنا بإنشاء كائن يشير للمجلد
            File pageFile      = new File(websitePath + "index.php");  // harmash الذي سننشئه لاحقاً بداخل المجلد index.php هنا قمنا بإنشاء كائن يشير للملف

            harmashFolder.mkdirs();      // harmash و بداخله المجلد project هنا قمنا بإنشاء المجلد
            imagesFolder.mkdir();        // videos هنا قمنا بإنشاء المجلد
            videosFolder.mkdir();        // images هنا قمنا بإنشاء المجلد
            pageFile.createNewFile();    // index.php هنا قمنا بإنشاء الملف

            System.out.println("c:\\projects\\harmash contain: ");

            // هنا قمنا بعرض جميع المجلدات و الملفات الموجودة في المشروع
            String[] paths = harmashFolder.list();

            for (String path:paths) {
                System.out.println("- " + path);
            }
        }
        catch(Exception e) {
            System.out.println("There is Exception!");
        }

    }
}
•سنحصل على النتيجة التالية عند التشغيل.

c:\projects\harmash contain:
- images
- index.php
- videos


Java
مفهوم الـ Serialization في جافا



مقدمة
في معظم البرامج التي نستخدمها, نلاحظ أنه يوجد مكان خاص لضبط إعدادات البرنامج يسمح للمستخدم أن يخصص البرنامج بالشكل الذي يريده.
بعد أن يقوم المستخدم بضبط إعدادت برنامجه, نلاحظ أن هذه الإعدادات تظل محفوظة في البرنامج حتى إذا خرج من البرنامج و فتحه من جديد.

فمثلاً في البرامج التي تستخدم للكتابة, تجد في صفحة الإعدادات أنه بإمكانك تغير أنواع الخطوط المستخدمة في البرنامج, و تغيير أحجامها و ألوانها. و وقد تجد في الإعدادات أيضاً أنه بإمكانك إستخدام التطبيق بأكثر من لغة إلخ..
بالنسبة للألعاب, غالباً ما تجد أنه في صفحة الإعدادات يمكنك تحديد ما إذا كنت تريد سماع نغمات حماسية أثناء اللعب أم لا, بالإضافة إلى إمكانية تحديد مستوى الصوت. أيضاً أحياناً تجد أنه يمكنك تحديد مستوى اللعب ( سهل - متوسط - صعب - صعب جداً ).

للحفاظ على إعدادات البرنامج التي ضبطها المستخدم, نقوم بحفظ هذه الإعدادات بداخل ملف و هذا ما ستتعلمه في هذا الدرس.

ملاحظة: عليك فهم درس التعامل مع الملفات جيداً حتى تستطيع فهم هذا الدرس, لأننا سنقوم بتخزن المعلومات في ملف.


شكل الكائن في الذاكرة
أثناء تشغيل البرنامج, كل كائن يتم إنشاءه فيه, يتم تمثيله في الذاكرة كسلسلة كبيرة من الـ Bytes يفهمها نظام التشغيل.
هذه السلسلة تحتوي على جميع معلومات الكائن, مثل:

الكلاس المشتق منه.
كل كلاس يرث منه.
كل إنترفيس يطبقه.
كل دالة يملكها.
كل متغير يملكه, بالإضافة إلى نوعه و قيمته الحالية إلخ..
عند تنفيذ جميع الأوامر الموجودة في البرنامج أو عند الخروج منه, يتم مسح جميع البيانات المتعلقة بهذا البرنامج من الذاكرة لأنه لم يعد لها حاجة. أي يتم مسح جميع الكائنات, المتغيرات و الدوال, و بالتالي يتوقف أي إتصال قائم بين البرنامج و الأشياء خارجية مثل ملف, شبكة أو قاعدة بيانات إلخ..


مفهوم الـ Serialization و الـ Deserialization
Serialization تعني حفظ حالة الكائن الحالية بداخل ملف.
عندما نقول: "حفظ حالة الكائن", فنحن بذلك نقصد إنشاء نسخة مطابقة من الكائن الموجود في الذاكرة و وضعها في ملف خارجي.

Deserialization تعني استرجاع حالة الكائن الموجودة في ملف.
عندما نقول: "استرجاع حالة الكائن", فنحن بذلك نقصد خلق الكائن الموجود في ملف خارجي بداخل ذاكرة الجهاز.

ملاحظة: بشكل عام عندما نحفظ حالة الكائن و نسترجعها, نقول أننا نفعل Serialization و لكننا فعلياً نفعل Serialization + Deserialization.


أهمية الـ Serialization
حفظ حالة الكائن الذي تم إنشاءه في الذاكرة في ملف خارجي.
حالة الكائن المحفوظة في ملف يمكن إستخدامها متى شئنا لخلق الكائن من جديد في الذاكرة.
مشاركة حالة الكائن عبر شبكة, حيث أنه يمكن استخدام الملف الذي حفظنا فيه حالة الكائن لخلق الكائن في جهاز آخر.
تخزين الصور في قواعد البيانات ( الصورة تحفظ في قاعدة البيانات كـ BLOB ).
إذاً في حال أردت حفظ معلومات الكائن قبل الخروج من البرنامج يمكنك إنشاء ملف متزامن يحفظ لك حالة الكائن, و بعدها يمكنك استرجاعها عند تشغيل البرنامج من جديد.


تطبيق الـ Serialization و الـ Deserialization
لتحقيق الـ Serialization, نستخدم الكلاس ObjectOutputStream لإنشاء نسخة من الكائن الموجود في الذاكرة و وضعها في ملف.
لتحقيق الـ Deserialization, نستخدم الكلاس ObjectInputStream لخلق الكائن المحفوظ في الملف في الذاكرة من جديد.

كل كلاس منهم يملك عدة كونستركتورات و دوال, سنشرح فقط الأشياء التي سنستخدمها في هذا الدرس.

خطوات الـ Serialization
لإنشاء كائن من كلاس معين و حفظ حالته عليك اتباع الخطوات التالية:

الكائن الذي تريد حفظ حالته, يجب أن يكون في الأساس مشتق من كلاس يفعل implements للإنترفيس Serializable.
إنشاء ملف إمتداده .ser بواسطة الكلاس FileOutputStream.
تجهيز كائن من الكلاس ObjectOutputStream الذي يستخدم لكتابة حالة الكائن في الملف.
نسخ حالة الكائن الموجود في الذاكرة في هذا الملف بواسطة الدالة writeObject().
عند الإنتهاء من عملية النسخ, نقوم بقطع كل إتصال قمنا بإجرائه مع هذا الملف.

الكلمة المحجوزة transient
في حال أردت عدم نسخ جميع الأشياء المتعلقة بالكائن في الذاكرة, عليك وضع الكلمة transient في تعريف كل شيء لا تريده أن ينسخ في الملف, و عندها سيتم تجاهله.

خطوات الـ Deserialization
لإسترجاع حالة الكائن التي تم حفظها في ملف معين, عليك اتباع الخطوات التالية:

إنشاء كائن فارغ من نفس نوع الكائن الذي نريد إستراجع حالته من الملف.
تجهيز كائن من الكلاس FileInputStream الذي يستخدم لإدخال بيانات ملف محدد في الذاكرة.
تجهيز كائن من الكلاس ObjectInputStream ليعيد خلق الكائن في الذاكرة.
قراءة حالة الكائن بواسطة الدالة readObject() و تخزينها في الكائن الفارغ الذي قمنا بإنشائه في الخطوة الأولى, و هنا سيكون عليك أن تفعل Downcasting لتحول نوع الكائن الذي ترجعه الدالة readObject() إلى نوع الكائن الحقيقي لأنها ترجع الكائن الموجود في الذاكرة كـ Object و ليس كنوعه الحقيقي.
عند الإنتهاء من عملية النسخ, نقوم بقطع كل إتصال قمنا بإجرائه مع هذا الملف.
مثال شامل
في المثال التالي قمنا بتعريف كلاس إسمه Editor, يطبق الإنترفيس Serializable, و يملك المتغيرات التالية:
language, encoding, fontSize, fontFamily, autoSave, autoComplete, direction.

المتغير direction قمنا بتعريفه كـ transient لأننا لا نريد أن يتم حفظ قيمته عندما نفعل Serialization.

بعدها قمنا بتعريف كلاس آخر إسمه Test قمنا فيه بتطبيق مبدأي الـ Serialization و الـ Deserialization.
من السطر 22 إلى السطر 52 قمنا بتطبيق مبدأ الـ Deserialization.
من السطر 59 إلى السطر 90 قمنا بتطبيق مبدأ الـ Serialization.

الملف الذي قمنا بتخزين حالة الكائن فيه قمنا بتسميته user-prefrences.ser.
عند تشغيل البرنامج سيتم إنشاؤه في المجلد الذي يحتوي على المشروع.


إنتبه: في حال ظهرت لك مشكلة في الكلاس Editor قم فقط بإضافة الكود التالي في السطر رقم 6 و سنشرح لك معنى هذا السطر لاحقاً.
private static final long serialVersionUID = 1L;


Editor.java
package serialization;

import java.io.Serializable;                      // Serializable هنا قمنا باستدعاء الإنترفيس

public class Editor implements Serializable {     // Serializable يطبق الإنترفيس Editor هنا قمنا بتعريف كلاس إسمه

    public String language;
    public String encoding;
    public String fontSize;
    public String fontFamily;
    public boolean autoSave;
    public boolean autoComplete;
    public transient String direction;            // transient كـ direction قمنا بتعريف المتغير

}

Test.java
package serialization;

import java.io.File;                              // File هنا قمنا باستدعاء الكلاس
import java.io.FileInputStream;                   // FileInputStream هنا قمنا باستدعاء الكلاس
import java.io.FileOutputStream;                  // FileOutputStream هنا قمنا باستدعاء الكلاس
import java.io.ObjectInputStream;                 // ObjectInputStream هنا قمنا باستدعاء الكلاس
import java.io.ObjectOutputStream;                // ObjectOutputStream هنا قمنا باستدعاء الكلاس
import java.io.IOException;                       // IOException هنا قمنا باستدعاء الكلاس

public class Test {

    public static void main(String[] args) {

        // e إسمه Editor في كل مرة نقوم فيها بتشغيل البرنامج سيتم إنشاء كائن من الكلاس
        Editor e = new Editor();


        // لمعرفة إذا كان يوجد ملف يحفظ حالة الكائن أم لا user-prefrences.ser بعدها سيتم البحث عن الملف
        if ( new File("./user-prefrences.ser").exists() )
        {
            // منه e موجوداً سيحاول البرنامج إستعادة حالة الكائن user-prefrences.ser في حال كان الملف
            try
            {
                // في الذاكرة user-prefrences.ser حتى نستطيع إدخال المعلومات الموجودة في الملف FileInputStream هنا قمنا بإنشاء كائن نوعه
                FileInputStream fis = new FileInputStream("./user-prefrences.ser");

                // في الذاكرة user-prefrences.ser المحفوظ في الملف Editor لنتمكن من إعادة خلق كائن الـ ObjectInputStream هنا قمنا بإنشاء كائن نوعه
                ObjectInputStream ois = new ObjectInputStream(fis);

                // e و قمنا بتخزين حالته في الكائن Editor هنا قمنا بقراءة حالة الكائن الذي تم خلقه في الذاكرة ككائن من الكلاس
                e = (Editor) ois.readObject();

                // user-prefrences.ser في الأخير قمنا بقطع كل إتصال قمنا بإجرائه مع الملف
                fis.close();
                ois.close();

                // في حال عدم حدوث أي خطأ, سيتم طباعة الجملة التالية التي تعني أن العملية تمت بنجاح
                System.out.println("Deserialized data has been created in the memory");
                System.out.println("Language:      " + e.language);
                System.out.println("Encoding:      " + e.encoding);
                System.out.println("Font size:     " + e.fontSize);
                System.out.println("Font family:   " + e.fontFamily);
                System.out.println("Auto save:     " + e.autoSave);
                System.out.println("Direction:     " + e.direction);
                System.out.println("Auto Complete: " + e.autoComplete);
                System.out.println();
            }
            catch(IOException | ClassNotFoundException ex)
            {
                // في حال حدوث أي خطأ عند محاولة إسترجاع حالة الكائن سيتم عرضعه
                System.out.println(ex.getMessage());
            }
        }




        // user-prefrences.ser و حفظها في ملف جديد إسمه e هنا حاولنا تغيير حالة الكائن
        try
        {
            // ( أي قمنا بتغيير إعدادات البرنامج ) e هنا قمنا بتغيير قيم الكائن
            e.language   = "arabic";
            e.encoding   = "utf-8";
            e.fontSize   = "12pt";
            e.fontFamily = "tahoma";
            e.autoSave   = true;
            e.direction  = "right to left";

            // .ser إمتداده ,user-prefrences.ser هنا قمنا بإنشاء ملف إسمه
            FileOutputStream fos = new FileOutputStream("./user-prefrences.ser");

            // user-prefrences.ser لنتمكن من استخراج حالة أي كائن موجود في الذاكرة و وضعها في الملف ObjectOutputStream هنا قمنا بإنشاء كائن نوعه
            ObjectOutputStream oos = new ObjectOutputStream(fos);

            // لحفظ الإعدادات التي قمنا بإدخالها user-prefrences.ser في الملف e هنا قمنا بنسخ حالة الكائن
            oos.writeObject(e);

            // user-prefrences.ser في الأخير قمنا بقطع كل إتصال قمنا بإجرائه مع الملف
            oos.close();
            fos.flush();
            fos.close();

            // في حال عدم حدوث أي خطأ, سيتم طباعة الجملة التالية التي تعني أن العملية تمت بنجاح
            System.out.println("Serialized data has been saved in the project in a file called user-prefrences.ser");
        }
        catch(IOException ex)
        {
            // في حال حدوث أي خطأ عند نسخ البيانات من الذاكرة إلى الملف سيتم عرضه
            System.out.println(ex.getMessage());
        }

    }

}

•في المرة الأولى التي تقوم فيها بتشغيل البرنامج ستحصل على النتيجة التالية.

Serialized data has been saved in the project in a file called user-prefrences.ser

•في المرة الثانية التي تقوم فيها بتشغيل البرنامج ستحصل على النتيجة التالية.

Deserialized data has been created in the memory
Language:      arabic
Encoding:      utf-8
Font size:     12pt
Font family:   tahoma
Auto save:     true
Direction:     null
Auto Complete: false

Serialized data has been saved in the project in a file called user-prefrences.ser

بما أنه قد تم إنشاء الملف user-prefrences.ser بنجاح, يمكنك البحث عنه و فتحه بواسطة أي محرر و عندها ستتمكن من رؤية شكل المعلومات التي كانت مسجلة في الذاكرة.

لاحظ أنه لم يتم حفظ قيمة المتغير direction في الملف لأننا قمنا بتعريفها كـ transient, لذلك تم إعطائه القيمة null كقيمة إفتراضية.

المتغير serialVersionUID
كل كلاس يطبق الإنترفيس Serializable يتم إعطاءه رقم إصدار خاص فيه.
هذا الرقم يتم تخزينه في المتغير serialVersionUID.

إذاً كل كلاس يطبق الإنترفيس Serializable, يملك متغير إسمه serialVersionUID حتى لو لم يتم تعريفه.

رقم الإصدار يضمن أن المرسل و المستقبل للملف على الشبكة يملكون نفس نسخة الكلاس للكائن المحفوظ في الملف.
في حال كان رقم الإصدار في كلاس المرسل مختلف عن رقم الإصدار في كلاس المستقبل يتم رمي إستثناء من النوع InvalidClassException.

إذاً رقم الإصدار serialVersionUID مهم جداً عند بناء تطبيق يشارك البيانات بين سيرفر و عميل, أي يوجد تطبيق على السيرفر و تطبيق عند المستخدم العادي مرتبطان مع بعضهما البعض. سنرى ذلك في الدرس التالي.

بشكل عام, تعريف المتغير serialVersionUID ليس أمراً إجبارياً في حال كنت تبني برنامج لا تشارك فيه البيانات مع برنامج آخر, لأن جافا أصلاً ستقوم بتعريفه عنك في حال لم تقم بتعريفه بنفسك, لكن في بعض بيئات العمل مثل بيئة Eclipse, نلاحظ أن الـ Complier يظهر تحذير في حال لم نقم بتعريف المتغير serialVersionUID من جديد في البرنامج, لذلك ننصحك بتعريفه في جميع الحالات لأنه لن يؤثر أصلاً على الكود.


طريقة تعريف المتغير serialVersionUID
في البداية يمكنك وضع أي Access Modifier و لن يشكل ذلك أي فرق هنا, لكنك مجبر على تعريفه كـ static final long.


مثال
•في الإصدار الأول من الكلاس Editor وضعنا قيمته 1L

private static final long serialVersionUID = 1L;

•في الإصدار الثاني من الكلاس Editor وضعنا قيمته 2L

private static final long serialVersionUID = 2L;



تعليقات