Immutability in Java: Building Objects That Never Change
Learn immutability in Java — what immutable objects are, why they are safer and thread-safe, and how to build your own immutable class step by step, with records as the modern shortcut.
Here's an idea that sounds restrictive but is actually one of the secrets to writing safe, bug-resistant code: immutability. An immutable object is one that can't be changed after it's created. You've already used the most famous example — String — many times. This lesson explains why immutability is so valued, and how to make your own immutable classes. It's the final lesson of Level 3. 🎉
What does immutable mean?
An immutable object's state (its field values) is fixed for its entire life. If you want a "changed" version, you don't modify the original — you create a new object. You saw this exact behaviour with Strings back in Lesson 11:
String s = "hello"; s.toUpperCase(); // does NOT change s — returns a NEW string System.out.println(s); // still "hello" s = s.toUpperCase(); // to "change" it, reassign to the new object
That's immutability in a nutshell: methods give you new objects rather than editing the existing one.
Why immutability is a good thing
- Safety — once created, an immutable object can never be put into a broken or unexpected state. What you see is what you get, forever.
- Predictability — you can pass it around freely knowing no other code can secretly change it behind your back.
- Thread-safety — because nothing can change it, multiple threads can share it with zero risk (hugely important, as you'll see in the multithreading lesson).
- Great map keys — since its value (and hashCode) never changes, an immutable object is a reliable
HashMapkey.
How to make a class immutable
There's a simple recipe. Make a class immutable by:
- Marking the class
final(so it can't be subclassed and altered). - Making all fields
private final(set once, never reassigned). - Setting all fields in the constructor.
- Providing getters but no setters — no way to change anything after construction.
public final class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } // getters only public int getY() { return y; } // NO setters! }
Once you create a Point(3, 4), it's (3, 4) for good. Need a different point? Make a new one. That constraint is exactly what makes it so safe to use.
A "change" returns a new object
If you want an operation that appears to modify the object, have it return a fresh instance — just like String does:
public Point moveBy(int dx, int dy) { return new Point(x + dx, y + dy); // new object, original untouched }
public record Point(int x, int y) {} automatically creates the fields, constructor, getters, and even equals()/hashCode() for you, all immutable. We'll meet records later, but know that they're the modern shortcut for exactly this pattern.Immutability was a mindset shift for me — it feels odd to "not be allowed" to change an object at first. But once I started making my value-type classes immutable, an entire category of "who changed this?!" bugs vanished. When you can guarantee an object never changes after creation, you can reason about your code with total confidence. It's a small discipline that pays off enormously as programs grow — and it's a hallmark of thoughtful, professional Java.
Key takeaways
- An immutable object can't be changed after creation; "changes" produce a new object.
- It brings safety, predictability, thread-safety, and reliable map keys.
- Recipe:
finalclass,private finalfields set in the constructor, getters and no setters. Stringis the classic example; modern records make immutable classes in one line.
← Previous: Lesson 25 — equals() and hashCode()
Next: Lesson 27 — Lambdas & Functional Interfaces →
↑ Back to the full Java course roadmap