English 🇬🇧
CPCS203  ·  الوحدة الخامسة  ·  الجزء 1 من 3

🔒 الكائنات غير القابلة للتغيير

البرمجة كائنية التوجه — الوحدة الخامسة، الجزء الأول

🔑 مراجعة التغليف (Encapsulation) 🔄 الكائنات القابلة للتغيير (Mutable) 🔒 الكائنات غير القابلة للتغيير (Immutable) 📋 3 قواعد للثبات (Immutability) 🛡️ النسخة الواقية (Defensive Copy)
📋 فهرس المحتويات
  1. الركيزتان الأساسيتان: التغليف (Encapsulation) والتجريد (Abstraction) — مراجعة
  2. الكائنات القابلة والغير القابلة للتغيير — نقاش داخل الفصل خطوة بخطوة
  3. القواعد الثلاث لبناء صنف غير قابل للتغيير
1
الركيزتان الأساسيتان: التغليف (Encapsulation) والتجريد (Abstraction)

أهلاً بكم يا طلابي الأعزاء! لقد أنهينا للتو دراسة الأصناف (Classes) والكائنات (Objects). الآن تغيّر تفكيرنا — انتقلنا من التفكير الإجرائي (Procedural Thinking) إلى التفكير كائني التوجه (Object-Oriented Thinking). أصبحنا نستطيع التعرف على الكائنات (Objects)، وخصائصها (الحقول — Fields)، وسلوكها (الدوال — Methods)، والتعبير عنها بمخططات UML، وترجمة تلك المخططات إلى كود Java.

قبل أن نبدأ المادة الجديدة، لنسترجع سريعاً الركيزتين الأساسيتين في البرمجة كائنية التوجه اللتين تعرفنا عليهما من قبل.

🔒
التغليف (Encapsulation)
إخفاء الحالة الداخلية للكائن (Object) عن العالم الخارجي. تُبقى جميع الحقول (Fields) private، ولا يتعامل الخارج معها إلا من خلال دوال محددة public — دوال الإرجاع (Getters) ودوال التعيين (Setters).

تخيّلها كالكبسولة الدوائية — المحتوى مخفي في الداخل؛ تتعامل فقط مع الغلاف الخارجي.
🧩
التجريد (Abstraction)
الفصل بين التطبيق والاستخدام. المستخدم لا يحتاج أن يعرف كيف يعمل الصنف داخلياً — بل يهمه فقط ماذا يفعل. الواجهة العامة (توقيعات الدوال) هي ما يهم.

كقيادة السيارة: تستخدم المقود والدواسات دون معرفة تفاصيل المحرك الداخلية.
📚 للمزيد من القراءة
يرجى مراجعة أقسام التغليف (Encapsulation) والتجريد (Abstraction) في الوحدة الرابعة (الكائنات والأصناف) للحصول على شرح أعمق وأمثلة تطبيقية.
2
الكائنات القابلة للتغيير (Mutable) مقابل غير القابلة للتغيير (Immutable)

كل كائن (Object) ننشئه إما قابل للتغيير (Mutable) أو غير قابل للتغيير (Immutable). سنستعرض نقاشاً داخل الفصل خطوة بخطوة — مع كود Java الفعلي في كل مرحلة حتى ترى بالضبط ما الذي يتغير.

🔄
الكائن القابل للتغيير (Mutable Object)
كائن يمكن تغيير محتواه بعد إنشائه. معظم كائنات Java (مثل المصفوفات — Arrays، وStringBuilder) قابلة للتغيير (Mutable) بشكل افتراضي.
🔒
الكائن غير القابل للتغيير (Immutable Object)
كائن لا يمكن تغيير محتواه بعد إنشائه. المثال الكلاسيكي في Java هو String. بمجرد إنشاء الـString، تبقى أحرفه ثابتة.
🏫 الخطوة 1 — الأستاذ يكتب أول صنف
👨‍🏫
الأستاذ
سأنشئ صنفاً بسيطاً Student — يحتوي على id وname وage. له باني (Constructor) واحد، ودوال إرجاع (Getters)، ودوال تعيين (Setters). سأنشئ كائناً وأتخيله في الذاكرة. سؤالي: هل هذا الكائن قابل للتغيير (Mutable) أم غير قابل للتغيير (Immutable)؟ هل يمكننا تغيير محتواه بعد الإنشاء؟
📄 Student.java  —  الإصدار 1: يحتوي على Setters (قابل للتغيير — MUTABLE)
public class Student { private int id; private String name; private int age; // الباني (Constructor) — يضبط جميع الحقول عند إنشاء الكائن public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } // دوال الإرجاع (Getters) public int getId() { return id; } public String getName() { return name; } public int getAge() { return age; } // دوال التعيين (Setters) — تسمح بالتعديل بعد الإنشاء → قابل للتغيير (MUTABLE) public void setId(int id) { this.id = id; } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } }
// ── TestMutableStudent.java ─────────────────────────────────── public class TestMutableStudent { public static void main(String[] args) { Student ali = new Student(1001, "Ali", 20); System.out.println("Before: age = " + ali.getAge()); // 20 // التعديل عبر دالة التعيين (Setter) — الكائن قابل للتغيير (MUTABLE) ✏️ ali.setAge(21); ali.setName("Ali Updated"); System.out.println("After: age = " + ali.getAge()); // 21 ← تغيّر! System.out.println("After: name= " + ali.getName()); // "Ali Updated" ← تغيّر! } }

📦 الكائن في الذاكرة — قبل وبعد setAge(21):

ali : Student  (عند الإنشاء)
id1001
name"Ali"
age20
→ ✏️ →
ali.setAge(21)
ali : Student  (بعد دالة التعيين)
id1001
name"Ali"
age21 ✏️
🏫 الخطوة 2 — "أزل دوال التعيين (Setters)!"
🙋
الطالب
نعم يا أستاذ — لدينا دوال التعيين (Setters)، لذا يمكننا تغيير الحقول (Fields). هذا الكائن قابل للتغيير (Mutable)! لجعله غير قابل للتغيير (Immutable)، يجب أن نزيل دوال التعيين (Setters) — بهذه الطريقة لن يكون هناك طريقة لتعديل الحقول بعد إنشاء الكائن.
👨‍🏫
الأستاذ
ممتاز! ✅ لنزل دوال التعيين (Setters) ونرى ما سيحدث. الآن انظروا إلى الصنف — وانظروا إلى دالة main. هل لا يزال بإمكاننا تغيير الكائن؟
📄 Student.java  —  الإصدار 2: إزالة دوال التعيين (Setters)
public class Student { private int id; private String name; private int age; public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } // دوال الإرجاع (Getters) فقط — لا توجد دوال تعيين (Setters) بعد الآن public int getId() { return id; } public String getName() { return name; } public int getAge() { return age; } // ✅ لا توجد Setters — لا يمكن تغيير الحقول من خلال الدوال }
// ── TestNoSetters.java ──────────────────────────────────────── public class TestNoSetters { public static void main(String[] args) { Student ali = new Student(1001, "Ali", 20); System.out.println("id = " + ali.getId()); // 1001 System.out.println("name = " + ali.getName()); // "Ali" System.out.println("age = " + ali.getAge()); // 20 // ❌ خطأ في الترجمة — setAge() غير موجودة! // ali.setAge(21); → ERROR: cannot find symbol // ✅ لا يمكن تغيير الكائن بعد الإنشاء — يبدو غير قابل للتغيير! } }
🏫 الخطوة 3 — حيلة الأستاذ: جعل الحقول public
👨‍🏫
الأستاذ
جيد! لكن انتظروا — ماذا لو غيّرت محدد الوصول للحقول من private إلى public؟ لا تزال لا توجد دوال تعيين (Setters). هل لا يزال الكائن غير قابل للتغيير (Immutable)؟
🙋
الطالب
آه! 😮 إذا كانت الحقول public، يمكن لأي كود كتابة ali.age = 99 مباشرةً — دون الحاجة إلى Setter! يجب أن تبقى الحقول private دائماً!
👨‍🏫
الأستاذ
بالضبط! ✅ إذن لدينا الآن القاعدة 1 والقاعدة 2 — يجب أن تكون جميع الحقول private، ولا يجب وجود أي دوال تعيين (Setters).
📄 Student.java  —  الإصدار 3: الحقول العامة (public) تكسر الثبات (Immutability) ❌
public class Student { // ❌ خطأ: حقول public — يمكن لأي أحد تغييرها مباشرة! public int id; public String name; public int age; public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } public String getName() { return name; } public int getAge() { return age; } }
// ── TestPublicFields.java — الحقول العامة خطرة! public class TestPublicFields { public static void main(String[] args) { Student ali = new Student(1001, "Ali", 20); // ❌ الوصول المباشر للحقل — لا حاجة لـ Setter، لكن لا يزال قابلاً للتعديل! ali.age = 99; ali.name = "Hacked!"; System.out.println(ali.name); // "Hacked!" — ليس Immutable! System.out.println(ali.age); // 99 — ليس Immutable! } }
📌 قاعدتان تم تحديدهما حتى الآن
1
يجب أن تكون جميع الحقول (Fields) private
الحقول العامة (public) تسمح لأي كود بتعديلها مباشرة — دون الحاجة إلى دوال التعيين (Setters).
2
لا توجد دوال مُغيِّرة (Mutator / Setter)
دوال التعيين (Setters) تسمح بتغيير قيم الحقول بعد عملية البناء (Construction).
3
القواعد الثلاث لبناء صنف غير قابل للتغيير

ثم أضاف الأستاذ مصفوفة درجات (marks) إلى صنف Student، وأثبت أنه حتى مع تحقق القاعدتين، لا يزال من الممكن تعديل الكائن. وهذا يقودنا إلى اكتشاف القاعدة الثالثة والأكثر إدهاشاً.

🏫 الخطوة 4 — الأستاذ يضيف مصفوفة الدرجات (marks)
👨‍🏫
الأستاذ
الآن سأضيف حقلاً جديداً: marks، وهو مصفوفة (Array) من نوع int[]. لا توجد دوال تعيين (Setters)، والحقل private. أضفت دالة إرجاع (Getter) تُعيد الدرجات. سؤالي — هل هذا الصنف غير قابل للتغيير (Immutable)؟
🙋
الطالب
نعم يا أستاذ! كلتا القاعدتين محققتان: جميع الحقول private، ولا توجد دوال تعيين (Setters). يجب أن يكون غير قابل للتغيير (Immutable)!
👨‍🏫
الأستاذ
لنرَ… سأطبع الدرجات قبل وبعد حيلة صغيرة. انتبهوا جيداً! 👀
📄 Student.java  —  الإصدار 4: private، بلا Setters، مع إضافة مصفوفة الدرجات (marks)
public class Student { private int id; private String name; private int age; private int[] marks; // جديد: مصفوفة (Array) درجات الامتحانات public Student(int id, String name, int age, int[] marks) { this.id = id; this.name = name; this.age = age; this.marks = marks; } public int getId() { return id; } public String getName() { return name; } public int getAge() { return age; } // ⚠️ تُعيد المرجع (Reference) الفعلي للمصفوفة — هنا تكمن المشكلة! public int[] getMarks() { return marks; } public void printExam(int index) { System.out.println("Exam " + (index + 1) + ": " + marks[index]); } }
🏫 الخطوة 5 — حيلة الأستاذ: استغلال المرجع (Reference)
👨‍🏫
الأستاذ
سأنشئ كائناً لعلي، ثم أستدعي getMarks() وأخزّن النتيجة في متغير مؤقت. بعدها سأغيّر العنصر الثاني من خلال ذلك المتغير. لنطبع الدرجات قبل وبعد…
📄 TestExploit.java  —  ❌ تعديل كائن "بلا Setters" عبر مرجع المصفوفة (Array Reference)
public class TestExploit { public static void main(String[] args) { int[] exams = {90, 85, 92}; Student ali = new Student(1001, "Ali", 20, exams); System.out.println("=== BEFORE ==="); ali.printExam(0); // Exam 1: 90 ali.printExam(1); // Exam 2: 85 ali.printExam(2); // Exam 3: 92 // 💥 getMarks() تُعيد المرجع (Reference) الفعلي للمصفوفة الداخلية! int[] temp = ali.getMarks(); temp[1] = 0; // غيّرنا درجات علي الداخلية عبر المرجع (Reference)! System.out.println("=== AFTER (no setter was called!) ==="); ali.printExam(0); // Exam 1: 90 ali.printExam(1); // Exam 2: 0 ← تغيّرت! 😱 ali.printExam(2); // Exam 3: 92 } }
😱 مفاجأة! تم تعديل الكائن — بدون أي Setter!

تغيّرت درجة الطالب من 85 → 0، رغم أنه لا توجد دوال تعيين (Setters) وجميع الحقول private. لأن getMarks() أعادت المرجع (Reference) الفعلي للمصفوفة الداخلية. استخدم المُستدعي ذلك المرجع ليكتب مباشرة في ذاكرة المصفوفة.

🏫 الخطوة 6 — اكتشاف القاعدة الثالثة
👨‍🏫
الأستاذ
إذن — القاعدتان محققتان. جميع الحقول private، ولا توجد دوال تعيين (Setters). لكن الكائن عُدِّل رغم ذلك! ما رأيكم؟ ماذا ينقصنا؟ ما القاعدة الثالثة؟
🙋
الطالب المتميز
نعم! فهمتها! حين تُعيد دالة الإرجاع (Getter) المصفوفة (Array)، فإنها تُعيد المرجع (Reference) الحقيقي — فأستطيع الوصول إلى داخل الكائن وتعديل المحتويات عبر ذلك المرجع. القاعدة الثالثة يجب أن تكون: لا توجد دالة إرجاع (Getter) تُعيد مرجعاً (Reference) لكائن قابل للتغيير (Mutable Object)!
👨‍🏫
الأستاذ
👏👏👏 ممتاز! صحيح تماماً! الجميع يصفّق له! الآن لدينا ثلاث قواعد للصنف غير القابل للتغيير (Immutable Class).
🏫 الخطوة 7 — "لكن يا أستاذ، ماذا تقصد بالكائن القابل للتغيير (Mutable)؟"
🙋
الطالب
يا أستاذ، أفهم القاعدة — لكن ماذا تقصد بالضبط بـ"دالة إرجاع (Getter) تُعيد مرجعاً (Reference) لكائن قابل للتغيير (Mutable)"؟ مثلاً، getName() أيضاً تُعيد مرجعاً (Reference) للـString. هل هذه مشكلة أيضاً؟
👨‍🏫
الأستاذ
سؤال رائع! لنفكّر. getName() تُعيد String. الآن — هل String قابل للتغيير (Mutable) أم غير قابل للتغيير (Immutable)؟
🙋
الطالب
الـString غير قابل للتغيير (Immutable)! بمجرد إنشائه، لا يمكن تغيير أحرفه.
👨‍🏫
الأستاذ
بالضبط! لذا حتى لو أعادت getName() المرجع (Reference) للـString الداخلي، فالمُستدعي لا يستطيع تغيير قيمته — لأن String لا يملك دوالاً لتعديل أحرفه. آمن للإعادة. ✅

لكن الآن — ماذا لو غيّرت نوع name من String إلى StringBuilder؟ الـStringBuilder قابل للتغيير (Mutable) — له دوال مثل .replace() و.delete(). لو أعادت getName() مرجعاً (Reference) لـStringBuilder، يمكن للمُستدعي استدعاء تلك الدوال وتغيير الاسم — رغم عدم وجود Setter!
🙋
الطلاب
آهاا! إذن القاعدة تنطبق فقط عندما يكون النوع المُعاد هو بحد ذاته كائن قابل للتغيير (Mutable Object) — كمصفوفة (Array)، أو StringBuilder، أو أي صنف يمكن تعديله بعد الإنشاء!
👨‍🏫
الأستاذ
ممتاز! 🎉 دائماً اسأل نفسك: "هل النوع الذي أُعيده قابل للتغيير (Mutable)؟" إذا نعم — أعد نسخة (Copy). إذا لا (كـString، int، double) — يمكن إعادته مباشرة بأمان.
🏫 الخطوة 8 — الحل: إعادة النسخة الواقية (Defensive Copy)
👨‍🏫
الأستاذ
الآن — كيف نُصلح صنف Student الذي يحتوي على مصفوفة الدرجات (marks)؟ نعلم أننا لا نستطيع حذف دالة الإرجاع (Getter) — الطلاب يحتاجون لقراءة درجاتهم. إذن ما الحل؟
🙋
الطالب المتميز
بدلاً من إعادة المصفوفة (Array) الحقيقية، ننشئ مصفوفة جديدة، ننسخ فيها جميع العناصر، ونُعيد تلك المصفوفة الجديدة. المُستدعي يحصل على نسخته الخاصة — إن غيّرها، يغيّر نسخته فقط، ولا تتأثر المصفوفة الأصلية داخل كائن الطالب!
👨‍🏫
الأستاذ
👏👏👏 ممتاز! هذه التقنية تُسمى النسخة الواقية (Defensive Copy). لنكتب الآن الصنف الصحيح غير القابل للتغيير (Immutable Class).
📄 ImmutableStudent.java  —  ✅ غير قابل للتغيير حقاً — القواعد الثلاث محققة
public class ImmutableStudent { private final int id; // ✅ القاعدة 1: private private final String name; // ✅ String غير قابل للتغيير (Immutable) أصلاً private final int age; private final int[] marks; // ✅ القاعدة 1: private public ImmutableStudent(int id, String name, int age, int[] marks) { this.id = id; this.name = name; this.age = age; this.marks = marks.clone(); // نسخة واقية (Defensive Copy) حتى عند البناء! } // ✅ القاعدة 2: لا توجد Setters إطلاقاً public int getId() { return id; } public String getName() { return name; } public int getAge() { return age; } // ✅ القاعدة 3: إعادة نسخة (COPY) — وليس المرجع (Reference) الحقيقي للمصفوفة public int[] getMarks() { return marks.clone(); // نسخة واقية (Defensive Copy) — المُستدعي يحصل على نسخته } public void printExam(int index) { System.out.println("Exam " + (index + 1) + ": " + marks[index]); } }
// ── TestImmutable.java — الكائن الآن غير قابل للتغيير حقاً ──── public class TestImmutable { public static void main(String[] args) { ImmutableStudent ali = new ImmutableStudent( 1001, "Ali", 20, new int[]{90, 85, 92} ); System.out.println("=== BEFORE ==="); ali.printExam(0); // Exam 1: 90 ali.printExam(1); // Exam 2: 85 ali.printExam(2); // Exam 3: 92 int[] temp = ali.getMarks(); // يحصل على نسخة (COPY) temp[1] = 0; // يعدّل النسخة فقط System.out.println("=== AFTER (attempted change) ==="); ali.printExam(0); // Exam 1: 90 ← لم تتغير ✅ ali.printExam(1); // Exam 2: 85 ← لم تتغير ✅ ali.printExam(2); // Exam 3: 92 ← لم تتغير ✅ } }
📌 خلاصة — ما الذي يجعل الكائن غير قابل للتغيير (Immutable)؟

الكائن (Object) غير قابل للتغيير (Immutable) عندما لا يمكن تغيير محتواه بعد إنشائه. أي صنف يحقق القواعد التالية كلها — أي كائن يُنشأ منه يكون غير قابل للتغيير (Immutable):

1
يجب أن تكون جميع الحقول (Fields) private
الحقول العامة (public) تسمح لأي كود بتعديلها مباشرة — حتى بدون دوال التعيين (Setters).
2
لا توجد دوال مُغيِّرة (Mutator / Setter)
دوال التعيين (Setters) تسمح بتغيير قيم الحقول بعد عملية البناء (Construction).
3
لا توجد دالة إرجاع (Getter) تُعيد مرجعاً (Reference) لكائن قابل للتغيير (Mutable Object)
إذا أعادت دالة الإرجاع (Getter) المرجع (Reference) المباشر لمصفوفة (Array) أو كائن قابل للتغيير (Mutable Object)، يستطيع المُستدعي تعديل المحتوى — حتى بدون Setter. أعد نسخة واقية (Defensive Copy) بدلاً من ذلك.
💡 ماذا عن كلمة final على المصفوفة (Array)؟

كلمة final على متغير مرجع (Reference Variable) تعني أن المرجع نفسه لا يمكن إعادة تعيينه. لكنها لا تمنع تعديل محتويات المصفوفة — فـmarks[1] = 0 لا تزال تُترجم وتُنفَّذ بنجاح. لذا كلمة final وحدها لا تكفي — ما زلت بحاجة إلى النسخة الواقية (Defensive Copy) في دالة الإرجاع (Getter)!