اذهب إلى المحتوى

مدخل إلى التعامل مع الملفات في جافا


رضوى العربي

ظل البيانات والبرامج المُخزَّنة بذاكرة الحاسوب الرئيسية main memory متوفرةً طوال فترة تشغيله، ولكن يجب الاستعانة بالملفات للإبقاء عليها بصورة دائمة؛ حيث تمثِّل مجموعةً من البيانات المُخزَّنة بقرصٍ صلب hard disk، أو بشريحة ذاكرة USB، أو بقرصٍ مضغوط CD-ROM، أو بأي نوعٍ آخر من أجهزة التخزين. تُنظَّم الملفات داخل مجلدات، وبإمكان كل مجلدٍ أن يحتوي على مجلداتٍ أخرى إلى جانب الملفات، كما يَملُك كل مجلدٍ وملف اسمًا يُعرِّف هويته.

تستطيع البرامج عمومًا قراءة البيانات من ملفاتٍ موجودة، وكذلك إنشاء ملفاتٍ جديدة وكتابة البيانات بها، وتعتمد جافا على مجاري تدفق streams الدْخل والخرج لفعل ذلك؛ حيث تُستخدَم الكائنات المنتمية للصنف FileReader -وهو صنفٌ فرعيٌ من الصنف Reader- لقراءة البيانات المحرفية المهيأة للقراءة human-readable من ملفٍ معين؛ وتُستخدَم بالمثل الكائنات المنتمية للصنف FileWriter -وهو صنفٌ فرعيٌ من الصنف Writer- لكتابة البيانات المهيأة للقراءة بملف.

يُستخدَم الصنفان FileInputStream و FileOutputStream للتعامل مع الملفات التي تُخزِّن البيانات بصيغةٍ مهيأة للآلة. سنناقش خلال هذا المقال الأصناف التي تتعامل مع الملفات بالصيغة المحرفية فقط، أي الصنفين FileReader و FileWriter، ولكن تذكَّر أن الصنفين FileInputStream و FileOutputStream يُستخدمان في العموم بنفس الطريقة. لاحِظ أن كلَّ تلك الأصناف مُعرَّفةٌ في حزمة java.io.

قراءة الملفات والكتابة بها

يستقبل باني constructor الصنف FileReader اسم ملف معين مثل معاملٍ parameter، ويُنشِئ مجرى stream مُدْخَلات لقراءة محتويات ذلك الملف؛ فإذا لم يَكُن الملف المُخصَّص موجودًا، فسيُبلِّغ الباني عن استثناء exception من النوع FileNotFoundException. بفرض لدينا ملفٌ اسمه "data.txt"، ونريد قراءة البيانات الموجودة به، يُمكِننا إذًا إنشاء مجرى مُدْخَلات لذلك الملف بكتابة ما يلي:

FileReader data;   // 1

try {
   data = new FileReader("data.txt");  // أنشِئ المجرى
}
catch (FileNotFoundException e) {
   ... // عالج الخطأ المُحتمَل
}

حيث تعني [1]: صرِّح عن المُتغيّر قبل تعليمة try؛ وإلا سيُصبِح محليًا ضمن كتلة try، ولن تتمكَّن من اِستخدَامه بالبرنامج لاحقًا.

يُمكِننا في الواقع ضبط تعليمة try...catch بالأعلى، بحيث تلتقط استثناءات الصنف IOException؛ لأن الصنف FileNotFoundException هو بالنهاية صنفٌ فرعيٌ subclass من الصنف IOException، ويُمكِننا في العموم التقاط أي خطأٍ يحدث أثناء عمليات الدخل والخرج باستخدام عبارة catch خُصِّصت لمعالجة الاستثناءات من النوع IOException.

يُمكِننا أن نبدأ بقراءة البيانات من كائنات الصنف FileReader بمجرد إنشائها، ولكن نظرًا لعدم تضمُّنها سوى بعض التوابع البسيطة، فسنضطّر عادةً إلى تغليفها ضمن كائنٍ من النوع Scanner أو النوع BufferedReader أو أي صنفٍ مُغلِّف آخر. انظر المقال السابق لمزيدٍ من المعلومات عن الصنفين BufferedReader و Scanner. تُنشِئ الشيفرة التالية كائنًا من النوع BufferedReader لقراءة بيانات ملفٍ اسمه "data.dat":

BufferedReader data;

try {
   data = new BufferedReader( new FileReader("data.dat") );
}
catch (FileNotFoundException e) {
   ... // عالج الاستثناء
}

يُسهِل تغليف كائنات الصنف Reader بكائناتٍ تنتمي للصنف BufferedReader من قراءة أسطر الملفات، كما تُعزِّز خاصية التخزين المؤقت buffering من كفاءتها.

يُمكِننا بنفس الكيفية إنشاء كائنٍ من الصنف Scanner لقراءة بيانات ملفٍ معين، مع أننا نلجأ عادةً في مثل تلك الحالات إلى إنشاء كائنٍ من النوع File مباشرةً (سنناقش ذلك بالأسفل):

Scanner in;

try {
   in = new Scanner( new File("data.dat") );
}
catch (FileNotFoundException e) {
   ... // عالِج الاستثناء
}

ينطبق الأمر نفسه على ملفات الخرج؛ حيث ينبغي في تلك الحالة إنشاء كائنٍ من النوع FileWriter، والذي نلجأ عادةً إلى تغليفه ضمن كائنٍ من النوع PrintWriter. قد يُبلِّغ باني الصنف FileWriter عن استثناء  من النوع IOException؛ ولهذا ينبغي أن نحيطه بتعليمة try..catch. لنفترض مثلًا أننا نريد كتابة بياناتٍ معينة بملفٍ اسمه "result.dat"، يُمكِننا أن نَستخدِم الشيفرة التالية:

PrintWriter result;

try {
   result = new PrintWriter(new FileWriter("result.dat"));
}
catch (IOException e) {
   ... // عالِج الاستثناء
     
}

كما هو الحال مع الصنف Scanner، نُمرِّر عادةً في تلك الحالات معاملًا من النوع File لباني الصنف PrintWriter، ويؤدي ذلك إلى تغليف كائن الصنف File تلقائيًا ضمن كائنٍ ينتمي للصنف FileWriter، ثم يُنشِئ الحاسوب بعدها كائنًا من الصنف PrintWriter. انظر الشيفرة التالية:

PrintWriter result;

try {
   result = new PrintWriter(new File("result.dat"));
}
catch (IOException e) {
   ... // عالِج الاستثناء
    
}

بإمكاننا أيضًا أن نُمرِّر للباني سلسلةً نصيةً من النوع String، ويَعُدُّها الباني في تلك الحالة اسمًا لملف؛ بينما لو مرَّرنا سلسلةً نصيةً من النوع String إلى باني الصنف Scanner، فإنه لا يَعُدّها اسمًا لملف، وإنما يقرأ محارف السلسلة النصية ذاتها.

في حالة عدم وجود ملفٍ اسمه "result.dat"، يُنشَئ ملفٌ جديدٌ بنفس الاسم؛ أما إذا كان موجودًا بالفعل، تَحِلّ البيانات التي يُفترض من البرنامج كتابتها بالملف محلّ محتوياته الحالية. لاحِظ أنك لن تتلقَّ أي تحذيرٍ بشأن ذلك. إذا أردت تجنُّب حدوث ذلك، عليك أن تفحص أولًا فيما إذا كان هناك ملفٌ بنفس الاسم قبل إنشاء مجرًى له كما سنناقش لاحقًا. قد يُبلِّغ باني الصنف PrintWriter عن استثناء من النوع IOException، إذا حاولت إنشاء ملفٍ داخل قرصٍ غير مسموحٍ بالكتابة به، أي لا يُمكِن تعديله.

عندما تُنهِي عملك مع كائن من الصنف PrintWriter، يجب أن تستدعِي تابعه flush()‎ بكتابة شيءٍ مثل result.flush()‎؛ وذلك حتى تتأكَّد من إرسال الخرج بالكامل إلى مقصده؛ وإذا نسيت أن تَستدعِيه، قد لا يظهر بالملف بعض البيانات التي أرسلتها إليه.

بعد أن تُنهِي تعاملك مع ملفٍ معين، يُفضَّل أن تغلقه؛ بمعنى أن تُبلِّغ نظام التشغيل أنك انتهيت من اِستخدَامه. يُمكِنك أن تَستدعِي التابع close()‎ المُعرَّف بالصنف PrintWriter، أو BufferedReader، أو Scanner حتى تغلق الملف. بمجرد إغلاق ملفٍ معين، لا يُمكِنك أن تقرأ بياناته، أو أن تُرسِل إليه أية بيانات، إلا إذا أعدت فتحه مرةً أخرى بإنشاء مجرًى جديد.

قد يُبلِّغ التابع close()‎ بغالبية أصناف المجاري -بما في ذلك الصنف BufferedReader- عن حدوث استثنناء من النوع IOException، والذي لا بُدّ من معالجته. يُعيد لحسن الحظ الصنفان PrintWriter و Scanner تعريف override ذلك التابع لمنعه من التبليغ عن مثل تلك الاستثناءات. إذا نسيت إغلاق ملفٍ معين، فإنه يُغلَق أوتوماتيكيًا بعد انتهاء البرنامج أو قد يُغلّق قبل ذلك بواسطة كانس المهملات garbage collection، ولكن لا يُفضَّل الاعتماد على ذلك.

اقتباس

ملاحظة: يؤدي استدعاء close()‎ إلى استدعاء flush()‎ أتوماتيكيًا قبل اغلاق الملف.

يقرأ البرنامج التالي أعدادًا من ملفٍ اسمه "data.dat"، ثم يعيد كتابة نفس تلك الأعداد، ولكن بترتيبٍ معاكس إلى ملفٍ آخر اسمه "result.dat". (ملاحظة: يَفترِض البرنامج احتواء الملف "data.dat" على أعدادٍ حقيقية فقط). يَستخدِم هذا البرنامج الصنف Scanner لقراءة ملف الدْخَل، كما يعتمد على معالجة الاستثناءات لفحص المشكلات المحتملة. قد لا يكون البرنامج التالي مفيدًا تمامًا، ولكنه يُظهِر على الأقل أساسيات التعامل مع الملفات بوضوح:

import java.io.*;
import java.util.ArrayList;
import java.util.Scanner;

// 1
public class ReverseFileWithScanner {

    public static void main(String[] args) {

        Scanner data;        // لقراءة البيانات
        PrintWriter result;  // مجرى محارف خرج لإرسال البيانات

        ArrayList<Double> numbers;  // قائمة لحمل البيانات

        numbers = new ArrayList<Double>();

        try {  // أنشئ مجرى دخل
            data = new Scanner(new File("data.dat"));
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file data.dat!");
            return;  // أنهِ البرنامج بالعودة من البرنامج
        }

        try {  // أنشِئ مجرى خرج
            result = new PrintWriter("result.dat");
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't open file result.dat!");
            System.out.println("Error: " + e);
            data.close();  // أغلق الملف
            return;        // أنهِ البرنامج
        }

        while ( data.hasNextDouble() ) {  // اقرأ الملف حتى نهايته
            double inputNumber = data.nextDouble();
            numbers.add( inputNumber );
        }

        // اطبع الأعداد بترتيبٍ معكوس

        for (int i = numbers.size()-1; i >= 0; i--)
            result.println(numbers.get(i));

        System.out.println("Done!");

        data.close();
        result.close();

    }  // end of main()

} // end class ReverseFileWithScanner

حيث أن [1] يقرأ البرنامج الأعداد من ملفٍ اسمه "data.dat"، ثم يكتبها إلى ملفٍ اسمه "result.dat" بترتيبٍ معكوس. لا بُدّ أن يحتوي الملف المُدْخَل على أعدادٍ حقيقيةٍ فقط.

يتوقف البرنامج السابق عن قراءة بيانات الملف بمجرد قراءته لمُدْخَلٍ غير عددي، ولا يَعُدّه خطأً.

كما ذكرنا بنهاية مقال الاستثناءات exceptions وتعليمة try..catch في جافا، يَشيع نمط إنشاء "موردٍ resource" معينٍ أو فتحه، ثم اِستخدَامه، وغلقه، وهو نمطٌ مدعومٌ من قِبَل تعليمة try..catch. بحسب هذا السياق، تُعدّ الملفات بمثابة مواردٍ مثلها مثل أصناف Scanner و PrintWriter وغيرها من مجاري جافا للدْخَل والخرج. تُعرِّف جميع تلك الموارد التابع close()‎، ويُفضَّل طبعًا إغلاقها بعد الانتهاء من اِستخدَامها. نظرًا لأن تلك الأصناف تُنفِّذ الواجهة AutoCloseable، تُعدُّ جميعها مواردًا بحسب تعليمة try..catch، ولهذا يُمكِننا إذًا أن نَستخدِم تلك التعليمة لإغلاق الموارد أوتوماتيكيًا بمجرد انتهاء تنفيذ التعليمة دون الحاجة إلى إغلاقها يدويًا ضمن تعليمة finally، وذلك بفرض أنك فتحت المورد واِستخدَمته ضمن نفس تعليمة try..catch.

يُعدّ البرنامج التوضيحي ReverseFileWithResources.java نسخةً أخرى من المثال الذي تعرَّضنا له بالأعلى، حيث يَستخدِم البرنامج تعليمات try..catch لقراءة البيانات من ملف، وكتابتها إلى ملفٍ آخر. كنا قد فتحنا الملف ضمن تعليمة try، واِستخدَمناه ضمن تعليمة try أخرى بالنسخة الأصلية من البرنامج. يتطلّب في المقابل نمط المورد حدوث الخطوتين ضمن تعليمة try واحدة، ولهذا علينا إعادة ترتيب الشيفرة، وهو ما قد يُصعِّب معرفة مصدر الاستثناء. تتضمَّن الشيفرة التالية تعليمة try..catch واحدةً مسؤولةً عن فتح ملف المُدْخَلات وقرائته وغلقه أتوماتيكيًا:

try( Scanner data = new Scanner(new File("data.dat")) ) {
        // اقرأ الأعداد وأضِفها إلى المصفوفة
    while ( data.hasNextDouble() ) {  // اقرأ حتى تصل إلى نهايته
        double inputNumber = data.nextDouble();
        numbers.add( inputNumber );
    }
}
catch (FileNotFoundException e) {
        // قد يحدث إذا لم يكن الملف موجودًا أو لا يُمكِن قراءته
    System.out.println("Can't open input file data.dat!");
    System.out.println("Error: " + e);
    return;  // عند حدوث خطأmain() العودة من
}

يُنشِئ السطر الأول المورد data. تتضمَّن قواعد الصيغة syntax لتعليمة try التصريح عن المورد وإعطائه قيمةً مبدئية داخل أقواسٍ بعد كلمة try. يُمكِننا أن نُصرِّح عن عدة مواردٍ يَفصِل بينها فاصلةٌ منقوطة، وتُغلَق جميعها بترتيبٍ معاكسٍ لترتيب التصريح عنها.

الملفات والمجلدات

هناك بعض الجوانب الأخرى المُتعلِّقة بأسماء الملفات، والتي لم نذكرها حتى الآن. بدايةً، إذا أردنا أن نُشير إلى ملفٍ مُحدَّد بوضوح، فلا بُدّ أن نوفِّر معلوماتٍ كافيةً عن كُلٍ من اسم الملف واسم المجلد الواقع به؛ لأنك إذا استخدمت اسم ملفٍ بسيطٍ، مثل "data.dat"، أو "result.dat"، فسيَفترِض الحاسوب وجود ذلك الملف بمجلدٍ يُعرَف باسم "المجلد الحالي current directory أو المجلد الافتراضي أو مجلد العمل"، والذي لا يُمثِل مكانًا ثابتًا، فقد يُغيِّره المُستخدِم أو حتى البرنامج. ولهذا، إذا أردت أن تُشير إلى ملفٍ معين، وكان ذلك الملف موجودًا بمجلدٍ غير المجلد الحالي، فيجب أن تشير إليه بواسطة مساره؛ أي بتوفير معلوماتٍ عن كُلٍ من اسم الملف، واسم المجلد الواقع به الملف.

يتوفَّر نوعان من أسماء المسارات، وهو ما قد يُعقِّد الأمور قليلًا، وهما: أسماء مطلقة للمسارات absolute path names، وأسماء نسبية للمسارات relative path names؛ حيث يُحدِّد الاسم المطلق للمسار اسم ملفٍ واحدٍ فقط من بين جميع الملفات المُتاحة بالحاسوب بوضوح، بسبب احتواء اسم المسار في تلك الحالة على كافة المعلومات المُتعلِّقة باسم الملف وبالمجلد المُتضمِّن له؛ بينما يُوضِّح الاسم النسبي للمسار الكيفية التي يستطيع الحاسوب بها العثور على الملف بدءًا من المجلد الحالي.

تختلف مع الأسف قواعد صيغة كُلٍ من أسماء الملفات والمسارات من حاسوبٍ إلى آخر إلى حدٍ ما. ألقِ نظرةً على بعض الأمثلة على ذلك:

  • data.dat: يُمثِّل ملفًا اسمه "data.dat" مع فرض وجوده بالمجلد الحالي. ينطبق ذلك على أي حاسوب.
  • ‎/home/eck/java/examples/data.dat: يُمثِّل الاسم المطلق لمسارٍ معين بأنظمة تشغيل UNIX، بما في ذلك Linux و Mac OS X، ويُشير إلى ملفٍ اسمه "data.dat"، موجودٍ بمجلدٍ اسمه "examples"، موجودٍ بدوره بمجلدٍ اسمه "java"، وهكذا.
  • ‎C:\eck\java\examples\data.dat: يُمثِّل الاسم المطلق لمسارٍ معيّن بأنظمة تشغيل Windows.
  • examples/data.dat: يُمثِّل الاسم النسبي لمسارٍ معيّن بأنظمة تشغيل UNIX، حيث يُمثِّل "examples" اسم مجلدٍ يُفترَض وجوده بالمجلد الحالي؛ أما "data.dat" فهو اسم ملفٍ موجودٍ ضمن المجلد "examples". الاسم النسبي المكافئ لذلك المسار بأنظمة تشغيل Windows هو "examples\data.dat".
  • ‎../examples/data.dat: يُمثِّل الاسم النسبي لمسارٍ معيّنٍ بأنظمة تشغيل UNIX، ويَعنِي ما يلي: اذهب إلى المجلد المُتضمِّن للمجلد الحالي.، حيث ستَجِد هناك مجلدًا اسمه "examples"، اذهب إليه وستعثُر على ملفٍ اسمه "data.dat". تعني ".." عُدّ مجلدًا واحدًا للوراء. يُمثِّل "‎..\examples\data.dat" نفس المسار بأنظمة Windows.

إذا كنت تتعامل مع الملفات من خلال برنامج سطر أوامر، وكانت أسماء الملفات بسيطةً نوعًا ما، ومُخزَّنة أيضًا بنفس مجلد البرنامج، فقد تسير الأمور على ما يرام. سنرى لاحقًا في هذا المقال طريقةً أفضل تَسمَح للمُستخدِم باختيار الملفات من خلال برنامج واجهة مُستخدِم رسومية، وهو ما يساعد على تجنُّب مشكلات أسماء المسارات تمامًا.

تستطيع برامج جافا الإشارة إلى الاسم المطلق لمساري مجلدين مهمين، هما المجلد الحالي والمجلد الرئيسي للمُستخدِم؛ حيث تُعدّ أسماء تلك المجلدات خاصياتٍ بالنظام، ويُمكِن قراءتها باستدعاء الدوال التالية:

  • System.getProperty("user.dir")‎: تعيد قيمةً من النوع String تُمثِّل الاسم المطلق لمسار المجلد الحالي.
  • System.getProperty("user.home")‎: تعيد قيمةً من النوع String تُمثِّل الاسم المطلق لمسار المجلد الرئيسي للمُستخدِم.

تُوفِّر جافا الصنف java.io.File، والذي يُجنِّبنا لحسن الحظ كثيرًا من المشكلات المتعلّقة بالاختلافات بين أسماء المسارات بالمنصات المختلفة. لا يُمثِّل الكائن المنتمي لهذا الصنف أي ملفٍ تمثيلًا فعليًا، وإنما يُمثِّل "اسم الملف"، ومن الممكن أن يكون الملف الذي يُشير إليه الاسم موجودًا أو غير موجود. ينطبِق الأمر ذاته على المجلدات؛ أي يُمكِن لكائنٍ من النوع File أن يُمثِّل مجلدًا معينًا بنفس الكيفية التي يُمكِنه بها أن يُمثِّل ملفًا.

يَستقبِل الباني new File(String)‎ المُعرَّف بطبيعة الحال بالصنف File اسمًا لمسارٍ معين، ويُنشِئ كائنًا من النوع File يُشير إلى الملف الموجود بذلك المسار. يُمكِن لاسم المسار المُمرَّر أن يكون بسيطًا أو نسبيًا أو مُطلقًا. على سبيل المثال، يُنشِئ الباني new File("data.dat")‎ كائنًا من النوع File يُشير إلى ملفٍ اسمه "data.dat" بالمجلد الحالي. يتوفَّر باني آخر هو new File(File,String)‎، والذي يَستقبِل مُعاملين: الأول هو كائنٌ من النوع File يُشير إلى مجلدٍ معين، أما الثاني فيُمكِنه أن يكون اسمًا لملفٍ موجودٍ ضمن المجلد المُخصَّص، أو مسارًا نسبيًا من ذاك المجلد إلى الملف المطلوب.

تتضمَّن كائنات الصنف File توابع نسخ instance methods مفيدة. بفرض أن file هو مُتغيّرٌ من النوع File، يُمكِننا أن نَستخدِم أيًا من التوابع التالية:

  • file.exists()‎: يُعيد القيمة المنطقية true إذا كان الملف الذي يُخصِّصه الكائن file موجودًا. اِستخدِم هذا التابع إذا أردت تجنُّب كتابة بياناتك بينما تُنشِئ مجرى خرجٍ جديد على ملفٍ موجودٍ مُسبقًا. تعيد الدالة file.canRead()‎ القيمة true إذا كان الملف موجودًا وكان البرنامج يَملُك صلاحيةً لقرائته؛ بينما تعيد الدالة file.canWrite()‎ القيمة true إذا كان البرنامج يَملُك صلاحيةً للكتابة بذلك الملف.
  • file.isDirectory()‎: يُعيد القيمة المنطقية true إذا كان file يشير إلى مجلدٍ ما؛ بينما يعيد القيمة false إذا كان الكائن يشير إلى ملفٍ سواءً كان ذلك الملف موجودًا أم لا.
  • file.delete()‎: يحذِف الملف إذا كان موجودًا، ويعيد قيمةً منطقيةً للدلالة على نجاح عملية الحذف أو فشلها.
  • file.list()‎: إذا كان file يشير إلى مجلد، فستُعيد الدالة مصفوفةً من النوع String[]‎ تحتوي على أسماء الملفات الموجودة بذلك المجلد؛ أما إذا لم يَكن كذلك، فستُعيد القيمة الفارغة null. يعمل التابع file.listFiles()‎ بنفس الطريقة باستثناء أنه يعيد مصفوفةً عناصرها من النوع File وليس String.

يُنشِئ البرنامج التالي قائمةً بأسماء جميع الملفات الموجودة بمجلدٍ معين يُخصِّصه المُستخدِم. لاحِظ أننا اِستخدَمنا الصنف Scanner من أجل قراءة مُدخَلات المُستخدِم:

import java.io.File;
import java.util.Scanner;

// 1
public class DirectoryList {


   public static void main(String[] args) {

      String directoryName;  // اسم المجلد الذي أدخله المُستخدِم
      File directory;        // كائنٌ يشير إلى المجلد
      String[] files;        // مصفوفة بأسماء الملفات الموجودة بالمجلد
      Scanner scanner;       // لقراءة سطرٍ مُدْخل واحد أدخله المُستخدِم

      scanner = new Scanner(System.in);  // للقراءة من الدخل القياسي

      System.out.print("Enter a directory name: ");
      directoryName = scanner.nextLine().trim();
      directory = new File(directoryName);

      if (directory.isDirectory() == false) {
          if (directory.exists() == false)
             System.out.println("There is no such directory!");
          else
             System.out.println("That file is not a directory.");
      }
      else {
          files = directory.list();
          System.out.println("Files in directory \"" + directory + "\":");
          for (int i = 0; i < files.length; i++)
             System.out.println("   " + files[i]);
      }

   } // end main()

} // end class DirectoryList

حيث تعني [1]: يَعرِض هذا البرنامج قائمةً بالملفات الموجودة بالمجلد الذي خَصَّصه المُستخدِم. يطلب البرنامج من المُستخدِم كتابة اسم المجلد، فإذا لم يكن الاسم المُدْخَل مجلدًا، يطبع البرنامج رسالةً ويُغلق.

تتضمَّن جميع الأصناف المُستخدَمة للقراءة من الملفات والكتابة بها بُناة constructors كائن، حيث تَستقبِل تلك البُناة كائنًا من النوع File مثل معاملٍ. على سبيل المثال، إذا كان file متغيرًا من النوع File، وكنت تريد قراءة محارف من ذلك الملف، يُمكِنك إنشاء كائنٍ من النوع FileReader بكتابة new FileReader(file)‎.

صناديق نوافذ التعامل مع الملفات

تحتاج الكثير من البرامج إلى طريقةٍ تَسمَح بها للمُستخدِم باختيار ملفٍ معين، بحيث يُمكِنها بعد ذلك استخدام الملف المُخصَّص أثناء عمليات الدخل والخرج. إذا سَمحَنا للمُستخدِم بكتابة اسم الملف يدويًا، فإننا بذلك نفترض فهمه لطريقة عمل الملفات والمجلدات. في المقابل، إذا دعَّمنا البرنامج بواجهة مُستخدمٍ رسومية، فإننا سنُمكِّن المُستخدِم من اختيار الملف من خلال صندوق نافذة ملف file dialog box؛ حيث يُعدُّ هذا الصندوق نافذةً يستطيع البرنامج أن يفتحها إذا أراد أن يَسمَح للمُستخدِم باختيار ملفٍ معينٍ للدخل أو للخرج. توفِّر مكتبة جافا إف إكس JavaFX الصنف FileChooser ضمن حزمة javafx.stage، والذي يُمكِنه عرض صندوق نافذة للتعامل مع الملفات، وذلك بصورةٍ مُستقلة عن المنصة التي يَعمَل عليها البرنامج.

يَعرِض صندوق نافذة فتح ملف للمُستخدِم قائمةً بالملفات والمجلدات الفرعية الموجودة ضمن مجلدٍ معين، مما يَسمَح له بأن يختار بسهولةٍ ملفًا معينًا ضمن ذلك المجلد، كما تُمكِّنه من التنقل بين المجلدات. لا يستقبل باني الصنف FileChooser أية معاملات. لاحِظ أن صندوق النافذة لا يظهر تلقائيًا على الشاشة بمجرد إنشاء كائنٍ من النوع FileChooser، وإنما ينبغي استدعاء تابعٍ مُعرَّفٍ بهذا الكائن لإظهار صندوق النافذة. ستَستدعِي عادةً بعض توابع النسخ الأخرى المُعرَّفة بالكائن، والخاصة بضَبْط بعض خاصيات صندوق النافذة قبل أن تَعرِضها، حيث يمكن مثلًا تخصيص قيمةٍ افتراضيةٍ مثل اسم للملف.

قد يكون لصندوق النافذة "مالك owner" يمثّل نافذةً، أي كائنًا من النوع Stage بمكتبة جافا إف إكس JavaFX. لا يَستطيع المُستخدِم أن يتفاعل مع النافذة المالكة حتى يُنهِي تعامله مع صندوق النافذة المفتوح؛ إما بغلقه؛ أو باختيار ملف. يُمكِننا أن نُخصِّص مالك صندوق نافذةٍ معينة بتمريره معاملًا للتابع المسؤول عن عرض صندوق النافذة؛ كما يُمكِن للمالك أن يكون فارغًا، وعندها لا يتوقف تفاعل المُستخدِم مع أي نوافذ أثناء عرض صندوق النافذة.

هناك نوعان من صناديق نوافذ الملفات: صندوق نافذة فتح ملف وصندوق نافذة حفظ ملف؛ حيث يَسمَح الأول للمُستخدِم بتخصيص إحدى الملفات الموجودة مُسبقًا لفتحها وقراءتها بالبرنامج؛ أما الثاني فيَسمَح للمُستخدِم بتخصيص ملفٍ قد يكون موجودًا أو لا لكتابة بعض البيانات به. يُعرِّف الصنف FileChooser تابعي نسخة لعرض أيٍّ من الصندوقين على الشاشة. إذا كان fileDialog مُتغيرًا من النوع FileChooser، فإنه يُوفِّر التوابع التالية:

  • fileDialog.showOpenDialog(window)‎: يَعرِض صندوق نافذة فتح ملف على الشاشة، حيث يَستقبِل مُعاملًا يُمثِّل مالك صندوق النافذة المُفترَض فتحها. لا يعيد التابع أي قيمةٍ حتى يختار المُستخدِم ملفًا أو يَغلِق النافذة بدون اختيار أي ملف؛ حيث يعيد التابع في الحالة الأولى قيمةً من النوع File تُمثِّل الملف الذي اختاره المُستخدِم؛ بينما يُعيد في الحالة الثانية القيمة الفارغة null.
  • fileDialog.showSaveDialog(window)‎: يَعرِض صندوق نافذة حفظ ملف، مالكها هو المعامل window. يعمل كلٌ من معامل التابع والقيمة المعادة منه بنفس أسلوب التابع showOpenDialog()‎؛ فإذا اختار المُستخدِم ملفًا موجودًا بالفعل، فسيسأله النظام أوتوماتيكيًا فيما إذا كان يريد بالفعل استبدال ذلك الملف، ويُمكِنك في تلك الحالة تخزين البيانات بالملف المُخصَّص دون القلق بشأن أي خطأٍ غير متوقَّع.
  • fileDialog.setTitle(title)‎: يَستقبِل التابع سلسلةً نصيةً مثل معاملٍ لتخصيص عنوانٍ يَظهَر بشريط عنوان صندوق النافذة. ينبغي أن تَستدعِي هذا التابع قبل عرض صندوق النافذة.
  • fileDialog.setInitialFileName(name)‎: يَضبُط اسمًا افتراضيًا يظهر بصندوق مُدْخَلات اسم الملف. لاحِظ أن المعامل هو سلسلةٌ نصية؛ فإذا كانت القيمة المُمرَّرة للمعامل فارغة، فإن صندوق الإدخال يكون بدوره فارغًا. ينبغي استدعاء هذا التابع قبل عرض صندوق النافذة المعنيّة.
  • fileDialog.setInitialDirectory(directory)‎: يََضبُط أي مجلدٍ ينبغي عرضه مبدئيًا عند فتح صندوق نافذة فتح الملف. لاحِظ أن المعامل الذي يَستقبِله التابع يَكون من النوع File؛ فإذا كانت القيمة المُمرَّرة للمعامل فارغة، يعتمد المجلد المبدئي على الإعدادات الافتراضية للنظام (قد يكون المجلد الذي شَغلّت البرنامج منه)؛ أما إذا لم تَكن القيمة المُمرَّرة فارغة، فلا بُدّ أن تكون كائنًا من النوع File يُمثِّل مجلدًا لا ملفًا، وإلا سيقع خطأ. ينبغي استدعاء هذا التابع قبل عرض صندوق النافذة المعنيّة.

يتضمَّن أي برنامجٍ نموذجي يتعامل مع الملفات الأمرين "افتح" و "احفظ"؛ فعندما يختار المُستخدِم ملفًا معينًا لفتحه أو لحفظ البيانات به، يُمكِننا أن نُخزِّن كائن الصنف File الذي يُمثِّل الملف الذي اختاره المُستخدِم بمتغير نسخة instance variable، بحيث نَستخدِمه بعد ذلك لضبط المجلد المعروض مبدئيًا، أو حتى لضبط اسم الملف بالمرة التالية التي نُنشِئ خلالها صندوق نافذة ملف.

إذا كان editFile مُتغيّر نسخة يحتوي على الملف الذي اختاره المُستخدِم، وإذا لم يَكُن ذلك الملف فارغًا، فسيُعيد الاستدعاء editFile.getName()‎ سلسلةً نصيةً من النوع String وتُمثِّل اسم الملف؛ في حين سيُعيد الاستدعاء editFile.getParent()‎ كائنًا من النوع File يُمثِّل المجلد المُتضمِّن لذلك الملف.

ننتقل الآن للسؤال التالي: ما الذي ينبغي فعله في حالة حدوث خطأ بينما نقرأ الملف المَعنِيّ أو نكْتُب به؟ ينبغي عمومًا التقاط ذلك الخطأ وتبليغ المُستخدِم عن حدوثه؛ فإذا كان البرنامج مُدعَّمًا بواجهة مُستخدِم رسومية، فيُعرَض عادةً للمُستخدِم صندوق نافذةٍ آخر يحتوي على رسالة الخطأ مع زر "OK" لغلق الصندوق. لم نتعرَّض لصناديق النوافذ سابقًا، ولكن يُمكِننا في العموم إنشاء كائناتٍ من الصنف Alert المُعرَّف بحزمة javafx.scene.control بسهولة لنَعرِض بعضًا من أكثر صناديق النوافذ البسيطة شيوعًا. تَعرِض الشيفرة التالية طريقة عرض صندوق نافذة يحتوي على رسالة خطأ:

Alert errorAlert = new Alert( Alert.AlertType.ERROR, message );
errorAlert.showAndWait();

يُمكِننا أن نُجمِّع كل ما سبق لنكتُب البرنامج الفرعي النموذجي التالي المسؤول عن حفظ البيانات بملف، حيث يَستخدِم البرنامج الصنف FileChooser لاختيار الملف، والصنف PrintWriter لكتابة البيانات بصيغةٍ نصية:

private void doSave() {
    FileChooser fileDialog = new FileChooser(); 
    if (editFile == null) {
           // 1
        fileDialog.setInitialFileName("filename.txt");
        fileDialog.setInitialDirectory( 
                new File( System.getProperty("user.home")) );
    }
    else {
           // 2
        fileDialog.setInitialFileName(editFile.getName());
        fileDialog.setInitialDirectory(editFile.getParentFile());
    }
    fileDialog.setTitle("Select File to be Saved");
    File selectedFile = fileDialog.showSaveDialog(mainWindow);
    if ( selectedFile == null )
        return;  // لم يختر المستخدم ملفًا
    // 3
    PrintWriter out; 
    try {
        FileWriter stream = new FileWriter(selectedFile); 
        out = new PrintWriter( stream );
    }
    catch (Exception e) {
           // لا يملُك المُستخدِم على الأغلب صلاحيةً للكتابة بالملف
        Alert errorAlert = new Alert(Alert.AlertType.ERROR,
                "Sorry, but an error occurred while\n" +
                trying to open the file for output.");
        errorAlert.showAndWait();
        return;
    }
    try {
           .
           .   // ‫اكتب النص إلى الملف باستخدام PrintWriter
               //WRITE TEXT TO THE FILE, using the PrintWriter
           .
        out.flush(); // ‫هل هي ضرورية؟ ربما ستُنجز من خلال الأمر ()out.close 
        out.close();
        if (out.checkError())   // افحص الأخطاء المحتملة
            (need to check for errors in PrintWriter)
            throw new IOException("Error check failed.");
        editFile = selectedFile;
    }
    catch (Exception e) {
        Alert errorAlert = new Alert(Alert.AlertType.ERROR,
                "Sorry, but an error occurred while\n" +
                "trying to write data to the file.");
        errorAlert.showAndWait();
    }    
}

حيث:

  • [1]: لم يُعدَّل أي ملف. اضبط اسم الملف إلى "filename.txt" واسم المجلد إلى المجلد الرئيسي للمُستخدِم.
  • [2]: استرجع اسم الملف والمجلد لصندوق النافذة من الملف الذي يُعدِّله المُستخدِم حاليًا.
  • [3]: ملاحظة: لقد اختار المُستخدِم ملفًا، وفي حال وجود ملفٍ بنفس الاسم، فإنه قد أكّد بالفعل على رغبته بحذف الملف الموجود.

يُمكِننا تطبيق نفس الفكرة على الملفات غير النصية، مع استخدام نوعٍ مختلف من مجاري الخرج.

تَعمَل قراءة البيانات من ملفٍ معينٍ بنفس الأسلوب، ولهذا لن نناقش التابع المكافئ doOpen()‎. يُمكِنك مع ذلك الإطلاع على البرنامج التوضيحي TrivialEdit.java، حيث ستَجِد برامجًا فرعية subroutines مسؤولةً عن فتح الملفات النصية وحفظها، كما يَسمَح هذا البرنامج للمُستخدِم بتعديل بعض الملفات النصية الصغيرة، وتعديل البرامج الفرعية المُعرَّفة ضمنه، وإعادة اِستخدَامها ضمن برامج واجهات مُستخدِم رسومية GUI أخرى تتعامل مع الملفات.

ترجمة -بتصرّف- للقسم Section 2: Files من فصل Chapter 11: Input/Output Streams, Files, and Networking من كتاب Introduction to Programming Using Java.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...