تختلف الأنواع الكائنية (object types) بلغة الجافا عن الأنواع البسيطة (primitive type)، فمثلًا، لا تُنشِئ تَعْليمَة التّصْريح (declare) عن مُتْغيِّر، نوعه عبارة عن صَنْف (class)، كائنًا (object) من ذلك الصنف، بل ينبغي أن تقوم بذلك صراحةً. وفقًا للحاسوب، تَتَكوَّن عملية إنشاء كائن من محاولة العثور على مساحة غَيْر مُستخدَمة بقسم الكومة (heap) من الذاكرة وبحيث تَكُون كبيرة بما فيه الكفاية لحَمْل الكائن المطلوب إنشائه، ومن ثَمَّ مَلْئ مُتْغيِّرات نُسخ (instance variables) ذلك الكائن. كمبرمج، أنت لا تهتم عادةً بالمكان الذي يُخزَّن فيه الكائن بالذاكرة، ولكن ستَرغَب غالبًا بالتَحكُّم بالقيم المبدئية المُخزَّنة بمُتْغيِّرات نُسخ ذلك الكائن، كما قد تحتاج في بعض الأحيان إلى إجراء تهيئة (initialization) أكثر تعقيدًا مع كل كائن جديد يُنشَئ.
تهيئة متغيرات النسخ (initialization)
تستطيع عمومًا إِسْناد قيمة مبدئية إلى أيّ مُتْغيِّر نسخة (instance variable) أثناء التَّصْريح (declaration) عنه، وذلك كأيّ مُتْغيِّر عادي آخر. على سبيل المثال، لنَفْترِض مثلًا أن لدينا الصَنْف PairOfDice
المُعرَّف بالأسفل. سيُمثِل أيّ كائن من هذا الصَنْف حجري نَّرد، وسيَحتوِي على مُتْغيِّري نسخة لتمثيل الأعداد الظاهرة على حجري النَّرد، بالإضافة إلى تابع نُسخة (instance method) مسئول عن مُهِمّة رمِي حجرى النَّرد.
public class PairOfDice { public int die1 = 3; // العدد الظاهر على الحجر الأول public int die2 = 4; // العدد الظاهر على الحجر الثاني public void roll() { // حاكي تجربة رمي حجري النرد die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; } } // نهاية الصنف PairOfDice
وفقًا لتعريف الصَنْف بالأعلى، سيُهيَئ (initialize) مُتْغيِّري النُسخة die1
و die2
إلى القيم ٣ و ٤ على الترتيب أينما بَنينا كائنًا من الصنف PairOfDice
. ينبغي أن تفهم كيفية حُدوث ذلك: لمّا كان من المُمكن إِنشاء عدة كائنات من الصَنْف PairOfDice
، فإنه، وبكل مرة يُنشَئ فيها واحد منها، فإن الكائن المُنشَىء سيَحصُل على مُتْغيِّري نُسخة (instance variables) خاصين به، ثم ستُنفَّذ تَعْليمتَي الإِسْناد die1 = 3
و die2 = 4
لمَلئ قيم مُتْغيِّراته. اُنظر النسخة التالية من الصَنْف PairOfDice
لمزيد من الإيضاح:
public class PairOfDice { public int die1 = (int)(Math.random()*6) + 1; public int die2 = (int)(Math.random()*6) + 1; public void roll() { die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; } } // نهاية الصنف PairOfDice
وفقًا للتعريف بالأعلى، فإنه، وبكل مرة يُنشَئ فيها كائن (object) من الصَنْف PairOfDice
، ستُهيَئ مُتْغيِّرات النُسخ إلى قيم عشوائية كما لو كنا نَرمِي حجري نَّرد على طاولة اللعب. لمّا كانت تلك التهيئة (initialization) تُنفَّذ لكل كائن على حدى، فستُحسَب تلك القيم لكل حجري نَّرد، وسيَحصُل كل حجري نَّرد مختلفين على قيم مبدئية مختلفة. سيَكُون الوضع مختلفًا بالتأكيد في حالة تهيئة المُتْغيِّرات الأعضاء الساكنة (static member variables)؛ وذلك لوجود نُسخة وحيدة فقط من أيّ مُتْغيِّر ساكن (static)، والتي تُهيَئ (initialization) مرة واحدة فقط عند تحميل الصَنْف (class) لأول مرة.
تُهيَئ مُتْغيِّرات النُسخ (instance variable) بقيم مبدئية افتراضية تلقائيًا إذا لَمْ يُوفِّر لها المبرمج قيمة مبدئية. فمثلًا، تُهيَئ مُتْغيِّرات النسخ من الأنواع العددية مثل int
و double
تلقائيًا إلى الصفر إذا لم يُوفِّر لها المبرمج قيم آخرى، أما المُتْغيِّرات من النوع boolean
فتُهيئ إلى القيمة false
، بينما المُتْغيِّرات من النوع char
تُهيَئ للمحرف المقابل لقيمة ترميز اليونيكود (Unicode code) رقم صفر \u0000
. وأخيرًا، بالنسبة لمُتْغيِّرات النُسخ كائنية النوع (object type)، فإن قيمها المبدئية الافتراضية هي القيمة الفارغة null
. على سبيل المثال، لمّا كانت السَلاسِل النصية من النوع String
عبارة عن كائنات (objects)، فإن القيمة المبدئية الافتراضية للمُتْغيِّرات من النوع String
تُساوِي null
.
بواني الكائنات (constructors)
يُستخدَم العَامِل new
لإنشاء الكائنات (objects)، فمثلًا، يُمكِننا كتابة الشيفرة التالية لإنشاء كائن من الصَنْف PairOfDice
:
PairOfDice dice; // صرح عن متغير من النوع PairOfDice // أنشئ كائنا جديدا من الصنف واسنده مرجعه الى المتغير dice = new PairOfDice();
يُخصِّص التعبير new PairOfDice()
مساحة بالذاكرة للكائن، ويُهيِئ مُتْغيِّرات النسخ (instance variables) الخاصة به، وأخيرًا، يُعيد مَرجِعًا (reference) إليه كقيمة للتعبير، والتي تُخزِّنها تَعْليمَة الإِسْناد (assignment statement) بالمُتْغيِّر dice
، أي سيُشير dice
إلى الكائن الجديد المُنشَئ للتو بعد تَّنْفيذ تَعْليمَة الإِسْناد. يبدو الجزء PairOfDice()
وكأنه عملية استدعاء لبرنامج فرعي (subroutine call). ليس هذا في الواقع مجرد مصادفة؛ فهو بالفعل يُمثِل عملية استدعاء، ولكن لنوع خاص من البرامج الفرعية تُعرَف باسم "البَانِي أو بَانِي الكائن (constructor)". قد يُربكك ذلك خاصة وأن تعريف الصَنْف PairOfDice
لا يَحتوِي على أي برنامج فرعي بنفس تلك البصمة. في الحقيقة، لابُدّ لأيّ صَنْف من أن يَحتوِي على بَانِي كائن (constructor) واحد على الأقل، لذا يُوفِّر النظام بَانِي كائن افتراضي (default constructor) لأي صَنْف لم يُعرِّف له المبرمج بَانِي كائن (constructor). يَقْتصِر دور البواني الافتراضية على تَخْصِيص مساحة بالذاكرة للكائن، وتهيئة مُتْغيِّرات النُسخ (instance variables)، أما إذا أردت تَّنْفيذ أشياء آخرى عند إنشاء كائن من صَنْف معين، فستحتاج إلى تعريف باني كائن (constructor) واحد أو ربما أكثر ضِمْن تعريف ذلك الصَنْف.
تُشبه تعريفات البَوانِي (constructors) في العموم تعريف أيّ برنامج فرعي (subroutine) آخر مع ثلاثة استثناءات. أولًا، لا يُمكِن تَخْصيص النوع المُعاد (return type) من البَانِي بما في ذلك المُبدِّل void
. ثانيًا، ينبغي أن يَكُون اسم الباني هو نفسه اسم الصَنْف المُعرَّف بداخله. أخيرًا، تَقْتصِر المُبدِّلات (modifiers) التي يُمكِن اِستخدَامها بتعريف أيّ باني على مُبدِّلات الوصول (access modifiers)، أي public
و private
و protected
، ولا يُمكِن اِستخدَام المُبدِّل static
بالتحديد أثناء التَّصْريح عنها.
في المقابل، يُمكِنك كتابة مَتْن البَانِي ككُتلَة من التَعْليمَات (statements) كأيّ مَتْن برنامج فرعي (subroutine body) تقليدي آخر؛ فلا يوجد أي قيود على التَعْليمَات التي يُمكِن اِستخدَامها ضِمْن المَتْن، كما يُمكِن للبَانِي أن يَستقبِل قائمة من المُعامِلات الصُّوريّة (formal parameters)، بل إن قدرته على اِستقبَال تلك المُعامِلات هي السبب الرئيسي من اِستخدَامه أساسًا، حيث تُوفِّر تلك المُعامِلات البيانات الضرورية لإِنشاء الكائن (object)، فمثلًا، يُمكِن لأحد بواني الصَنْف PairOfDice
أن يُوفِّر قيم الأعداد المبدئية لحجري النَّرد. تَعرَض الشيفرة التالية تعريف الصَنْف في تلك الحالة:
public class PairOfDice { public int die1; // العدد الظاهر بالحجر الأول public int die2; // العدد الظاهر بالحجر الثاني public PairOfDice(int val1, int val2) { // يُنشئ الباني حجري نرد ويُهيئهما مبدئيًا بالقيم الممررة die1 = val1; die2 = val2; } public void roll() { // حاكي تجربة الرمي die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; } } // نهاية الصنف PairOfDice
صَرَّحت الشيفرة بالأعلى عن بَانِي كائن (constructor) على الصورة public PairOfDice(int val1, int val2) ...
. ستُلاحِظ أننا لَمْ نُخصِّص النُوع المُعاد (return type) من البَانِي، كما أننا أعطيناه نفس اسم الصَنْف، وهذه هي الكيفية التي تُمكِّن مُصرِّف الجافا (Java compiler) من تَمييز بَوانِي الكائن. نُلاحِظ أيضًا أن البَانِي يَستقبِل مُعامِلين (parameters)، ينبغي لقيمهما أن تُمرَّر عند استدعاء البَانِي. على سبيل المثال، سيُنشِئ التعبير new PairOfDice(3,4)
كائنًا (object) من الصَنْف PairOfDice
، ويُهيِئ مُتْغيِّرات نُسخه die1
و die2
مبدئيًا إلى القيم ٣ و ٤ على الترتيب. أخيرًا، ينبغي أن تُستخدَم القيمة المُعادة (return value) من البَانِي بطريقة ما، كالتالي:
// صرح عن متغير من الصنف PairOfDice PairOfDice dice; // سيشير dice إلى كائن جديد من الصنف PairOfDice مُهيَأ مبدئيًا بالقيم 1 و 1 dice = new PairOfDice(1,1);
الآن، وبعد أن أَضفنا بَانِي كائن إلى الصَنْف PairOfDice
، لَمْ يَعُدْ بمقدورنا اِستخدَام التعبير new PairOfDice()
لإنشاء كائن؛ حيث تَحتوِي النسخة الجديدة من الصَنْف PairOfDice
-بالأعلى- على بَانِي وحيد، والذي يَتَطلَّب مُعامِلين فعليين (actual parameter)، بينما نحن نحاول استدعاء بَانِي بدون أيّ مُعامِلات، ولأن النظام لا يُوفِّر البَانِي الافتراضي (default constructor) سوى للأصناف التي لا يَتَضمَّن تعريفها (class definition) أي بَانِي على الإطلاق، فسنحتاج إلى إضافة بَانِي آخر لا يَستقبِل أي مُعامِلات للصنف، وهو في الواقع أمر بسيط. تستطيع عمومًا إضافة أيّ عدد من البَوانِي (constructors) طالما كانت بَصْمتهم (signatures) مختلفة، أيّ طالما كان لديهم أعداد مختلفة أو أنواع مختلفة من المُعامِلات الصُّوريّة (formal parameters). بالشيفرة التالية، أَضفنا بَانِي بدون أي مُعامِلات إلى الصَنْف PairOfDice
سيُهيِئ حجري النَّرد بقيم مبدئية عشوائية، كالتالي:
public class PairOfDice { public int die1; // العدد الظاهر بالحجر الأول public int die2; // العدد الظاهر بالحجر الثاني public PairOfDice() { // يرمي الباني حجري النرد ليحصل على قيم عشوائية مبدئية roll(); } public PairOfDice(int val1, int val2) { // يُنشئ الباني حجري نرد ويُهيئهما مبدئيًا بالقيم الممررة die1 = val1; die2 = val2; } public void roll() { // حاكي تجربة الرمي die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; } } // end class PairOfDice
الآن، تستطيع إنشاء كائن من الصَنْف PairOfDice
بطريقتين، إِما باِستخدَام التعبير new PairOfDice()
أو باِستخدَام التعبير new PairOfDice(x,y)
، حيث x
و y
هي تعبيرات من النوع العَدَدَي الصحيح.
تستطيع أيضًا اِستخدَام نفس الصَنْف -بالأعلى- بأي برنامج آخر يَتَعامَل مع حجري نَّرد، وبذلك لن تَضَطرّ إلى اِستخدَام التعبير الغامض نوعًا ما (int)(Math.random()*6)+1
مرة آخرى؛ لأنه مُضمَّن بالفعل داخل الصَنْف PairOfDice
، أيّ ستحتاج للتَعامُل مع مسألة رمِي حجري النَّرد مرة واحدة ضِمْن الصَنْف، ثم لا حاجة للقلق بشأنها مُجددًا. بالمثال التالي، يَستخدِم البرنامج main
الصَنْف PairOfDice
؛ لعدّ عدد مرات رمِي زوجين من حجري النَّرد حتى يَتساوَى حاصل مجموع كِلا الزوجين، وهو ما يُوضِح إِمكانية إنشاء عدة نُسخ (instances) من نفس الصَنْف:
public class RollTwoPairs { public static void main(String[] args) { PairOfDice firstDice; // يشير إلى الزوج الأول من حجري النرد firstDice = new PairOfDice(); PairOfDice secondDice; // يشير إلى الزوج الثاني من حجري النرد secondDice = new PairOfDice(); int countRolls; // عدد مرات الرمي int total1; // حاصل مجموع الزوج الأول int total2; // حاصل مجموع الزوج الثاني countRolls = 0; do { // إرم زوجي حجري النرد حتى يتساوى حاصل مجموع كلا منهما firstDice.roll(); // إرم الزوج الأول total1 = firstDice.die1 + firstDice.die2; // Get total. System.out.println("First pair comes up " + total1); secondDice.roll(); // إرم الزوج الثاني total2 = secondDice.die1 + secondDice.die2; // Get total. System.out.println("Second pair comes up " + total2); countRolls++; // أزد عدد الرميات System.out.println(); // Blank line. } while (total1 != total2); System.out.println("It took " + countRolls + " rolls until the totals were the same."); } // نهاية main() } // نهاية الصنف RollTwoPairs
البَوانِي في العموم عبارة عن برامج فرعية (subroutines)، ولكن من نوع خاص. هي بلا شك ليست توابع نُسخ (instance methods)؛ لأنها لا تنتمي إلى الكائنات (objects). لكونها مسئولة عن إِنشاء الكائنات، أي لابُدّ من وُجودها قَبْل وُجود أيّ كائن من الصَنْف، فإنها قد تَكُون أكثر شبهًا بالبرامج الفرعية الأعضاء الساكنة (static) مع أنه لا يُسمَح باِستخدَام المُبدِّل static
أثناء تعريفها. تقنيًا، البَوانِي ليست أعضاء (members) ضِمْن الصَنْف على الإطلاق، ولا يُشار إليها على أساس كَوْنها توابع (methods) بالصَنْف.
بخلاف البرامج الفرعية (subroutines) الآخرى، تستطيع استدعاء البَوانِي فقط من خلال العَامِل new
، ويُكْتَب على الصيغة التالية:
new <class-name> ( <parameter-list> )
قد تَكُون قائمة المُعامِلات
يُعدّ استدعاء البَانِي (constructor call) عمومًا أكثر تعقيدًا من أيّ استدعاء عادي لبرنامج فرعي (subroutine) أو لدالة (function)، لذا من المهم أن تَفهَم الخطوات التي يُنفِّذها الحاسوب أثناء استدعاء البَوانِي:
- يَعثُر الحاسوب على كُتلَة غَيْر مُستخدَمة بقسم الكَوْمة (heap) من الذاكرة، بشَّرْط أن تَكُون كبيرة بما فيه الكفاية لتَحمِل كائن من النوع المُخصَّص.
- يُهيِئ الحاسوب مُتْغيِّرات النُسخ (instance variables) للكائن، فإذا كان التَّصْريح عنها يَتَضمَّن قيمة مبدئية، فإنه يَحسِب تلك القيمة ويُخزِّنها بمُتْغيِّر النُسخة. أما إن لَمْ تَكُن مُضمَّنة، فإنه يَستخدِم القيمة المبدئية الافتراضية.
- تُحسَب قيم المُعامِلات الفعليّة (actual parameters) بالبَانِي -إن وُجدت-، ثم تُسنَد إلى المُعامِلات الصُّوريّة (formal parameters).
- تُنفَّذ التَعْليمَات الموجودة بمَتْن البَانِي (constructor body) إن وُجدت.
- يُعاد مَرجِع (reference) إلى الكائن المُنشَئ كقيمة لتعبير استدعاء البَانِي (constructor call)
تَكُون النتيجة النهائية هي حُصولك على مَرجِع (reference) يُشير إلى الكائن المُنشَئ حديثًا. كمثال آخر، سنُضيف بَانِي كائن (constructor) إلى الصَنْف Student
المُستخدَم بالقسم الأول، كذلك أَضفنا مُتْغيِّر نُسخة (instance variable) خاص اسمه هو name
. اُنظر الشيفرة:
public class Student { private String name; // اسم الطالب public double test1, test2, test3; // درجات الطالب public Student(String theName) { // يستقبل باني الكائن اسم الطالب if ( theName == null ) throw new IllegalArgumentException("name can't be null"); name = theName; } public String getName() { // تابع جلب لقراءة قيمة متغير النسخة return name; } public double getAverage() { // احسب متوسط درجات الطالب return (test1 + test2 + test3) / 3; } } // نهاية الصنف Student
عَرَّفنا بَانِي كائن (constructor) يَستقبِل مُعامِلًا (parameter) من النوع String
ليُمثِل اسم الطالب.
يَحتوِي أي كائن من النوع Student
على بيانات طالب معين، ويُمكِننا عمومًا إِنشاء كائنات من ذلك الصَنْف باِستخدَام التَعْليمَات التالية مثلًا:
std = new Student("John Smith"); std1 = new Student("Mary Jones");
بالنسخة الأصلية من ذلك الصَنْف، كان المُبرمج يَضطرّ لإِسْناد قيمة مُتْغيِّر النُسخة name
بعد إنشاء الكائن، مما يَعنِي عدم وجود ضمانة لأن يَتَذكَّر المُبرمج ضَبْط قيمة ذلك المُتْغيِّر بصورة سليمة. في المقابل، بالنسخة الجديدة من الصَنْف، لا يُمكِن إنشاء كائن من الصَنْف Student
إلا عن طريق استدعاء البَانِي (constructor)، والذي يَضبُط قيمة مُتْغيِّر النُسخة name
تلقائيًا، كما أنه يَضمَن ألا تَكُون تلك القيمة فارغة. يُسهِل ذلك عمومًا من عَمَل المبرمج، ويُجنِّبه عددًا كبيرًا من الأخطاء البرمجية (bugs). لاحِظ أننا قد اِستخدَمنا المُبدِّل private
ضِمْن تَعْليمَة التَّصْريح عن مُتْغيِّر النُسخة name
، وهو ما يُوفِّر نوعًا آخر من الضمانة؛ فبذلك لن يَتَمكَّن أي مكان بالشيفرة خارج الصنف Student
من الوصول مباشرة إلى مُتْغيِّر النُسخة name
، فمثلًا، تُضبَط قيمته بشكل غَيْر مباشر عند استدعاء البَانِي (constructor). في حين يَتَضمَّن الصَنْف دالة جَلْب (getter function) هي getName()
، والتي يُمكِن من خلالها مَعرِفة قيمة مُتْغيِّر النُسخة name
لطالب معين من مكان خارج الصَنْف، فإن الصَنْف لا يَحتوِي على أيّ تابع ضَبْط (setter method) أو على أي طريقة آخرى لتَعْدِيل قيمة المُتْغيِّر name
، أيّ بمُجرَّد إنشاء كائن من ذلك الصَنْف، فإنه سيَظلّ مُحتفِظًا بنفس قيمة المُتْغيِّر name
المَبدئية طوال فترة وجوده بالبرنامج.
في تلك الحالة، رُبما من الأفضل التَّصْريح عن مُتْغيِّر النُسخة name
باِستخدَام المُبدِّل final
. يُمكِنك التَّصْريح عن أيّ مُتْغيِّر نُسخة (instance variable) عمومًا باِستخدَام المُبدل final
بشَّرْط إِسْناد قيمة إليه إما بتَعْليمَة التَّصْريح (declaration) أو بكل البواني (constructor) المُعرَّفة بذلك الصَنْف. في العموم، لا يُمكِن إِسْناد قيمة إلى مُتْغيِّر نُسخة (instance variable) قد صُرِّح عنه باِستخدَام المُبدِّل final
، ولكن يُستَثنَى من ذلك مَتْن البواني (constructor).
حسنًا، ماذا سيَحدُث عندما تَستخدِم أعضاء ساكنة (static) وآخرى غَيْر ساكنة (non-static) ضِمْن نفس الصَنْف؟ يَعرِف أيّ كائن عمومًا الصَنْف الذي يَنتمِي إليه، ويُمكِنه الإشارة إلى أعضاء صَنْفه الساكنة (static members). لذا يُمكِن لأيّ تابع نُسخة (instance method) بالصَنْف أن يُشير إلى المُتْغيِّرات الأعضاء (member variables) الساكنة أو أن يَستدعِي البرامج الفرعية الأعضاء (member subroutines) الساكنة. لكن، لاحِظ أن هناك دائمًا نُسخة وحيدة فقط من أيّ عضو ساكن، والتي تَنتمِي للصَنْف ذاته، مما يَعنِي أن جميع كائنات صَنْف معين تَتَشارك نُسخة وحيدة من أيّ عضو ساكن مُعرَّف بالصَنْف.
كمثال، اُنظر النسخة التالية من الصَنْف Student
، والتي أُضيف إليها مُتْغيِّر نُسخة ID
لكل طالب، بالإضافة إلى عضو ساكن (static member) هو nextUniqueID
. على الرغم من وجود مُتْغيِّر ID
بكل كائن من النوع Student
، فهناك مُتْغيِّر nextUniqueID
واحد فقط:
public class Student { private String name; // اسم الطالب public double test1, test2, test3; // درجات الطالب private int ID; // رقم معرف لهوية الطالب private static int nextUniqueID = 0; // للاحتفاظ برقم الهوية المتاح التالي Student(String theName) { // باني كائن يستقبل اسم الطالب ويسند رقم هوية فريد إليه name = theName; nextUniqueID++; ID = nextUniqueID; } public String getName() { // تابع جلب لقراءة قيمة متغير النسخة name return name; } public int getID() { // تابع جلب لقراءة قيمة رقم الهوية return ID; } public double getAverage() { // احسب متوسط درجات الطالب return (test1 + test2 + test3) / 3; } } // نهاية الصنف Student
لمّا كان المُتْغيِّر nextUniqueID
ساكنًا (static)، فإنه يُهيَئ مبدئيًا باِستخدَام التعبير nextUniqueID = 0
مرة واحدة فقط عند تحميل الصَنْف لأول مرة. بعد ذلك، عندما نُحاوِل إنشاء كائن من الصَنْف Student
، سيُنفِّذ الباني (constructor) التَعْليمَة nextUniqueID++;
، والتي تُزِيد دائمًا قيمة نفس المُتْغيِّر العضو الساكن (static member variable) بمقدار الواحد، فتُصبِح قيمة المُتْغيِّر nextUniqueID
مُساوِية للعدد ١ بَعْد إنشاء أول كائن من الصَنْف Student
، ثم تُصبِح مُساوِية للعدد ٢ بَعْد إنشاء الكائن الثاني، وتُصبِح مُساوِية للعدد ٣ بَعْد الكائن الثالث، ويستمر المُتْغيِّر بالزيادة مع كل كائن جديد يُنشَىء من ذلك الصَنْف. في المقابل، لمّا كان المُتْغيِّر ID
عبارة عن مُتْغيِّر نُسخة (instance variable)، فإن كل كائن لديه نُسخته الخاصة من ذلك المُتْغيِّر، والتي يُخْزِّن فيها البَانِي القيمة الجديدة من المُتْغيِّر nextUniqueID
بَعْد إِنشائه للكائن. صُمّم الصَنْف Student
عمومًا بحيث يَحصُل كل طالب على قيمة مختلفة لنُسخته من المُتْغيِّر ID
تلقائيًا. ولأننا قد صَرَّحنا عن المُتْغيِّر ID
بكَوْنه خاصًا (private)، فيَستحِيل أيضًا التَلاعُب بقيمة ذلك المُتْغيِّر بأيّ طريقة بَعْد إنشاء الكائن، وهو ما يَضمَن تَخْصِيص رقم هوية فريد ودائم لكل كائن من الصَنْف Student
، وهو أمر رائع إذا فكرت بالأمر!
لو أعدت التفكير بالأمر، ستَجِدْ أن تلك الضمانة ليست مُطلقة تمامًا، وإنما هي مُقْتصِرة فقط على البرامج التي تَستخدِم خيطًا (thread) واحدًا، أما البرامج مُتعددة الخيوط (multi-thread)، أي التي يُمكِن خلالها تَّنْفيذ عدة أشياء بنفس ذات الوقت، فإن الأشياء تُصبِح غريبة قليلًا، فقد يُنشِئ خيطان كائنين من الصَنْف Student
بنفس ذات الوقت، ولهذا قد يَحصُل كِلا الكائنين في تلك الحالة على نفس رقم الهوية. سنعود إلى هذا الموضوع بالقسم الفرعي ١٢.١.٣، حيث ستَتَعلَّم كيفية حلّ تلك المشكلة.
كانس المهملات (garbage collection)
حتى الآن، كان حديثنا خلال هذا القسم مُقْتصِرًا على إنشاء الكائنات، فماذا عن هَدْمِها؟ تُحذَف الكائنات بلغة الجافا أتوماتيكيًا. يَقع أيّ كائن عمومًا بقسم الكَوْمة (heap) من الذاكرة، ويُمكِن الوصول إلى كائن معين فقط عَبْر المُتْغيِّرات التي تَحمِل مَراجِع (references) تُشير إليه. والآن، ماذا سيَحدُث لو لم يَعُدْ هناك أيّ مُتْغيِّرات تُشير إلى كائن معين؟ اُنظر مثلًا للشيفرة التالية (لاحِظ أنها لأغراض تعليمية فقط؛ فليس من المُحتمل على الإطلاق كتابة تَعْليمَتين على هذا النحو ببرنامج فعليّ):
Student std = new Student("John Smith"); std = null;
بالسطر الأول، اُنشِئ كائن من الصَنْف Student
، ثم خُزِّن مَرجِع (reference) يُشير إليه بالمُتْغيِّر std
، ثم بالسطر التالي، تَغيَّرت قيمة std
، ولم يَعُدْ مَرجِع الكائن المُنشَئ للتو موجودًا، بل لم يَعُدْ هناك أي مَراجِع (references) تُشير إليه بأي مُتْغيِّر آخر، وبالتالي، لن يَتَمكَّن البرنامج من اِستخدَام ذلك الكائن مُجددًا، ولهذا يَنبغي استعادة مساحة الذاكرة المُخصَّصة للكائن لكي تُصبِح مُتاحة للاِستخدَام لأغراض آخرى.
تَستخدِم الجافا إجراءً (procedure) يُعرَف باسم "كَنْس المُهملات (garbage collection)"؛ لاستعادة أجزاء الذاكرة التي كانت قد خُصِّصت للكائنات (objects)، والتي لَمْ يَعُدْ بإِمكان البرنامج الوصول إليها. يَتولَّى النظام -لا المبرمج- مسئولية تَعقُّب تلك الكائنات التي أَصبحت ضِمْن "المُهملات (garbage)". بالمثال السابق، تستطيع بسهولة أن تَرى أن الكائن قد أَصبح ضِمْن المُهملات، ولكن تَكُون الأمور في العادة أكثر تعقيدًا خاصة بَعْد اِستخدَام الكائن لفترة، فيُحتمَل عندها وجود عدة مَراجِع (references) تُشير إليه مُخزَّنة بعدة مُتْغيِّرات. لاحِظ أن الكائن لا يُعدّ جزءًا من المُهملات إلا بعد أن تُصبِح جميع مَراجِعه غَيْر مُتوفرة.
تَقَع مسئولية حَذْف المُهملات على عاتق المبرمج بالكثير من اللغات البرمجية الأخرى. ولمّا كان تَعقُّب اِستخدَام الذاكرة يَنطوِي على الكثير من التعقيدات، فإنه عادةً ما يَتسبَّب بحُدوث الكثير من الأخطاء البرمجية (bugs) الخطيرة. أحد تلك الأخطاء هو خطأ المُؤشر المُعلّق (dangling pointer)، والذي يَحدُث عندما يَحذِف المبرمج عن طريق الخطأ كائنًا من الذاكرة ما يزال البرنامج يَملُك مَرجِعًا (references) إليه، مما يَتسبَّب بحُدوث مشاكل إذا حَاول البرنامج الوصول إلى كائن لم يَعُدْ موجودًا. نوع آخر من الأخطاء هو تَسريب الذاكرة (memory leak)، والذي يَحدُث عندما لا يهتم المبرمج بحَذْف الكائنات التي لم تَعُدْ مُستخدَمة، مما يُؤدي إلى مَلئ الذاكرة بكائنات غَيْر قابلة للوصول، ومِن ثَمَّّ قد يُعاني البرنامج من مشكلة نفاد الذاكرة مع أنها فقط مُهدرة في الواقع.
يَستحِيل حُدوث مثل تلك الأخطاء بالجافا؛ لأنها تَعتمِد على إجراء كَنْس المُهملات (garbage collection)، وهو عمومًا فكرة قديمة اِستخدَمتها بعض اللغات البرمجية بدايةً من ستينيات القرن الماضي. لماذا إذًا لا تَستخدِمها جميع اللغات؟ لأنها في الماضي كانت بطيئة جدًا، ولكنها أَصبحت عملية بفضل العديد من الدراسات العلمية التي تناولت موضوع كَنْس المهملات عن كثب، بالإضافة إلى السرعة الفائقة للحواسيب الحديثة.
ترجمة -بتصرّف- للقسم Section 2: Constructors and Object Initialization من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.