🇸🇦 العربية
CPCS203  ·  Module 05  ·  Part 1 of 3

🔒 Mutable & Immutable Objects

Understanding when an object can — and cannot — be changed after creation

🔑 Encapsulation Recap 🔄 Mutable Objects 🔒 Immutable Objects 📋 3 Rules for Immutability 🛡️ Defensive Copy
📋 Table of Contents
  1. The Two Pillars: Encapsulation & Abstraction (Recap)
  2. Mutable vs Immutable Objects — Step-by-Step Classroom Discussion
  3. The Three Rules for an Immutable Class
1
The Two Pillars: Encapsulation & Abstraction

Welcome, dear students! We have just finished studying Classes & Objects. By now, our mindset has shifted — we have moved from procedural thinking to object-oriented thinking. We can identify objects, their properties (fields) and their behaviour (methods), express them in UML, and translate that UML into Java code.

Before we dive into today's new material, let us quickly recall the two foundational pillars of OOP that we have already met.

🔒
Encapsulation
Hiding the internal state of an object from the outside world. All data fields are kept private; the outside can only interact through well-defined public methods (getters / setters).

Think of it as a medicine capsule — the contents are hidden inside; you only interact with the outer shell.
🧩
Abstraction
Separating implementation from usage. A user of a class does not need to know how it works internally — only what it does. The public interface (method signatures) is what matters.

Think of driving a car: you use the steering wheel and pedals without knowing the engine internals.
📚 Where to read more
Please revisit the Encapsulation and Abstraction sections in Module 04 (Objects & Classes) for a deeper explanation and worked examples.
2
Mutable vs Immutable Objects

Every object we create is either mutable or immutable. Let us walk through a classroom discussion step by step — with the actual Java code at each stage so you can see exactly what changes.

🔄
Mutable Object
An object whose content CAN be changed after it has been created. Most Java objects (e.g., arrays, StringBuilder) are mutable by default.
🔒
Immutable Object
An object whose content CANNOT be changed once it is created. Classic Java example: String. Once a String is created, its characters are fixed.
🏫 Step 1 — The Professor Writes the First Class
👨‍🏫
Professor
I will create a simple Student class — it has id, name, and age. It has one constructor, getters, and setters. I will create an object and visualize it. Now, my question: is this object mutable or immutable? Can we change its content after it is created?
📄 Student.java  —  Version 1: has setters (MUTABLE)
public class Student { private int id; private String name; private int age; // Constructor — sets all fields when the object is created 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 — these allow modification AFTER creation → 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 // Modify through setter — object is MUTABLE ✏️ ali.setAge(21); ali.setName("Ali Updated"); System.out.println("After: age = " + ali.getAge()); // 21 ← changed! System.out.println("After: name= " + ali.getName()); // "Ali Updated" ← changed! } }

📦 Object in memory — before and after setAge(21):

ali : Student  (at creation)
id1001
name"Ali"
age20
→ ✏️ →
ali.setAge(21)
ali : Student  (after setter)
id1001
name"Ali"
age21 ✏️
🏫 Step 2 — "Remove the Setters!"
🙋
Student
Yes, Professor — we have setters, so we can change the fields. This object is mutable! To make it immutable, we should remove the setters — that way there is no way to modify the fields after the object is created.
👨‍🏫
Professor
Excellent! ✅ Let us remove the setters and see what happens. Now look at the class — and look at the main. Can we still change the object?
📄 Student.java  —  Version 2: setters removed
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 only — NO setters anymore public int getId() { return id; } public String getName() { return name; } public int getAge() { return age; } // ✅ No setters — fields cannot be changed through methods }
// ── 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 // ❌ Compile error — setAge() does not exist! // ali.setAge(21); → ERROR: cannot find symbol // ✅ Object cannot be changed after creation — looks immutable! } }
🏫 Step 3 — The Professor's Trick: Making Fields public
👨‍🏫
Professor
Good! But wait — what if I change the access modifier of the data fields from private to public? There are still no setters. Is it still immutable?
🙋
Student
Oh! 😮 If the fields are public, anyone can write ali.age = 99 directly — no setter needed! The fields must stay private!
👨‍🏫
Professor
Exactly! ✅ So we now have Rule 1 and Rule 2 — all fields must be private, and there must be no setters.
📄 Student.java  —  Version 3: public fields break immutability ❌
public class Student { // ❌ BAD: public fields — anyone can change them directly! 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 fields are dangerous! public class TestPublicFields { public static void main(String[] args) { Student ali = new Student(1001, "Ali", 20); // ❌ Direct field access — no setter needed, still modifiable! ali.age = 99; ali.name = "Hacked!"; System.out.println(ali.name); // "Hacked!" — NOT immutable! System.out.println(ali.age); // 99 — NOT immutable! } }
📌 Two Rules Established So Far
1
All data fields must be private
Public fields let any code modify them directly — no setter needed.
2
No mutator (setter) methods
Setters allow changing field values after construction.
3
The Three Rules for an Immutable Class

The professor then added an array of marks to the Student class and showed that even with two rules satisfied, the object can still be modified. This leads to the discovery of the third and most surprising rule.

🏫 Step 4 — Professor Adds a Marks Array
👨‍🏫
Professor
Now I will add a new data field: marks, which is an int[] array. There are no setters, and the field is private. I add a getter that returns the marks. My question — is this class immutable?
🙋
Student
Yes, Professor! Both rules are satisfied: all fields are private, and there are no setters. It must be immutable!
👨‍🏫
Professor
Let us see… I will print the marks before and after a small trick. Watch carefully! 👀
📄 Student.java  —  Version 4: private, no setters, marks array added
public class Student { private int id; private String name; private int age; private int[] marks; // NEW: array of exam marks 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; } // ⚠️ Returns the ACTUAL array reference — this is the problem! public int[] getMarks() { return marks; } public void printExam(int index) { System.out.println("Exam " + (index + 1) + ": " + marks[index]); } }
🏫 Step 5 — The Professor's Trick: Exploiting the Reference
👨‍🏫
Professor
I create an object for Ali, then I call getMarks() and store the result in a temporary variable. Then I change the second element through that variable. Let us print the marks before and after…
📄 TestExploit.java  —  ❌ Modifying a "no-setter" object through 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() returns the ACTUAL internal array reference! int[] temp = ali.getMarks(); temp[1] = 0; // Changed ali's internal marks through the reference! System.out.println("=== AFTER (no setter was called!) ==="); ali.printExam(0); // Exam 1: 90 ali.printExam(1); // Exam 2: 0 ← CHANGED! 😱 ali.printExam(2); // Exam 3: 92 } }
😱 Surprise! The object was modified — without any setter!

The student's mark changed from 85 → 0, even though there are no setter methods and all fields are private. Because getMarks() returned the reference to the real internal array. The caller used that reference to directly write into the array's memory.

🏫 Step 6 — Discovering the Third Rule
👨‍🏫
Professor
So — the two rules were satisfied. All data fields are private, and there are no setters. But the object was still modified! What do you think? What is missing? What should the third rule be?
🙋
Smart Student
Yes! I got it! When the getter returns the array, it returns the real reference — so I can reach inside the object and change the contents through that reference. The third rule must be: no getter should return a reference to a mutable object!
👨‍🏫
Professor
👏👏👏 Excellent! Absolutely correct! Everyone, give him a round of applause! Now we have three rules for an immutable class.
🏫 Step 7 — "But Professor, what do you mean by a mutable object?"
🙋
Student
Professor, I understand the rule — but what exactly do you mean by "a getter that returns a reference to a mutable object"? For example, getName() also returns a reference to a String. Is that a problem too?
👨‍🏫
Professor
Great question! Let us think about it. getName() returns a String. Now — is String mutable or immutable?
🙋
Student
String is immutable! Once it is created, its characters cannot be changed.
👨‍🏫
Professor
Exactly! So even if getName() returns the reference to the internal String, the caller cannot change its value — because String has no method to modify its characters. It is safe to return. ✅

But now — what if I change the type of name from String to StringBuilder? StringBuilder is mutable — it has methods like .replace() and .delete(). If getName() returns the reference to a StringBuilder, the caller can call those methods and change the name — even though there is no setter!
🙋
Students
Ahhh! So the rule applies only when the returned type is itself a mutable object — like an array, a StringBuilder, or any class that can be changed after creation!
👨‍🏫
Professor
Perfect! 🎉 Always ask yourself: "Is the type I am returning mutable?" If yes — return a copy. If no (like String, int, double) — it is safe to return directly.
🏫 Step 8 — The Fix: Return a Defensive Copy
👨‍🏫
Professor
Now — how do we fix our Student class with the marks array? We know we cannot just remove the getter — students need to read their marks. So what is the solution?
🙋
Smart Student
Instead of returning the real array, create a new array, copy all the elements into it, and return that new array. The caller gets their own copy — if they change it, they change only their copy, and the original inside the student object is untouched!
👨‍🏫
Professor
👏👏👏 Excellent! This technique is called a defensive copy. Let us write the correct immutable class now.
📄 ImmutableStudent.java  —  ✅ Truly immutable — all 3 rules satisfied
public class ImmutableStudent { private final int id; // ✅ Rule 1: private private final String name; // ✅ String is already immutable private final int age; private final int[] marks; // ✅ Rule 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 even at construction! } // ✅ Rule 2: No setters at all public int getId() { return id; } public String getName() { return name; } public int getAge() { return age; } // ✅ Rule 3: Return a COPY — not the real array reference public int[] getMarks() { return marks.clone(); // defensive copy — caller gets their own copy } public void printExam(int index) { System.out.println("Exam " + (index + 1) + ": " + marks[index]); } }
// ── TestImmutable.java — now the object is truly immutable ──── 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(); // gets a COPY temp[1] = 0; // only modifies the copy System.out.println("=== AFTER (attempted change) ==="); ali.printExam(0); // Exam 1: 90 ← unchanged ✅ ali.printExam(1); // Exam 2: 85 ← unchanged ✅ ali.printExam(2); // Exam 3: 92 ← unchanged ✅ } }
📌 Conclusion — What Makes an Object Immutable?

An object is immutable when its content cannot be changed after it is created. Any class that meets all of the following rules — any object created from it is immutable:

1
All data fields must be private
Public fields let any code modify them directly — even without setters.
2
No mutator (setter) methods
Setters allow changing field values after construction.
3
No getter that returns a reference to a mutable object
If a getter returns the direct reference to an array or mutable object, the caller can modify the contents — even without a setter. Return a defensive copy instead.
💡 What about final on an array?

final on a reference variable means the reference cannot be reassigned. But it does NOT prevent modifying the array's contentsmarks[1] = 0 still compiles and runs. So final alone is not enough — you still need defensive copies in your getter!