Programming Basics SQL HTML CSS JavaScript Python C++ Java JavaFX Swing Problem Solving English English Conversations Computer Fundamentals Learn Typing

أنواع المراجع في لغة جافا

  • مفهوم المراجع و طريقة عملها
  • الفرق بين المراجع و الأنواع البدائية
  • أنواع المراجع

لغة جافا هي لغة كائنية التوجه (Object Oriented Programming) تسمح بإنشاء الكائنات و التعامل معها و كأنها نوع بيانات (Data Type) جديد.

الكائنات في جافا يتم إنشاءها بإستخدام الكلمة المفتاحية new التي تقوم بحجز مكان لها في الذاكرة (RAM) و تحديداً في المساحة التي تسمى Heap.

البرنامج بدوره يمتلك مراجع (References) للكائنات التي يتم تخزينها في الـHeap بحيث يستطيع الوصول إليها من خلالها.

في هذا المقال سنتعرف على المراجع و أنواعها و طريقة عملها.


مفهوم المراجع و طريقة عملها

المراجع تستخدم للإشارة للكائنات الموجودة في الـ Heap و هي شبيهة بالمؤشرات (Pointers) في لغة C فهي تقوم فقط بتخزين عنوان الكائن في الذاكرة و ليس بيانات الكائن نفسه.

في المثال التالي p يعتبر مرجع للكائن Player الذي تم إنشاؤه في الذاكرة.

Player p = new Player();
Player p = new Player();

بالطبع من خلال المرجع p يمكننا التعامل مع الكائن الموجود في الذاكرة.

ملاحظة: في الشروحات نقول عادةً الكائن p بهدف أن يكون الشرح أسهل و غير معقد و لكن فعلياً p هو مجرد مرجع للكائن الموجود في الذاكرة.

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

public static void main(String[] args){
Player p = new Player("Waleed");
p.score = 0;
Player reference = p; // p يشير لنفس الكائن الذي يشير له المرجع reference هنا قمنا بجعل
p.score = 20; // p من خلال المرجع score هنا قمنا بتغيير قيمة المتغير
System.out.println(reference.score); // reference من خلال المرجع score هنا قمنا بعرض قيمة المتغير
}
public static void main(String[] args){ Player p = new Player("Waleed"); p.score = 0; Player reference = p; // p يشير لنفس الكائن الذي يشير له المرجع reference هنا قمنا بجعل p.score = 20; // p من خلال المرجع score هنا قمنا بتغيير قيمة المتغير System.out.println(reference.score); // reference من خلال المرجع score هنا قمنا بعرض قيمة المتغير }

يمكن التأكد من أن مرجعان يشيران لنفس الكائن من خلال عامل المساواة المزدوجة == كالتالي.

public static void main(String[] args) {
Player p = new Player("Waleed");
Player reference = p; // p يشير لنفس الكائن الذي يشير له المرجع reference هنا قمنا بجعل
System.out.println(p == reference); // reference يساوي المرجع الذي يشير له p لأن المرجع الذي يشير له true هنا سيتم طباعة
}
public static void main(String[] args) { Player p = new Player("Waleed"); Player reference = p; // p يشير لنفس الكائن الذي يشير له المرجع reference هنا قمنا بجعل System.out.println(p == reference); // reference يساوي المرجع الذي يشير له p لأن المرجع الذي يشير له true هنا سيتم طباعة }


الفرق بين المراجع و الأنواع البدائية

بعد أن تحدثنا عن مراجع الكائنات لا بد من التحدث عن الفرق بينها وبين أنواع البيانات البدائية (Primitive Types).

بدايةً أنواع البيانات البدائية ليست كائنات – و لذلك لا تُعد لغة جافا كائنية بالكامل – حيث أنه عند تخزين قيمة بدائية في متغير سيقوم المتغير بحفظ قيمتها مباشرةً, و لا يمكن تخزين القيمة null في متغير.

المصفوفات في لغة جافا بشكل عام تعتبر كائنات حتى لو كانت تخزن بيانات بدائية.


أنواع المراجع

1- المراجع القوية (Strong References)

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

تعد قوية لأن جامع القمامة لا يمكنه إزالة الكائن من الـHeap إن كان هناك أي مرجع قوي يشير له, فالمرجع القوي يعني أن الكائن مهم جداً حتى يعمل البرنامج بالشكل الصحيح.

منذ قليل عرفنا كيف يمكننا تخزين مراجع للكائنات و لكن كيف نزيل هذه المراجع؟

  • عند إسناد null لمرجع سيتم مسح عنوان الكائن الذي يشير إليه.
  • عن تعريف المرجع الذي يشير لكائن كمرجع محلي (Local), أي عند تعريفه بداخل بلوك (أي الكود القابل للتنفيذ و المحصور بين القوسين {} مثل الدوال و جمل if و for و while إلخ, و لا يعتبر الكلاس بلوك) لأنه بمجرد إنتهاء الأوامر الموجودة في البلوك سيتم حذفه بشكل تلقائي.
  • في حال كان المرجع الخاص بالكائن موضوع بداخل كائن آخر كحقل (Field أو Attribute) و قام جامع القمامة بإزالة الكائن الأساسي من الذاكرة سيتم إزالة المرجع معه أيضاً.

إذا بعد إزالة جميع المراجع القوية للكائن سيتم إزالة ذلك الكائن من الـHeap, و لكن هناك حالة شاذة لهذه القاعدة و هي أن يكون هناك كائنان (أو أكثر) لهما حقلان يحويان على مرجعين لبعضهما, فهنا إن لم يعد البرنامج يملك مرجعاً لأي من هذين الكائنين سيتم إزالتهما معاً, و تسمى هذه الحالة Circular Reference.

مثال يوضح كيف تنطبق الحالة على الكائنين x و y:

class X {
Y y;
}
class Y {
X x;
}
public class Test{
public static void main(String[] args) {
Y y = new Y();
X x = new X();
y.x = x;
x.y = y;
y = null;
x = null;
System.out.println("Now we don't have any reference to x and y and they could be removed");
}
}
class X { Y y; } class Y { X x; } public class Test{ public static void main(String[] args) { Y y = new Y(); X x = new X(); y.x = x; x.y = y; y = null; x = null; System.out.println("Now we don't have any reference to x and y and they could be removed"); } }


2- المراجع اللينة (Soft References)

تستخدم هذه المراجع للإشارة للكائنات بليونة, و يعني ذلك أن جامع القمامة قد يزيل الكائن (إن لم يكن هناك أي مرجع قوي يشير له) حتى و إن كان يوجد مرجع ليّن يشير له.

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

يتم إنشاء مرجع ليّن من خلال الكلاس SoftReference و تمرير الكائن الذي سيشير إليه للكونستركتور الخاص به, و عندها يمكننا إستدعاء الكائن من خلال الدالة get(), و في حال قرر جامع القمامة أنه يجب إزالة الكائن من الذاكرة سيقوم بتنفيذ الدالة finalize() على الكائن ثم يزيل المرجع الليّن (و سترجع الدالة get() عند إستدعائها null) ثم سيزيل الكائن من الذاكرة, و لإزالة المرجع يمكنك إستدعاء الدالة clear().

SoftReference<Student> soft = new SoftReference<>(new Student("Rami"));
Student rami = soft.get();
SoftReference<Student> soft = new SoftReference<>(new Student("Rami")); Student rami = soft.get();

إن كان لديك مجموعة من المراجع الليّنة و أردت معرفة الوقت الذي يقرر فيه جامع القمامة إزالة الكائن الذي يشير إليه أحد المراجع فيمكنك إستخدام طابور المراجع ReferenceQueue, و بعد تمرير كائن الطابور لكونستركتور الـ SoftReference يمكنك تنفيذ الدالة remove() و التي ستنتظر حتى يتم إزالة أي من المراجع لتعيده, وهذا مثال يوضح ذلك:

public class Test {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue queue = new ReferenceQueue();
SoftReference reference = new SoftReference(new Test(), queue);
queue.remove();
System.out.println("now Test object is removed from the heap");
}
}
public class Test { public static void main(String[] args) throws InterruptedException { ReferenceQueue queue = new ReferenceQueue(); SoftReference reference = new SoftReference(new Test(), queue); queue.remove(); System.out.println("now Test object is removed from the heap"); } }

قد تسأل عن فائدة كائن يمكن أن يختفي فجأة أثناء تنفيذ برنامجك!

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


3- المراجع الضعيفة (Weak References)

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

 

4- المراجع الوهمية (Phantom References)

المراجع الوهمية تستخدم فقط لمعرفة متى سيتم إزالة الكائن, و لا يمكن الوصول من خلالها للكائن فالدالة get() الخاصة بالكلاس PhantomReference ترجع دائماً null عند إستدعائها.

لذلك يجب تمرير طابور المراجع كمعامل و إلا فلا فائدة من المراجع الوهمية.

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

public class Test {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue queue = new ReferenceQueue();
PhantomReference reference = new PhantomReference(new Test(), queue);
Reference clearedReference = queue.remove();
if(reference == clearedReference){
System.out.println("we can write finalizing code here for Test object");
}
}
}
public class Test { public static void main(String[] args) throws InterruptedException { ReferenceQueue queue = new ReferenceQueue(); PhantomReference reference = new PhantomReference(new Test(), queue); Reference clearedReference = queue.remove(); if(reference == clearedReference){ System.out.println("we can write finalizing code here for Test object"); } } }

مشكلة كتابة الكود داخل الدالة finalize() هي أنه يمكن للمبرمج إنقاذ الكائن من الإزالة أثناء تنفيذ الدالة!

يتم ذلك عبر عمل مرجع جديد لنفس الكائن, و هذا قد يكون مقصوداً في حالات نادرة إلا أن فعل ذلك عن غير قصد سيتسبب بتسرب الذاكرة (Memory Leak) و قد ينتهي الأمر بإظهار خطأ OutOfMemoryError.

آخر تحديث في 06-01-2024

الكاتب

رامي عبدالله

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

تعليقات 1

أضف تعليق

يجب تسجيل الدخول حتى تتمكن من إضافة تعليق أو رد.

الدورات

أدوات مساعدة

أقسام الموقع

دورات
مقالات كتب مشاريع أسئلة