🌐 English

Static والمحددات ومصفوفات الكائنات

البرمجة كائنية التوجه — الجزء الثالث
CPCS 203  |  البرمجة ٢  |  الوحدة ٠٤
📋 ما سنتناوله في هذا الدرس
  1. تمرير الكائنات إلى الدوال
  2. الحالة (instance) مقابل static — القصة الكاملة
  3. لماذا نحتاج static؟ الدوافع والأسباب
  4. الحزم ومحددات الوصول
  5. التغليف: الحقول الخاصة والجاليات والمُعيِّنات
  6. مصفوفات الكائنات
1
تمرير الكائنات إلى الدوال

لنبدأ باستكمال رحلتنا في عالم البرمجة كائنية التوجه. في الدروس السابقة تعلّمنا كيف نعرّف الكلاس، وننشئ الكائنات، ونكتب المُنشئات. والآن، ماذا لو أردنا تمرير كائن كمعامل إلى دالة؟ الأمر بسيط بشكل مدهش — وفي غاية القوة.

فكّر في الأمر هكذا: تمامًا كما يمكنك تمرير int أو double إلى دالة، يمكنك تمرير كائن كامل. وستعمل الدالة مع ذلك الكائن عن طريق مرجع (reference).

Circle.java
public class Circle { private double radius; private String color; public Circle(double radius, String color) { this.radius = radius; this.color = color; } public double getArea() { return Math.PI * radius * radius; } public String toString() { return "Circle[radius=" + radius + ", color=" + color + "]"; } }
Main.java — تمرير كائن Circle إلى دالة
public class Main { // هذه الدالة تَستقبل كائن Circle كمعامل public static void printCircleInfo(Circle c) { System.out.println("Circle info : " + c); System.out.println("Area : " + c.getArea()); } public static void main(String[] args) { Circle myCircle = new Circle(5.0, "red"); // تمرير الكائن — تمامًا كأي متغير آخر printCircleInfo(myCircle); } }
💡 مفهوم أساسي — التمرير بقيمة المرجع
في جافا، عندما تمرر كائنًا إلى دالة، فأنت في الحقيقة تمرّر المرجع (عنوان الكائن في الذاكرة). كلٌّ من المُستدعي والدالة ينظران إلى نفس الكائن في الذاكرة. أي تعديلات تُجرى على حقول الكائن داخل الدالة ستكون مرئية خارجها أيضًا.
📌 لماذا هذا مفيد؟
  • يمكنك كتابة دوال مساعدة تعمل على أي كائن من كلاس معيّن.
  • يمكنك مقارنة كائنين داخل دالة (مثلاً: compareAreas(Circle c1, Circle c2)).
  • يمكن للدوال تعديل حالة الكائن الممرَّر — مما يمنحك مرونة هائلة.
2
الحالة (instance) مقابل static — القصة الكاملة

2.1 — ماذا يعني مصطلح «الحالة (instance)»؟

لنبدأ بشيء تعرفه بالفعل. لقد كتبت من قبل كودًا مثل هذا:

Circle c1 = new Circle(5.0, "red");

ربما قلت: «لقد أنشأت كائنًا من كلاس Circle.» هذا صحيح. لكن إليك النقطة المهمة — يمكنك قول الأمر بطريقة مختلفة:

«لقد أنشأت حالةً (instance) من Circle.»

الجملتان تعنيان تمامًا نفس الشيء. كلمة instance تؤكّد فقط أن هذا الكائن بالذات وُلد من كلاس Circle. ويمكننا إنشاء حالات متعددة من نفس الكلاس — كل واحدة مستقلة، ولها بياناتها الخاصة.

📖 التعريف — الحالة (instance)
الحالة (instance) هي كائن محدد وملموس يُنشأ من كلاس. في كل مرة تستخدم فيها الكلمة المفتاحية new، تُنشئ حالة جديدة. كل حالة تعيش في منطقة خاصة بها في الذاكرة (الـ Heap).

2.2 — متغيرات الحالة (instance) ودوالها

انظر إلى كلاس Circle. يحتوي على radius وcolor. تخيّل الآن أننا أنشأنا دائرتين:

Circle c1 = new Circle(5.0, "red"); Circle c2 = new Circle(10.0, "blue");
تصوير الذاكرة — حالتان مستقلتان
Stack  →  Heap
c1
Circle Object #1
radius5.0
color"red"
c2
Circle Object #2
radius10.0
color"blue"
📌 متغير الحالة (Instance Variable)
radius وcolor هما متغيرا حالة لأن كل حالة (c1, c2) لها حالة خاصة بها من هذه القيم. تغيير نصف قطر c1 لا يؤثر على نصف قطر c2.
📌 دالة الحالة (Instance Method)
دالة مثل getArea() هي دالة حالة لأنها تنتمي إلى حالة محددة — إنها تعمل على بيانات تلك الحالة (instance) (this.radius). يجب استدعاؤها على كائن: c1.getArea().

2.3 — المتغيرات الساكنة (Static): مشتركة بين جميع الحالات

والآن السؤال الكبير: ماذا لو أردنا متغيرًا لا يملكه أي كائن بمفرده، بل يكون مشتركًا بين جميع الحالات؟ على سبيل المثال، لنقل إننا نريد حساب كم كائن Circle تم إنشاؤه حتى الآن. أين نضع هذا العدّاد؟

لو جعلنا counter متغير حالة عادية، فكل دائرة ستمتلك عدّادها الخاص — وسيكون دائمًا 1. هذا لا فائدة منه. نحتاج متغيرًا تنظر إليه جميع الدوائر معًا.

هذا بالضبط ما تفعله الكلمة المفتاحية static.

Circle.java — إضافة عدّاد static
public class Circle { // متغيرات الحالة (instance) — لكل كائن حالته الخاصة private double radius; private String color; // متغير static — مشترك بين جميع حالات Circle private static int counter = 0; public Circle(double radius, String color) { this.radius = radius; this.color = color; counter++; // كل Circle جديدة تزيد العدّاد المشترك } public static int getCounter() { return counter; } }
Main.java — إنشاء ثلاثة كائنات Circle
// ننشئ الآن ثلاثة كائنات من نوع Circle Circle c1 = new Circle(5, "Red"); // Object 1 — radius: 5, color: Red Circle c2 = new Circle(10, "Blue"); // Object 2 — radius: 10, color: Blue Circle c3 = new Circle(7, "Green"); // Object 3 — radius: 7, color: Green // بعد تشغيل كل مُنشئ، يزيد العدّاد الساكن. // عند إنشاء c3، يساوي العدّاد 3 — مشترك بين الثلاثة. System.out.println(Circle.getCounter()); // 3

لقد أنشأنا الآن ثلاثة كائنات من نوع Circle. كل كائن له radius وcolor خاصان به — فهما متغيرا حالة، خاصان بكل كائن. أما counter فهو متغير static: لا ينتمي لأي كائن بعينه. في كل مرة تُنشأ دائرة جديدة، تنظر الثلاثة إلى نفس قيمة العدّاد.

التصوير التالي يُظهر بالضبط كيف يبدو هذا في الذاكرة — كيف أن c1 وc2 وc3 يشيرون جميعهم إلى نفس counter الساكن.

تصوير الذاكرة — static مقابل حالة
Static vs Instance visualization — C1, C2, C3 each hold a counter field that points to one shared static Counter = 3
كيف يتغير العدّاد مع إنشاء الكائنات:
قبل أي كائن
0
لم تُنشأ أي حالة بعد
بعد new Circle(...) الأولى
1
الحالة (instance) الأولى أُنشئت
بعد new Circle(...) الثانية
2
العدّاد مشترك — حالة واحدة فقط
بعد new Circle(...) الثالثة
3
جميع الحالات ترى نفس الرقم 3
⭐ القاعدة الذهبية — Static مقابل الحالة (instance)
  • متغير/دالة الحالة (instance) — ينتمي لكائن محدد. كل كائن له حالته الخاصة. يُستخدم عبر: objectName.field
  • المتغير/الدالة الساكنة (Static) — ينتمي للـكلاس نفسه. مشترك بين جميع الكائنات. يُستخدم عبر: ClassName.field (مثل: Circle.getCounter())
مخطط UML — الخط تحت العنصر يعني أنه static:
Circle
– radius : double         (instance)
– color : String          (instance)
counter : int          (static)
+ getArea() : double  (instance)
+ getCounter() : int  (static)
3
لماذا نحتاج static؟ الدوافع والأسباب

ربما يرفع أحد الطلاب يده ويقول: «أستاذ، أنا أفهم ما هو static — لكن لماذا نحتاجه؟ أليس بإمكاننا فعل كل شيء بمتغيرات الحالة (instance) العادية؟»

سؤال ممتاز. دعنا نتأمّل سيناريوهين حقيقيين يجعلان static ضرورةً لا غنى عنها.

السيناريو الأول — معرّفات طلاب عشوائية فريدة (لماذا نحتاج متغير static)

🎓 المشكلة
تخيّل أن لدينا كلاس Student بحقول: studentID وname وage.

الشرط هو: يجب أن يحصل كل طالب تلقائيًا على معرّف عشوائي فريد من النطاق 1 إلى 2000. المعرّفات ليست تسلسلية — إنها عشوائية. قد يحصل علي على 5، وهناء على 100، وعمر على 800. لكن لا يجوز لطالبَين أن يشتركا في نفس المعرّف.

فكّر جيدًا — كيف نضمن التفرّد؟ كل حالة مستقلة تمامًا. عند إنشاء كائن علي، لا يعلم بالمعرّفات التي أخذتها حالات هناء وعمر. هذا هو التحدي الحقيقي.

الخطوة ١ — فكرة الطالب الأولى (ولماذا تفشل)

يرفع أحد الطلاب يده ويقول: «أستاذ، يمكن لكلاس Student توليد المعرّف العشوائي بنفسه داخل المُنشئ. لكن سأحتفظ بمصفوفة متابعة في main. بعد إنشاء الطالب، أتحقق من معرّفه — إن كان مأخوذًا، أتجاهل الكائن وأنشئ آخر. أكرر حتى أحصل على معرّف فريد.»

ابتسم الأستاذ. «تفكير جيد — لكن دعنا نرى كيف يبدو ذلك في الكود.»

Student.java — الكلاس يولّد معرّفًا عشوائيًا (بدون ضمان التفرّد)
public class Student { private String name; private int age; private int studentID; public Student(String name, int age) { this.name = name; this.age = age; // يولّد معرّفًا عشوائيًا — لكن لا يضمن التفرّد this.studentID = new Random().nextInt(2000) + 1; } public int getStudentID() { return studentID; } public String toString() { return "Student[ID=" + studentID + "] " + name; } }
Main.java — المستدعي يتحقق من التكرار، يتجاهل الكائنات ويعيد الإنشاء (تصميم سيء)
public static void main(String[] args) { // يجب على المستدعي الاحتفاظ بهذه المصفوفة لتتبع المعرّفات boolean[] usedIDs = new boolean[2001]; // إنشاء علي — نكرر التجاهل والإنشاء حتى يكون المعرّف فريدًا Student s1; do { s1 = new Student("Ali", 20); // إنشاء كائن } while (usedIDs[s1.getStudentID()]); // إذا كان المعرّف مأخوذًا → تجاهل، أعد المحاولة usedIDs[s1.getStudentID()] = true; // سجّل المعرّف كمستخدم // نفس الإجراء لكل طالب جديد... Student s2; do { s2 = new Student("Hana", 21); } while (usedIDs[s2.getStudentID()]); usedIDs[s2.getStudentID()] = true; Student s3; do { s3 = new Student("Omar", 19); } while (usedIDs[s3.getStudentID()]); usedIDs[s3.getStudentID()] = true; }
❌ لماذا هذا الأسلوب خاطئ — يخرق مبدأ التجريد
تخيّل مطوّرًا يريد استخدام كلاس Student. سيصاب بالجنون. في كل مرة يُنشئ طالبًا، يُجبَر على الاحتفاظ بقائمة، وكتابة حلقة، وإدارة التحقق من المعرّفات بنفسه. هذه ليست مهمته — هذه مهمة الكلاس. التجريد مكسور.

يقول الأستاذ: «وجدت الفكرة الصحيحة — مصفوفة المتابعة. لكنها تنتمي داخل الكلاس، لا في main. اجعلها static، وسيتولى الكلاس كل شيء بنفسه.»

الخطوة ٢ — الحل الحقيقي: مصفوفة static داخل الكلاس

الإدراك هنا: نحتاج مخزنًا مشتركًا للمعرّفات المستخدمة يمكن لكل حالة من Student الرجوع إليه وتحديثه. يجب أن يكون هذا المخزن مشتركًا — لا مضاعفًا لكل كائن.

نُعلن عن مصفوفة static boolean[] داخل كلاس Student. لأنها static، فهناك حالة واحدة بالضبط بغض النظر عن عدد كائنات Student الموجودة. كل حالة تنظر إلى نفس المصفوفة.

الخوارزمية داخل المُنشئ هي:

📋 خوارزمية توليد المعرّف (تعمل داخل المُنشئ)
1
توليد رقم عشوائي بين 1 و2000.
2
البحث عن هذا الرقم في مصفوفة usedIDs الساكنة. هل usedIDs[randomNumber] مُسجّل بالفعل كـ true؟
3
إذا كان نعم (مستخدم) — عُد إلى الخطوة 1 وولّد رقمًا آخر.
4
إذا كان لا (متاح) — سجّل usedIDs[randomNumber] = true واجعل هذا الرقم معرّف الطالب.
تصوير — مصفوفة usedIDs الساكنة بعد إنشاء ٣ طلاب:
المنطقة الساكنة — حالة واحدة مشتركة، مرئية لجميع حالات Student
usedIDs[ ]
[1]
false
[2]
false
[3]
false
[4]
false
[5] ← علي
true
···
[100] ← هناء
true
···
[800] ← عمر
true
···
[2000]
false
■ true = هذا المعرّف مأخوذ بحالة  |  ■ false = هذا المعرّف لا يزال متاحًا
Student.java — معرّفات عشوائية فريدة ذاتية بمصفوفة static
import java.util.Random; public class Student { // ── حقول الحالة (instance) — كل كائن يمتلك حالته الخاصة ────────── private String name; private int age; private int studentID; // ── الحقول الساكنة — حالة واحدة مشتركة للكلاس كله ────── private static boolean[] usedIDs = new boolean[2001]; // index 1..2000 private static Random rand = new Random(); // ── المُنشئ ─────────────────────────────────────────────── public Student(String name, int age) { this.name = name; this.age = age; this.studentID = generateUniqueID(); // تتم المعالجة داخليًا } // ── مساعد static خاص — يولّد معرّفًا عشوائيًا فريدًا ────── private static int generateUniqueID() { int id; do { id = rand.nextInt(2000) + 1; // رقم عشوائي: 1 – 2000 } while (usedIDs[id]); // إذا كان مأخوذًا، أعد المحاولة usedIDs[id] = true; // سجّله كمأخوذ return id; } public String toString() { return "Student[ID=" + studentID + "] " + name + ", age=" + age; } } // ── في main — المستدعي لا يفعل شيئًا خاصًا ───────────────── Student s1 = new Student("Ali", 20); // قد يحصل على المعرّف 5 Student s2 = new Student("Hana", 21); // قد يحصل على المعرّف 100 Student s3 = new Student("Omar", 19); // قد يحصل على المعرّف 800 System.out.println(s1); // Student[ID=5] Ali, age=20 System.out.println(s2); // Student[ID=100] Hana, age=21 System.out.println(s3); // Student[ID=800] Omar, age=19 // المعرّفات مضمونة التفرّد — الكلاس يتولى كل شيء بنفسه
💡 لماذا يعمل هذا — المصفوفة الساكنة هي السر
  • usedIDs هي static — توجد مصفوفة واحدة بالضبط، مشتركة بين كل حالة Student أُنشئت.
  • عندما تُسجّل s1 القيمة usedIDs[5] = true، يظهر هذا التغيير فورًا لـ s2 وs3. ولن يختارا الرقم 5 أبدًا.
  • المستدعي يكتب فقط new Student("Ali", 20). تعقيد المعرّف مخفي تمامًا داخل الكلاس. هذا هو التغليف والتجريد يعملان معًا.
  • generateUniqueID() هي أيضًا static لأنها تتعامل فقط مع الحقول الساكنة — لا تحتاج أي بيانات من حالة معينة.

السيناريو الثاني — دوال المساعدة (لماذا نحتاج دالة static)

الآن دعنا نفكّر في الدوال الساكنة (static methods). متى يجب أن تكون الدالة static؟

يجب أن تكون الدالة static عندما لا تحتاج الوصول إلى أي متغير حالة. إنها تؤدي عملية عامة لا تعتمد على حالة أي كائن محدد.

🧮 السيناريو — دوال رياضية مساعدة
فكّر في Math.sqrt() وMath.pow() وMath.abs(). استخدمتها طوال الفصل الدراسي. لاحظ أنك لم تكتب new Math() أبدًا. لماذا؟ لأنها دوال static — تعيش على الكلاس نفسه، لا على أي كائن.

مثال آخر حقيقي: لنقل إننا نكتب كلاس Circle ونريد دالة تأخذ قيمتَي نصف قطر وتخبرنا أيهما ينتج مساحة أكبر. هذه الدالة لا تحتاج حالة محددة — إنها مجرد حساب. جعلها static هو الاختيار الطبيعي والصحيح.
Circle.java — دالة static مساعدة
public class Circle { private double radius; public Circle(double radius) { this.radius = radius; } // دالة الحالة (instance) — تحتاج this.radius public double getArea() { return Math.PI * radius * radius; } // دالة static مساعدة — لا تحتاج أي بيانات حالة public static double largerArea(double r1, double r2) { double area1 = Math.PI * r1 * r1; double area2 = Math.PI * r2 * r2; return Math.max(area1, area2); } } // ── في main ──────────────────────────────────────────── // استدعاء دالة static على الكلاس — لا حاجة لكائن double bigger = Circle.largerArea(4.0, 7.0); System.out.println("Larger area: " + bigger);
⭐ متى نستخدم الدوال الساكنة؟
  • الدالة لا تستخدم أي متغير حالة (لا يوجد this.something).
  • الدالة تؤدي عملية مساعدة عامة (رياضيات، تحويل، منطق مصنع).
  • تريد أن يستخدمها المستدعون بدون إنشاء كائن أولًا.
⚠️ قيد مهم
الدالة الساكنة لا تستطيع الوصول إلى متغيرات الحالة (instance) أو استدعاء دوال الحالة (instance) مباشرةً. لماذا؟ لأنه عند استدعاء دالة static، قد لا يوجد أي كائن على الإطلاق — لذا this غير موجود داخل السياق الساكن.
4
الحزم ومحددات الوصول

4.1 — الصورة الكبيرة: المشروع ← الحزمة ← الكلاس

يُنظَّم برنامج جافا في طبقات. فكّر في الأمر كبناية:

🏗️
MyProject
التطبيق الشامل
📦
com.university.shapes
حزمة — تجمع الكلاسات المترابطة
📄
Circle.java
كلاس داخل الحزمة
📄
Rectangle.java
كلاس آخر في نفس الحزمة
📦
com.university.students
حزمة مختلفة
📄
Student.java
اهتمام مستقل تمامًا
📦 لماذا نحتاج الحزم؟
الحزم للتنظيم البحت. مع نموّ مشروعك إلى عشرات أو مئات الكلاسات، يصبح وضع كل شيء في مجلد واحد فوضى. تتيح لك الحزم تجميع الكلاسات المترابطة — مثل المجلدات على جهازك. كما تسمح لحزمتين مختلفتين بامتلاك كلاسات بنفس الاسم دون تعارض.

4.2 — محددات الوصول

السؤال الحاسم الآن: إذا كان مشروعي يحتوي كلاسات عديدة موزّعة على حزم متعددة، من يحق له رؤية واستخدام أي حقول ودوال؟ هذا يتحكم فيه محددات الوصول (Visibility Modifiers).

في جافا أربعة محددات:

🌍
public — مرئي في كل مكان
أي كلاس في أي حزمة في أي مشروع يمكنه الوصول إلى هذا العضو. هذا أعلى مستوى من الانفتاح.
public int radius; — يمكن لأي كود قراءته.
🔒
private — مرئي داخل هذا الكلاس فقط
فقط الكود المكتوب داخل نفس الكلاس يمكنه الوصول. حتى الكلاس الوارث لا يستطيع رؤيته. هذا أعلى مستوى من الحماية.
private int radius; — فقط دوال Circle تستطيع قراءته.
📦
default (بدون كلمة مفتاحية) — مرئي داخل نفس الحزمة
إذا لم تكتب أي محدد، تستخدم جافا الـ «default» أو «package-private». فقط الكلاسات في نفس الحزمة تستطيع الوصول. الكلاسات في حزم أخرى لا تستطيع.
int radius; — بدون كلمة مفتاحية يعني default.
🛡️
protected — نفس الحزمة + الكلاسات الوارثة
مثل default، لكنه يمتد أيضًا إلى الكلاسات الوارثة (الكلاسات الأبناء)، حتى لو كانت في حزمة مختلفة. سنفهم هذا تمامًا بعد دراسة الوراثة (Inheritance) — لذا سنعود إليه لاحقًا.
protected int radius;
جدول ملخّص — من يمكنه الوصول؟
المحدد نفس الكلاس نفس الحزمة كلاس وارث (حزمة مختلفة) في كل مكان
public
protected
default
private
تصوير — الوصول عبر المشروع
🏗️ Project
📦 الحزمة: com.shapes
Circle
private radius
public getArea()
default helper()
Rectangle
✔ يمكن استدعاء getArea() (public)
✔ يمكن استدعاء helper() (default — نفس الحزمة)
✗ لا يمكن الوصول إلى radius (private)
📦 الحزمة: com.students
Student
✔ يمكن استدعاء getArea() (public)
✗ لا يمكن استدعاء helper() (default — حزمة مختلفة)
✗ لا يمكن الوصول إلى radius (private)
⏳ عن protected — يأتي لاحقًا
لكي تفهم protected كاملًا، تحتاج أن تفهم الوراثة (Inheritance) (الكلاس الأب والكلاس الابن). سنعود إليه في درس الوراثة. في الوقت الحالي، فقط اعلم بوجوده وأنه يقع بين default وpublic من حيث الانفتاح.
5
التغليف: الحقول الخاصة والجاليات والمُعيِّنات

5.1 — لماذا يجب أن تكون حقول البيانات private؟

كنّا نكتب private أمام حقول البيانات. لكن لماذا؟ ما المشكلة في استخدام public؟

🔓 تخيّل حقولًا public — الخطر
لنقل إنك أعلنت: public double radius;

الآن أي كود في أي مكان في المشروع يمكنه كتابة: myCircle.radius = -999;

نصف قطر سالب لا معنى له. لكن جافا ستسمح بذلك لأنه لا توجد حماية. كائنك الآن في حالة غير صالحة ومعطوبة — ومن المستحيل تطبيق القواعد من داخل الكلاس.
✅ حقول private + وصول متحكَّم = التغليف
عندما يكون الحقل private، الطريقة الوحيدة لقراءته أو تغييره من الخارج هي الدوال التي تقدّمها أنت عن قصد. داخل تلك الدوال يمكنك التحقق من أي قيمة قبل قبولها. هذا يحمي سلامة كائنك في جميع الأوقات.
📖 التغليف (Encapsulation)
التغليف هو مبدأ الـ OOP الذي يقضي بتجميع البيانات والدوال التي تعمل عليها داخل كلاس واحد، وتقييد الوصول المباشر للبيانات. يجعل الكود أسهل للصيانة، ويمنع إساءة الاستخدام، ويسمح بتغيير التنفيذ الداخلي دون التأثير على الكود الخارجي.

5.2 — دوال الوصول (Getters) والتعيين (Setters)

عندما تجعل الحقل private، توفّر وصولًا متحكَّمًا عبر نوعين من الدوال ستسمع عنهما باستمرار في تطوير جافا الاحترافي:

📖 دالة الوصول (Accessor — Getter)
الغرض: السماح بقراءة قيمة حقل private.

اتفاقية التسمية: get + اسم الحقل

تُرجع: قيمة الحقل — بدون تعديل.

مثال: getRadius()، getColor()، getName()
✏️ دالة التعيين (Mutator — Setter)
الغرض: السماح بتغيير قيمة حقل private — مع التحقق.

اتفاقية التسمية: set + اسم الحقل

تُرجع: void. تأخذ معاملًا واحدًا.

مثال: setRadius(double r)، setColor(String c)
Circle.java — كلاس مُغلَّف بالكامل
public class Circle { // الحقول الخاصة — لا وصول خارجي مباشر private double radius; private String color; public Circle(double radius, String color) { setRadius(radius); // إعادة استخدام الـ setter حتى يعمل التحقق حتى في المُنشئ this.color = color; } // ── دالة الجالي (Getter) ───────────────────────────────── public double getRadius() { return radius; } public String getColor() { return color; } // ── دالة المُعيِّن (Setter) — مع التحقق ──────────────── public void setRadius(double radius) { if (radius < 0) { System.out.println("Error: radius cannot be negative. Keeping default."); this.radius = 1.0; // قيمة افتراضية آمنة } else { this.radius = radius; } } public void setColor(String color) { this.color = color; } public double getArea() { return Math.PI * radius * radius; } } // ── في main ──────────────────────────────────────────── Circle c = new Circle(5.0, "red"); // Reading a field — must use getter System.out.println(c.getRadius()); // ✔ 5.0 // Changing a field — must use setter (validation runs!) c.setRadius(-10); // Error message printed, radius stays 1.0 c.setRadius(8.0); // ✔ Valid — radius becomes 8.0 // Direct access is BLOCKED by the compiler: // c.radius = -999; ✗ Compile error — radius is private
💡 لماذا هذا التصميم قوي
  • سلامة البيانات: القيم غير الصالحة تُرفض قبل إفساد الكائن.
  • سهولة الصيانة: غيّر تنسيق التخزين الداخلي في أي وقت — الكود الخارجي الذي يستخدم الـ getters/setters لن يتأثر.
  • التصحيح سهل: كل قراءة/كتابة تمر من مكان واحد — يمكنك إضافة التتبع فورًا.
6
مصفوفات الكائنات

حتى الآن خزّنّا الكائنات المفردة في متغيرات مثل Student s1 وStudent s2 وStudent s3. هذا مناسب لكائنَين أو ثلاثة. لكن ماذا لو كان لدينا 100 طالب؟ كتابة 100 متغير مستقل أمر مستحيل.

الحل هو مصفوفة من الكائنات — وتعمل بشكل مشابه جدًا لمصفوفة البدائيات، مع فارق جوهري واحد يجب أن تفهمه جيدًا.

6.1 — إعلان مصفوفة الكائنات وإنشاؤها

إنشاء مصفوفة من 5 كائنات Student
// الخطوة ١: إعلان المصفوفة وإنشاؤها (الحاوية فقط، لا الكائنات) Student[] students = new Student[5]; // في هذه اللحظة، جميع الخانات الخمس تحتوي null — لم تُنشأ أي كائنات بعد! // الخطوة ٢: إنشاء كائنات Student فعلية وتعيينها في الخانات students[0] = new Student("Ali", 20); students[1] = new Student("Hana", 21); students[2] = new Student("Omar", 19); students[3] = new Student("Lina", 22); students[4] = new Student("Reem", 20); // الخطوة ٣: الوصول للكائنات عبر المصفوفة System.out.println(students[0].getName()); // Ali
🔑 مفهوم حاسم — المصفوفة تخزّن المراجع (References)، لا الكائنات

هذه واحدة من أهم النقاط التي يجب فهمها. عند إنشاء new Student[5]، تُنشئ جافا مصفوفة من 5 خانات مرجعية — لا 5 كائنات Student. الكائنات نفسها تعيش بشكل منفصل في ذاكرة الـ Heap.

المصفوفة تحتوي عناوين (مراجع) تشير إلى أماكن الكائنات. يمكن أن تكون الكائنات مبعثرة في أي مكان من الـ Heap.

تصوير الذاكرة — مصفوفة مراجع الكائنات
students[ ]
[0]ref
[1]ref
[2]ref
[3]ref
[4]ref
Student — "Ali"
name: "Ali"  |  age: 20  |  ID: 5
Student — "Hana"
name: "Hana"  |  age: 21  |  ID: 100
Student — "Omar"
name: "Omar"  |  age: 19  |  ID: 800
Student — "Lina"
name: "Lina"  |  age: 22  |  ID: 433
Student — "Reem"
name: "Reem"  |  age: 20  |  ID: 1756
💡 الكائنات ليست مُحزَّمة في المصفوفة
المصفوفة مجرد صف أنيق من العناوين. كل عنوان هو مرجع يشير إلى حيثما يعيش الكائن المقابل في الـ Heap. قد تكون كائنات Student متباعدة في الذاكرة — المصفوفة لا تنقلها معًا. إنها تحتفظ فقط بالاتجاهات للعثور عليها.

6.2 — التكرار عبر مصفوفة الكائنات

باستخدام حلقة for
// حلقة for العادية for (int i = 0; i < students.length; i++) { System.out.println(students[i].toString()); }
باستخدام حلقة for-each المحسّنة (أكثر وضوحًا)
// for-each — تُقرأ: «لكل Student s في مصفوفة students» for (Student s : students) { System.out.println(s.getName() + " — Age: " + s.getAge()); }
⚠️ انتبه دائمًا للقيمة null
تذكر: عند إنشاء new Student[5]، جميع الخانات تحتوي null افتراضيًا. إذا حاولت استدعاء دالة على خانة null، ستحصل على NullPointerException. إذا لم تملأ جميع الخانات، استخدم التحقق من null:
for (Student s : students) { if (s != null) { System.out.println(s.getName()); } }

6.3 — مثال كامل قابل للتشغيل

Main.java — مثال كامل على مصفوفة كائنات
public class Main { public static void main(String[] args) { // Create array container Student[] students = new Student[5]; // Populate with Student objects students[0] = new Student("Ali", 20); students[1] = new Student("Hana", 21); students[2] = new Student("Omar", 19); students[3] = new Student("Lina", 22); students[4] = new Student("Reem", 20); // Print all students System.out.println("=== All Students ==="); for (Student s : students) { System.out.println(s); } // Find the oldest student Student oldest = students[0]; for (int i = 1; i < students.length; i++) { if (students[i].getAge() > oldest.getAge()) { oldest = students[i]; } } System.out.println("\nOldest student: " + oldest.getName()); } }
📋 الناتج
=== All Students ===
Student[1] Ali — Age: 20
Student[2] Hana — Age: 21
Student[3] Omar — Age: 19
Student[4] Lina — Age: 22
Student[5] Reem — Age: 20

Oldest student: Lina
الخلاصة — المواضيع الستة
١. تمرير الكائنات
عند تمرير كائن إلى دالة، تُمرَّر قيمة المرجع. تُشير كلٌّ من الدالة والمُستدعي إلى نفس الكائن في ذاكرة الـ Heap.
٢. الحالة (instance) مقابل Static
متغيرات/دوال الحالة (instance) مرتبطة بكائن محدد. المتغيرات/الدوال الساكنة مرتبطة بالكلاس وتُشارَك بين جميع الحالات.
٣. دوافع Static
استخدم static عندما تحتاج بيانات مشتركة (مثل عدّاد أو مُتتبّع) أو دوال مساعدة عامة لا تحتاج حالة الكائن.
٤. الحزم
الحزم تُنظّم الكلاسات في مجموعات منطقية. المحددات (public/private/default/protected) تتحكم في مدى رؤية كل عضو.
٥. التغليف
اجعل الحقول private. وفّر getters للقراءة وsetters مع التحقق للتعديل. هذا يحمي سلامة الكائن.
٦. مصفوفات الكائنات
المصفوفات تخزّن المراجع لا الكائنات. الكائنات تعيش في الـ Heap. تحقق دائمًا من null قبل الوصول.