1 لماذا نحتاج المُنشئات؟
في الدرس السابق، رأينا كيفية إنشاء صنف Circle بسيط. لكن لإنشاء دائرة قابلة للاستخدام، كان علينا دائمًا القيام بـ خطوتين:
هذا يعمل، ولكن يبدو محرجًا بعض الشيء. ماذا لو نسينا الخطوة الثانية؟ عندها سنحصل على دائرة بنصف قطر صفر، وهذا ليس منطقيًا في الغالب.
ألن يكون من الأفضل إنشاء الكائن وتعيين نصف القطر في خطوة واحدة؟
هذا أفضل بكثير! وهذا بالضبط ما تفعله المُنشئات (constructors). المُنشئ هو طريقة خاصة تُستدعى تلقائيًا عند إنشاء كائن جديد باستخدام الكلمة المفتاحية new.
المُنشئات تسمح لنا بـ:
- التهيئة عند الإنشاء: تعيين القيم الأولية للكائن مباشرة عند إنشائه
- ضمان حالة صالحة: التأكد من أن جميع الكائنات تُنشأ في حالة صحيحة وقابلة للاستخدام
- صفات إلزامية: إجبار المستخدمين على تقديم قيم ضرورية معينة
- كائنات غير قابلة للتغيير: إنشاء كائنات لا يمكن تغييرها بعد الإنشاء
2 ما المميز في المُنشئ؟
المُنشئ يبدو مثل طريقة عادية، ولكن مع بعض القواعد الخاصة:
- الاسم: يجب أن يكون نفس اسم الصنف تمامًا (بما في ذلك الحروف الكبيرة/الصغيرة)
- لا نوع إرجاع: المُنشئ لا يحتوي على نوع إرجاع، حتى ولا void
- يُستدعى بـ new: يُستدعى تلقائيًا عند استخدام new
- الغرض: تهيئة حقول البيانات للكائن الجديد
لنكتب صنف Circle مع مُنشئ:
الآن يمكننا إنشاء دائرة في خطوة واحدة:
عند كتابة new Circle(5.0)، يحدث شيئان:
- ١. تُنشئ Java كائنًا جديدًا من نوع Circle في الذاكرة
- ٢. تستدعي Java المُنشئ Circle(double r) وتمرر 5.0 كمعامل
3 هل يمكن أن يحتوي الصنف على أكثر من مُنشئ؟
نعم! يمكن للصنف أن يحتوي على مُنشئات متعددة. هذا يسمى التحميل الزائد للمُنشئ (constructor overloading). لنفترض أننا نريد إضافة لون إلى دائرتنا:
الآن يمكننا إنشاء دوائر بطرق مختلفة:
المُنشئات المتعددة تعطي مرونة للمستخدمين. يمكنهم اختيار المُنشئ الأنسب لاحتياجاتهم:
- إذا كانوا يهتمون فقط بنصف القطر → استخدم Circle(double r)
- إذا كانوا يريدون تحديد كليهما → استخدم Circle(double r, String c)
- إذا كانوا يريدون القيم الافتراضية → استخدم Circle()
4 المُنشئ الافتراضي — مفهوم مهم جدًّا
لنعد إلى المثال الأول من الدرس السابق، عندما كتبنا صنف Circle بدون أي مُنشئ على الإطلاق:
ومع ذلك، تمكنا من كتابة:
كيف نجح هذا إذا لم نكتب مُنشئًا؟ الإجابة: إذا لم تكتب أي مُنشئ، فإن Java توفر تلقائيًا مُنشئًا افتراضيًا (default constructor) لك.
المُنشئ الافتراضي يختفي عند تعريف أي مُنشئ!
إذا كتبت أي مُنشئ (حتى مُنشئ واحد فقط)، فإن Java لن توفر لك المُنشئ الافتراضي تلقائيًا.
لنرى مثالًا:
الآن، إذا حاولنا كتابة:
إذا كنت تريد كلا المُنشئين، يجب عليك كتابتهما بشكل صريح:
- توفره Java تلقائيًا
- لا معاملات
- لا يفعل شيئًا (جسم فارغ)
- يختفي إذا عرّفت أي مُنشئ
- غير مرئي في الكود
- تكتبه أنت بنفسك
- لا معاملات
- يمكن أن يحتوي على كود تهيئة
- موجود حتى مع مُنشئات أخرى
- مرئي في الكود
5 كلمة this — "أنا أتحدث عن نفسي"
هناك مشكلة شائعة عند كتابة المُنشئات. لنلقِ نظرة على هذا الكود:
الاسم r يعمل، لكنه ليس وصفيًا. من الأفضل استخدام radius كاسم المعامل أيضًا:
هذا غامض! الآن لدينا اسمان radius:
- حقل البيانات radius (متغير الكائن)
- المعامل radius (متغير المُنشئ)
عندما تكتب radius = radius، فإن Java تعتبر كلاهما يشير إلى المعامل! حقل البيانات يكون "مخفيًا" (shadowed).
الكلمة المفتاحية this هي مرجع للكائن الحالي. يمكنك استخدامها للإشارة بوضوح إلى حقول وطرق الكائن الحالي.
الآن الكود واضح تمامًا:
- this.radius يشير إلى حقل البيانات للكائن
- radius (بدون this) يشير إلى المعامل
فكّر في this كأنه كلمة "أنا" أو "نفسي". عندما يقول الكائن this.radius، فهو يقول: "نصف قطري الخاص".
في الواقع، this هو مرجع (reference) يحمل عنوان الكائن الحالي في الذاكرة. على سبيل المثال، إذا كان الكائن موجودًا في العنوان 0x3A7F، فإن this يحتوي على 0x3A7F.
مثال شامل مع عدة حقول:
6 استخدام this() لاستدعاء مُنشئ آخر
هناك استخدام آخر لـ this: يمكنك استخدام this() لاستدعاء مُنشئ آخر من نفس الصنف. هذا مفيد لتجنب تكرار الكود.
تذكر المثال من القسم 3 حيث كان لدينا 4 مُنشئات؟ كان هناك الكثير من التكرار. لنعيد كتابته باستخدام this():
استخدام this() يتبع مبدأ "لا تكرر نفسك" (DRY). الآن، منطق التهيئة موجود في مكان واحد فقط (المُنشئ الرئيسي). إذا احتجنا لتغيير كيفية التهيئة، نغيره في مكان واحد فقط.
- يجب أن يكون أول تعليمة: استدعاء this() يجب أن يكون السطر الأول في المُنشئ
- فقط في المُنشئات: لا يمكن استخدام this() إلا داخل مُنشئ
- مرة واحدة فقط: يمكنك استدعاء مُنشئ واحد فقط (لا يمكنك كتابة this() مرتين)
- يشير إلى حقل الكائن
- يُستخدم لحل تعارض الأسماء
- يمكن استخدامه في أي مكان
- مثال: this.radius = radius;
- يستدعي مُنشئًا آخر
- يُستخدم لتجنب تكرار الكود
- فقط في المُنشئات
- يجب أن يكون أول تعليمة
- مثال: this(1.0, "red");
7 متغيرات المرجع للكائنات
الآن بعد أن فهمنا المُنشئات وكلمة this، لنلقِ نظرة فاحصة على ما يحدث فعلًا عندما نكتب سطرًا مثل هذا:
هذا السطر الواحد يفعل ثلاثة أشياء:
- Circle c1 — يُعلن عن متغير اسمه c1 من النوع Circle. هذا المتغير يُسمى متغير مرجع. لا يحمل الكائن نفسه — بل يحمل مرجعًا (مؤشرًا) إلى مكان وجود الكائن في الذاكرة.
- new Circle(5.0, "blue") — ينشئ كائنًا جديدًا من نوع Circle في الذاكرة، يستدعي المُنشئ لتهيئته بـ radius = 5.0 و color = "blue"، ويُرجع مرجعًا للكائن المُنشأ حديثًا.
- = — يُسند ذلك المرجع إلى المتغير c1. الآن c1 يشير إلى (يؤشر إلى) الكائن.
لنصور هذا في الذاكرة:
(يحمل مرجعًا للكائن)
(يحتوي على حقول البيانات والطرق)
المتغير c1 ليس هو الكائن. إنه جهاز تحكم عن بُعد يشير إلى الكائن. الكائن نفسه يعيش في مكان ما في الذاكرة، وc1 يحمل مرجعًا إليه. لهذا يُسمى متغير مرجع — لأنه يشير إلى (يؤشر إلى) الكائن.
لهذا السبب أيضًا تعمل كلمة this بهذه الطريقة: داخل الكائن، this يحمل نفس المرجع (0x3A7F)، مما يسمح للكائن بالإشارة إلى نفسه.
8 الوصول إلى أعضاء الكائن
الآن بعد أن أصبح لدينا متغير مرجع يشير إلى كائن، كيف نتفاعل معه؟ كيف نقرأ بياناته أو نستدعي طرقه؟
الإجابة هي عامل النقطة (.) — المسمى أيضًا عامل الوصول للأعضاء.
الصيغة بسيطة: referenceVariable.member
فكّر في النقطة (.) على أنها تقول "اذهب إلى الكائن والوصول إلى...". عندما تكتب c1.getRadius()، فأنت تخبر Java: "اذهب إلى الكائن الذي يشير إليه c1، واستدعِ طريقته getRadius()."
متغير المرجع هو البوابة. النقطة هي الباب. والعضو (حقل أو طريقة) هو ما تصل إليه على الجانب الآخر.
9 القيم الافتراضية لحقول البيانات
هذا أمر مهم جدًّا يجب على كل طالب معرفته: عند إنشاء كائن، يتم تهيئة جميع حقول البيانات تلقائيًا بقيم افتراضية، حتى لو لم يحددها المُنشئ بشكل صريح. Java تضمن ذلك.
| نوع البيانات | القيمة الافتراضية |
|---|---|
| int, long, short, byte | 0 |
| double, float | 0.0 |
| boolean | false |
| char | '\u0000' |
| Reference types (String, Object, etc.) | null |
لنرَ هذا عمليًا. لنأخذ صنفًا يحتوي على عدة حقول لا يُهيئها المُنشئ:
كل حقل له قيمته الافتراضية — حتى مع أننا لم نحدد أيًا منها. Java تُهيئ جميع حقول البيانات تلقائيًا.
حقول البيانات مقابل المتغيرات المحلية: فرق حاسم
الآن نصل لنقطة يحدث فيها الالتباس كثيرًا للطلاب. القاعدة أعلاه تنطبق فقط على حقول البيانات (الحقول المُعلنة داخل الصنف). لا تنطبق على المتغيرات المحلية (المتغيرات المُعلنة داخل طريقة).
Java ترفض ترجمة هذا الكود. المتغير المحلي x تم الإعلان عنه ولكن لم يُسند له قيمة أبدًا. بخلاف حقول البيانات، المتغيرات المحلية لا تحصل على قيم افتراضية. يجب عليك تهيئتها بنفسك قبل استخدامها.
- تُعلن داخل الصنف، خارج أي طريقة
- تُهيأ تلقائيًا بـ قيم افتراضية
- يمكن استخدامها مباشرة دون تهيئة يدوية
- مثال: private int age; ← افتراضيًا 0
- تُعلن داخل طريقة
- لا قيمة افتراضية — غير مُهيأة
- يجب تهيئتها قبل الاستخدام، وإلا لن يُترجَم الكود
- مثال: int x; ← خطأ في الترجمة إذا استُخدم قبل الإسناد
10 الأنواع البدائية مقابل الأنواع المرجعية
الآن لنفهم فرقًا مهمًا جدًّا في كيفية تعامل Java مع المتغيرات البدائية (مثل int، double) مقابل متغيرات المرجع (مثل Circle، String). هذا الفرق يؤثر على ما يحدث عند إسناد متغير إلى آخر.
المتغيرات البدائية: القيم يتم نسخها
مع الأنواع البدائية، كل متغير يحمل قيمته الخاصة المستقلة. عندما تُسند أحدهما للآخر، القيمة يتم نسخها:
كل متغير له صندوقه الخاص بقيمته الخاصة. تغيير أحدهما لا يؤثر على الآخر.
متغيرات المرجع: المراجع يتم نسخها
مع الأنواع المرجعية، السلوك مختلف جذريًا. المتغير لا يحمل الكائن — بل يحمل مرجعًا للكائن. عندما تُسند متغير مرجع إلى آخر، فأنت تنسخ المرجع، وليس الكائن نفسه.
الآن راقب ما يحدث عندما نُسند c1 إلى c2:
بعد c2 = c1، المتغير c2 لم يعد يشير إلى الدائرة ذات radius = 10.0. بدلاً من ذلك، كلا c1 و c2 يشيران الآن إلى نفس الكائن (الدائرة عند 0x1A2B مع radius = 5.0).
هذا يعني إذا عدّلنا الكائن عبر c2، فإن التغيير سيكون مرئيًا عبر c1 أيضًا — لأنهما يشيران إلى نفس الكائن تمامًا:
جامع النفايات: ماذا يحدث للكائن المهجور؟
لكن ماذا عن الدائرة الأصلية التي كان c2 يشير إليها — تلك التي مع radius = 10.0 و color = "red"؟ لا تزال موجودة في الذاكرة، تشغل مساحة، لكن لا يوجد متغير يشير إليها. ليس لدينا طريقة للوصول إليها بعد الآن.
Java لديها نظام مدمج يُسمى جامع النفايات (Garbage Collector) يفحص الذاكرة بشكل دوري بحثًا عن كائنات ليس لها متغير مرجع يشير إليها. عندما يجد مثل هذا الكائن اليتيم، يقوم تلقائيًا بتحرير الذاكرة التي يشغلها حتى يمكن إعادة استخدام الذاكرة.
هذا يحدث تلقائيًا في الخلفية — المبرمج لا يحتاج للقلق بشأنه، ولا يحتاج لحذف الكائنات يدويًا، ولا يحتاج لكتابة أي كود تنظيف. جامع النفايات يعتني بكل شيء. هذه واحدة من المزايا العظيمة لـ Java مقارنة بلغات مثل C و C++، حيث المبرمج مسؤول عن تحرير الذاكرة يدويًا.
المتغيرات البدائية تحمل قيمًا فعلية. إسناد إحداها للأخرى ينسخ القيمة. كل متغير مستقل.
متغيرات المرجع تحمل مراجع للكائنات. إسناد إحداها للأخرى ينسخ المرجع. كلا المتغيرين يشيران إلى نفس الكائن. الكائن المهجور يتم تنظيفه بواسطة جامع النفايات.
11 أصناف مدمجة مفيدة: Date و Random
Java تأتي مع مئات الأصناف الجاهزة في مكتبتها القياسية. لا أحد يُتوقع منه حفظها جميعًا — لكن كمبرمج Java، من مسؤوليتك استكشاف وتعلم تلك التي تحتاجها. توثيق Java الرسمي (Javadoc) هو دليلك.
هنا، سنقدم بإيجاز صنفين شائعي الاستخدام لنريك كم هو سهل استخدام الأصناف الموجودة بمجرد فهمك للكائنات.
صنف Date
صنف java.util.Date يمثل نقطة زمنية محددة. إنشاء كائن Date بدون معاملات يعطيك التاريخ والوقت الحاليين:
صنف Date هو واحد من أقدم الأصناف في Java. للعمل الأكثر تقدمًا مع التاريخ/الوقت، توفر Java أصنافًا أحدث مثل LocalDate، LocalTime، و LocalDateTime في حزمة java.time. لكن Date نقطة انطلاق جيدة لفهم كيفية عمل الكائنات.
صنف Random
صنف java.util.Random يُولّد أرقامًا عشوائية زائفة. لنبدأ بمثال بسيط:
في كل مرة تشغل فيها هذا البرنامج، تحصل على أرقام مختلفة. لكن الآن، لنجرب شيئًا مثيرًا...
البذرة: لماذا يمكن لبرنامجين إنتاج نفس الأرقام "العشوائية"
إليك سؤال يفاجئ العديد من المبتدئين: هل هذه الأرقام عشوائية حقًا؟ الإجابة هي لا. تُسمى أرقامًا عشوائية زائفة — يتم توليدها بواسطة معادلة رياضية، وليس بواسطة عشوائية حقيقية.
المعادلة تعمل هكذا: تبدأ برقم أولي يُسمى بذرة (seed). من تلك البذرة، تطبق حسابًا رياضيًا لإنتاج الرقم "العشوائي" الأول. ثم تغذي تلك النتيجة مرة أخرى في المعادلة لإنتاج الرقم الثاني، وهكذا. كل رقم يُحدد بواسطة الرقم السابق، مكونًا سلسلة.
فكر فيها هكذا:
seed → formula → number₁ → formula → number₂ → formula → number₃ → ...
الفكرة الأساسية هي: إذا بدأ كائنا Random بـ نفس البذرة، فسينتجان نفس التسلسل تمامًا من الأرقام. كل مرة. لأن المعادلة نفسها، ونقطة البداية نفسها.
لنثبت ذلك:
كلا rand1 و rand2 تم إنشاؤهما بـ نفس البذرة (42). بما أن المعادلة الرياضية نفسها ونقطة البداية نفسها، فإنهما ينتجان نفس التسلسل تمامًا من الأرقام: 0، 68، 54، ... كل مرة، على كل جهاز، إلى الأبد.
شغّل هذا البرنامج 100 مرة — ستحصل دائمًا على نفس الناتج. الأرقام حتمية، وليست عشوائية.
الآن قارن هذا مع استخدام بدون بذرة:
إذا أردت نتائج مختلفة في كل مرة (ألعاب، محاكاة)، استخدم new Random() بدون بذرة — Java ستستخدم تلقائيًا الوقت الحالي كبذرة، والذي يختلف في كل تشغيل.
إذا أردت نتائج قابلة لإعادة الإنتاج (اختبار، تصحيح أخطاء، تجارب علمية)، استخدم new Random(seed) مع بذرة محددة — هذا يضمن نفس التسلسل في كل مرة، مما يجعل نتائجك متوقعة وقابلة للتحقق.
12 الملخص
- المُنشئات تُهيئ الكائنات في لحظة إنشائها — تعطي الحياة للكائنات.
- المُنشئ يجب أن يكون له نفس اسم الصنف وليس له نوع إرجاع.
- الصنف يمكن أن يحتوي على مُنشئات متعددة (التحميل الزائد للمُنشئ).
- كل صنف في Java له مُنشئ واحد على الأقل — المُنشئ الافتراضي الضمني يختفي إذا عُرّف أي مُنشئ.
- this.field يشير إلى حقل البيانات للكائن الحالي، يحل تعارض الأسماء مع المتغيرات المحلية.
- this(args) يستدعي مُنشئًا آخر في نفس الصنف (سلسلة المُنشئات).
- متغير المرجع يحمل مرجعًا لكائن، وليس الكائن نفسه.
- عامل النقطة (.) يُستخدم للوصول إلى حقول وطرق الكائن.
- حقول البيانات تحصل على قيم افتراضية تلقائيًا؛ المتغيرات المحلية لا.
- إسناد مرجع لآخر ينسخ المرجع — كلاهما يشير إلى نفس الكائن. الكائن المهجور يتم تنظيفه بواسطة جامع النفايات.
- صنف Random يولد أرقامًا عشوائية زائفة باستخدام معادلة رياضية. نفس البذرة = نفس التسلسل.
في الدرس القادم، سنستكشف الأعضاء النسخية مقابل الأعضاء الساكنة (instance vs. static)، وكيفية العمل مع مصفوفات من الكائنات. سنستكشف أيضًا تصاميم أصناف أكثر تعقيدًا مع كائنات متعددة متفاعلة.