What immutable means
An immutable object cannot be changed after it is created. If you ask it to change, it gives you a new object and leaves the old one alone.
Once the object is built, everyone can trust that it stays the same. String is the immutable type everybody uses every day.
String city = "bengaluru";
city.toUpperCase();
System.out.println(city); // bengaluru, nothing changed
String upper = city.toUpperCase();
System.out.println(upper); // BENGALURU, a new StringtoUpperCase() did not touch city. It built a fresh String and returned it. If you do not store the result, it is lost. This is a very common bug in coding rounds.
Why Java made String immutable
This comes up in almost every Java interview. There are four reasons, and you should be able to give all of them.
- The string pool. Java reuses one copy of each literal. That is only safe if nobody can change it. If
"Swiggy"were mutable and one class changed it, every other class holding"Swiggy"would see the change. - Security. Strings carry file paths, DB URLs, usernames and class names. If a String could change after you validated it, someone could pass the check with
"/home/app/report.pdf"and then flip it to"/etc/passwd"before you open the file. - Cached hashCode. A String works out its hash (a number made from its characters) once and stores it in a private field. Since the content never changes, the hash never goes stale. That is why Strings make fast and safe
HashMapkeys. - Thread safety for free. Nothing changes, so many threads can share a String without any locking.
The string pool
The heap is the big shared memory area where Java keeps every object you create (note 20 covers it in detail). The string pool is a table inside the heap where Java keeps one copy of each string literal.
String a = "Swiggy";
String b = "Swiggy";
String c = new String("Swiggy");
System.out.println(a == b); // true, both point to the same pooled object
System.out.println(a == c); // false, new String() always makes a new object
System.out.println(a.equals(c)); // true, same charactersnew String("Swiggy") is almost never what you want. It creates an extra object for no reason. Just write the literal.
== vs equals with strings
== checks "same object". equals checks "same characters". For strings, you want equals every single time.
The trap is that == sometimes works, which makes people think it is fine.
String plate = "KA01AB1234";
String fromLiteral = "KA01" + "AB1234"; // the compiler joins these at compile time
String prefix = "KA01";
String fromVariable = prefix + "AB1234"; // built at runtime, new object
System.out.println(plate == fromLiteral); // true
System.out.println(plate == fromVariable); // false
System.out.println(plate.equals(fromVariable)); // trueThe compiler can join two literals before the program runs, so it stores the finished "KA01AB1234" in the pool. A variable has no known value until the program runs, so that join happens later and builds a new object. One more twist: if prefix were declared final String prefix = "KA01", the compiler would know its value too, and == would be true again. Without these rules, == on strings looks random.
Strings from user input, a database, JSON or a file are always new objects. == will fail on them in production even if it passed on your test literals.
If one side might be null, put the literal first so you never call a method on null:
"UPI".equals(paymentMode) // false if paymentMode is null, no crash
paymentMode.equals("UPI") // NullPointerException if paymentMode is nullintern()
intern() returns the pooled copy of a string, adding it to the pool if needed.
String c = new String("Swiggy");
System.out.println("Swiggy" == c.intern()); // trueYou rarely need it in normal code. It can save memory when you have millions of repeated values (city names, status codes) loaded from a file. Do not call it on user input in a long running service, though. Every distinct value goes into one table shared by the whole JVM, and you cannot size or inspect it easily. If duplicate strings are eating your heap, a plain HashMap you control, or the JVM's -XX:+UseStringDeduplication flag, is the safer fix.
Concatenation in a loop
Because a String never changes, s += "x" creates a brand new String each time and copies all the old characters into it.
String s = "";
for (int i = 0; i < 100_000; i++) {
s += "x"; // copies 1, then 2, then 3 ... characters
}That is about 5 billion character copies in total. The work grows with the square of the loop size, so 10 times more iterations means about 100 times more work.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100_000; i++) {
sb.append("x"); // writes into one growing buffer
}
String s = sb.toString();Rough, illustrative numbers for 100,000 iterations (not measured in this repo; they change with your machine and Java version):
s += "x" ~350 ms
sb.append("x") ~2 ms
Double the loop and the first one takes three to four times as long. The second one takes about twice as long. At a million iterations the += version runs for tens of seconds, and it also creates a lot of short lived garbage for the garbage collector (GC) to clean up.
A single line like "Order " + id + " for " + name is fine. Since Java 9 the compiler turns that into one call that works out the final size and copies each piece once. The problem is only += inside a loop.
The production version of this bug is usually logging. log.debug("Built cart " + cart) builds the whole string, including cart.toString(), even when debug logging is switched off. In a hot path that runs thousands of times a second, that is real CPU and real garbage. Use the placeholder form, log.debug("Built cart {}", cart), and the string is only built if the line is actually logged.
StringBuilder vs StringBuffer
| StringBuilder | StringBuffer | |
|---|---|---|
| Mutable | Yes | Yes |
| Thread safe | No | Yes, every method is synchronized |
| Speed | Faster | Slower, pays for locking |
| Added in | Java 5 | Java 1.0 |
| Use it | Almost always | Almost never |
A builder is nearly always a local variable inside one method, so only one thread ever sees it. That is why StringBuilder is the default. StringBuffer is mostly found in old code.
String methods interviewers ask about
" Masala Dosa ".trim(); // "Masala Dosa"
" Masala Dosa ".strip(); // "Masala Dosa", also handles Unicode spaces (Java 11)
" ".isBlank(); // true, only whitespace (Java 11)
" ".isEmpty(); // false, length is 3
String.join(", ", List.of("idli", "dosa", "vada")); // "idli, dosa, vada"
"-".repeat(20); // "--------------------" (Java 11)
"UPI".chars(); // IntStream of character codes
"order-1042".split("-"); // ["order", "1042"]
"KA01AB1234".substring(0, 4); // "KA01"
"Swiggy".contains("wig"); // true
"swiggy".equalsIgnoreCase("SWIGGY"); // truestrip vs trim: trim removes spaces, tabs and newlines from both ends (any character with a code at or below the plain space, U+0020). strip also removes other Unicode spaces such as the em space (U+2003). Prefer strip in new code. Neither one removes the non breaking space (U+00A0), a space that looks normal but has a different code. It often shows up in data pasted from Excel or a web page. If a "trimmed" value still fails to match, check for that.
Counting characters with chars():
long vowels = "bengaluru".chars()
.filter(ch -> "aeiou".indexOf(ch) >= 0)
.count();
System.out.println(vowels); // 4Text blocks (Java 15+)
Multi line strings without \n and escaped quotes everywhere.
String json = """
{
"orderId": "SWG-1042",
"amount": 349
}
""";Java measures the indent of every content line and of the closing """ line, takes the smallest, and removes that much from every line. Here the smallest is 8 spaces, so the { ends up at column zero. Move the closing """ further left and the content keeps more of its indent. Very handy for SQL, JSON and test data.
Writing your own immutable class
The rules:
- Make the class
finalso nobody can subclass it and add mutable state. - Make every field
private final. - No setters.
- Copy mutable inputs in the constructor (lists, arrays,
Date), so the caller's later changes cannot reach you. - Never hand out your mutable internals. Return a copy or an unmodifiable view.
- "Change" methods return a new object.
Money
public final class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add " + other.currency + " to " + currency);
}
return new Money(amount.add(other.amount), currency); // new object, this one untouched
}
public BigDecimal getAmount() { return amount; }
public String getCurrency() { return currency; }
}BigDecimal and String are already immutable, so no copies are needed. add works like String.toUpperCase(): it returns a new object. The currency check is there because adding rupees to dollars is a real bug that a plain double amount field would never catch.
Order, and why defensive copies matter
Here is the version that looks immutable but is not:
public final class Order {
private final String orderId;
private final List<String> items;
public Order(String orderId, List<String> items) {
this.orderId = orderId;
this.items = items; // stores the caller's list
}
public List<String> getItems() {
return items; // hands out our list
}
}
List<String> items = new ArrayList<>(List.of("Paneer Tikka"));
Order order = new Order("SWG-1042", items);
items.add("Gulab Jamun"); // caller changes their list
order.getItems().clear(); // or anyone clears oursfinal only stops the field from pointing to a different list. It does nothing to stop the list itself from changing.
The fixed version uses defensive copies, which are private copies made so outside code cannot touch your data:
public final class Order {
private final String orderId;
private final List<String> items;
private final Date placedAt;
public Order(String orderId, List<String> items, Date placedAt) {
this.orderId = orderId;
this.items = List.copyOf(items); // copy in
this.placedAt = new Date(placedAt.getTime()); // Date is mutable, copy it
}
public List<String> getItems() {
return items; // already unmodifiable
}
public Date getPlacedAt() {
return new Date(placedAt.getTime()); // copy out
}
}Better still, use LocalDateTime or Instant instead of Date. They are immutable, so no copying is needed at all.
List.copyOf vs Collections.unmodifiableList
They look similar but behave differently.
List<String> source = new ArrayList<>(List.of("Masala Dosa"));
List<String> copy = List.copyOf(source);
List<String> view = Collections.unmodifiableList(source);
source.add("Filter Coffee");
System.out.println(copy); // [Masala Dosa]
System.out.println(view); // [Masala Dosa, Filter Coffee]List.copyOf(Java 10) takes a real copy. Later changes to the source do not show up. It throws on null elements.Collections.unmodifiableListis a read only window onto the original list. You cannot change it through the view, but you will see changes made to the source.
For an immutable class, you want List.copyOf. A view is fine when you want callers to watch live data without being able to edit it.
Common mistakes
- Calling
s.trim()ors.replace(...)without storing the result. The original never changes. - Comparing strings with
==. Works on literals in tests, fails on real input. +=in a loop. UseStringBuilder.- Using
StringBufferby habit. You are paying for locks you do not need. - Thinking
finalmakes a field immutable. It only freezes the reference. The list behind it can still change. - Forgetting the copy out. Copying in the constructor is half the job. A getter that returns the internal list undoes it.
- Leaving the class non final. A subclass can add setters and break every promise.
Interview questions
"Why is String immutable in Java?"
So the string pool can safely share one copy of each literal, so a validated path or URL cannot change after the check, so the hashCode can be cached, and so threads can share strings without locks.
"What is the difference between a string literal and new String(...)?"
The literal comes from the string pool and is reused.
new Stringalways creates a new object on the heap, even if the same text is already in the pool, so==between the two is false.
"Why should you not use += on a String inside a loop?"
Each
+=creates a new String and copies every character so far, so the total work grows with the square of the loop size.StringBuilderappends into one buffer and stays linear.
"StringBuilder or StringBuffer?"
StringBuilder. StringBuffer synchronizes every method, which costs time, and a builder is almost always a local variable that only one thread uses.
"How do you make a class immutable?"
Make the class final, every field private and final, no setters, copy mutable inputs in the constructor, and return copies or unmodifiable versions from getters. Methods that change something return a new object.
"Is List.copyOf the same as Collections.unmodifiableList?"
No.
List.copyOftakes a snapshot, so later changes to the source do not show.unmodifiableListis a read only view, so changes to the source still show through it.
Where you see it in this repo
StringBuilderbuilds receipts inOOOD/06-grocery-store-system/src/grocery/Receipt.javaandLLD/ParkingLot/src/service/ReceiptService.java. The grocery one appends inside a loop over order items, which is exactly where+=would hurt. The parking one appends a fixed list of lines.- String as a map key:
SeatLockManagerinOOOD/02-movie-ticket-bookingbuilds keys likescreeningId + "-" + seatNumber. This only works because String is immutable and its hashCode never changes. - A small immutable class:
SeatLock, nested inside the sameSeatLockManager. Private final fields, no setters, andLocalDateTimewhich is itself immutable. - Unmodifiable views:
Hand.getCards()inOOOD/08-blackjack-game,VendingMachine.getTransactionHistory()inOOOD/04-vending-machine, andMenuinOOOD/11-restaurant-management-system. These are views, not copies.Tablein the same restaurant system goes one step further and copies each list before wrapping it. - A mutable
Date:Receiptin the grocery store storesnew Date(). It is never handed out, so it is safe today, butLocalDateTimewould remove the question entirely.
No value class in the repo is declared final yet. The final classes are static helpers with private constructors, such as Rules in InterpreterPattern/After/Main.java and Money and DebtSimplifier in LLD/Splitwise, or internals like Node in LLD/LruCache. None of them is an immutable object. The Ticket in OOOD/01-parking-lot is close (most fields are private final) but keeps exitTime mutable on purpose, since a ticket really does change when the car leaves.
Related: 06-StaticAndFinal.md for what final does and does not freeze, 15-EqualsAndHashCode.md for why immutable keys matter in a HashMap, and 22-RecordsAndSealedClasses.md for records, which give you an immutable class in one line.