Effective Java essentials
Minimize mutability, defensive copies, prefer interfaces, avoid finalizers, design for inheritance or prohibit it.
Minimize mutability
Immutable objects can't change after construction, so they're thread-safe and simple to reason about. Records help: a record class is final and its fields are private final. But final only freezes the reference, not the object it points to...
record Team(List<String> names) {}
// record: final class
// names: private final fieldThe leaky record
What does this print?
record Team(List<String> names) {}
void main() {
var list = new ArrayList<>(List.of("Ali"));
var t = new Team(list);
list.clear();
IO.println(t.names().size());
}10Throws UnsupportedOperationException
Show the answer
The record stored **the same ArrayList the caller still holds. Clearing the caller's list cleared the record's state too. Records store the reference they're given; they don't copy** mutable inputs.
Defensive copies
record Team(List<String> names) {}Anyone holding the original list can change the team.
record Team(List<String> names) {
Team {
names = List.copyOf(names);
}
}A compact constructor makes an unmodifiable copy, so neither side can change it later.
A constructor calling a hook
Base's constructor calls an overridable method. What does this print?
class Base {
Base() { init(); }
void init() {}
}
class Sub extends Base {
int size = 5;
void init() { System.out.println(size); }
}
void main() { new Sub(); }50Compile errorThrows NullPointerException
Show the answer
Base() runs before Sub's field initializers, so when the overridden init() runs, size still holds its default 0. With a String field you'd see null. Rule: constructors must not call overridable methods.
Design for inheritance, or prohibit it
Subclassing a class that wasn't designed for it couples the subclass to internal details that may change. Effective Java: either document the hooks carefully, or prohibit inheritance by declaring the class final or giving it only private constructors.
public final class Money {
private Money(long cents) { ... }
public static Money of(long c) { ... }
}Finalizers
finalize() runs unpredictably, maybe never, and can even resurrect objects. Finalization is deprecated for removal. For cleanup, implement AutoCloseable and use try-with-resources, or java.lang.ref.Cleaner as a safety net.
@Override
protected void finalize() { close(); } // no!
try (var in = Files.newInputStream(p)) {
...
} // closed here, deterministicallyRefer to objects by their interfaces
Declare variables, parameters and return types as List<String>, not ArrayList<String>. Then you can swap ArrayList for another List without touching the code that uses it. Declaring the concrete class makes switching *harder*, not easier.
List<String> names = new ArrayList<>();
Map<String, Integer> counts = new HashMap<>();Why teams cite it
Effective Java is one of the most quoted books in Java code reviews. Immutable value objects, defensive copies of mutable inputs and outputs, final classes and try-with-resources prevent whole families of concurrency, security and resource-leak bugs.
Key takeaways
- Immutable objects are thread-safe and simpler to reason about
- Copy mutable inputs and outputs (lists, arrays, Date)
- Declare List<String>, not ArrayList<String>
- Use try-with-resources or Cleaner instead of finalize()
💡 Photocopy a contract before filing it, so nobody can change your copy by editing theirs.
Object.finalize() was deprecated in Java 9, and JEP 421 in Java 18 deprecated finalization for removal altogether.
Practice questions
What does this print?
record Team(List<String> members) {}
void main() {
var names = new ArrayList<>(List.of("Ali"));
var t = new Team(names);
names.add("Sara");
System.out.println(t.members());
}- [Ali]
- Throws UnsupportedOperationException
- [Sara]
- [Ali, Sara]
Check your answer
[Ali, Sara]. The record stores the reference it was given. The caller still holds the same ArrayList, so changing it changes the 'immutable' record's state.
How do you make this record truly immutable?
record Team(List<String> members) {}- Declare the record final
- Mark the members component private final
- Add a compact constructor that does members = List.copyOf(members);
- Return members.stream() instead of the list
Check your answer
Add a compact constructor that does members = List.copyOf(members);. Records are already final with private final fields; the problem is the shared mutable list. List.copyOf makes an unmodifiable copy, so neither side can change it later.