Chapter 1: Creating and Destroying Objects
effective-java object-lifecycle java best-practices constructors builders singletons
Book: Effective Java, 3rd Edition β Joshua Bloch
Status: π© Complete
Difficulty: Medium
Items: 1-9 (9 items)
Time to complete: ~45 min
Overview
Chapter 1 addresses a deceptively fundamental question: how should objects be born and how should they die? Poor choices here ripple through an entire codebase β rigid APIs that canβt evolve, objects that carry hidden costs, resource leaks, and subtle memory problems. Bloch covers the full lifecycle: creation patterns (static factories, builders, singletons, and dependency injection), avoiding wasteful instantiation, cleaning up stale references, and the dangers of finalizers. These items collectively encode decades of Java platform design experience and directly influence API ergonomics, performance, and correctness.
Items
Item 1: Consider Static Factory Methods Instead of Constructors
The Problem
Constructors are the default, but they are inflexible. Every call to new creates a new instance, the constructor name is fixed to the class name (giving no contextual information), and there is no way to return a subtype or a cached instance.
// BAD: raw constructor β no semantic name, always allocates
Boolean flag = new Boolean("true"); // deprecated in Java 9, removed in Java 17
// BAD: caller cannot tell what kind of connection this is
Connection c = new Connection("localhost", 5432, true, false);The Solution
Provide static factory methods β ordinary static methods that return an instance of the class (or any subtype).
// GOOD: semantically named, can cache, can return subtype
public class Connection {
private static final Connection LOOPBACK =
new Connection("127.0.0.1", 5432);
private Connection(String host, int port) { ... }
// Descriptive name tells the caller what they get
public static Connection loopback() {
return LOOPBACK; // cached β no new allocation
}
public static Connection withHost(String host, int port) {
return new Connection(host, port);
}
// Can return a subtype β callers need not know the impl class
public static Connection encrypted(String host, int port) {
return new TlsConnection(host, port); // TlsConnection extends Connection
}
}
// Usage:
Connection c1 = Connection.loopback(); // clear intent
Connection c2 = Connection.withHost("db", 5432); // readable
Connection c3 = Connection.encrypted("db", 5432); // subtype hidden from callerWhy This Works
- Names convey meaning:
BigInteger.probablePrime(bitLength, rnd)is clearer thannew BigInteger(bitLength, 1, rnd). - Instance control: the factory decides whether to return a new object, a cached one, or a subtype β callers cannot tell the difference.
- Return-type flexibility: the returned object can be any subtype, enabling interface-based APIs where the implementation class is an implementation detail (e.g.,
Collections.unmodifiableList()returns an internal class). - Reduces the API surface:
EnumSet.of(...)returns aRegularEnumSetorJumboEnumSetdepending on the enum size. The caller only knowsEnumSet.
Naming Conventions
| Convention | Example | Meaning |
|---|---|---|
from | Date.from(instant) | Type conversion β single param |
of | EnumSet.of(JACK, QUEEN) | Aggregation β multiple params |
valueOf | BigInteger.valueOf(long) | Like from/of, more verbose |
instance / getInstance | Calendar.getInstance() | May return cached instance |
create / newInstance | Array.newInstance(cls, len) | Guarantees new object |
getType | Files.getFileStore(path) | Factory on a different class |
newType | BufferedReader.newBufferedReader() | Guarantees new object |
| type | Collections.list(enumeration) | Concise alternative |
When to Apply
- When the constructor name would be ambiguous (multiple constructors with similar signatures).
- When you want instance control (caching, singletons, interning).
- When you want to return a subtype without exposing its class.
- When you need to hide a complex creation process.
When NOT to Apply
- When subclassing is needed β classes with only private constructors and static factories cannot be subclassed (this is actually sometimes desirable to encourage composition).
- When discoverability matters and javadoc is the primary API surface β constructors appear in a dedicated section; static methods do not.
Java 17 Update
Boolean.valueOf(String) and Boolean.valueOf(boolean) are the preferred factories since Java 9 (the Boolean(String) constructor is deprecated for removal). More broadly, records (Java 16+) generate a canonical constructor automatically, but you can still add static factory methods alongside them:
// Java 16+ record with static factory
public record Point(double x, double y) {
public static Point origin() { return new Point(0, 0); }
public static Point fromPolar(double r, double theta) {
return new Point(r * Math.cos(theta), r * Math.sin(theta));
}
}List.of(), Set.of(), Map.of() (Java 9+) are canonical examples of static factory methods β they return unmodifiable implementations whose concrete classes are internal.
Item 2: Consider a Builder When Faced with Many Constructor Parameters
The Problem
When a class has many parameters (especially optional ones), constructors become unreadable and error-prone. The telescoping constructor pattern (providing a chain of constructors with increasing numbers of parameters) scales poorly. The JavaBeans pattern (setters) allows inconsistent state and prevents immutability.
// BAD β telescoping constructor: which int means what?
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
// BAD β JavaBeans: mutable, no atomicity
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
// Any thread can see a partially-constructed objectThe Solution
Use the Builder pattern: the client calls a static factory (or constructor) to get a builder object, sets optional parameters on it, and finally calls build() to get the constructed object.
// GOOD β Builder pattern
public final class NutritionFacts {
private final int servingSize; // required
private final int servings; // required
private final int calories; // optional
private final int fat; // optional
private final int sodium; // optional
private final int carbohydrate; // optional
public static final class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters β initialized to defaults
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) { calories = val; return this; }
public Builder fat(int val) { fat = val; return this; }
public Builder sodium(int val) { sodium = val; return this; }
public Builder carbohydrate(int val) { carbohydrate = val; return this; }
public NutritionFacts build() {
// Validate invariants here before constructing
if (calories < 0) throw new IllegalArgumentException("Negative calories");
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
// Usage β reads like documentation
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();Hierarchical Builders
The Builder pattern works well with class hierarchies using a recursive type parameter (simulated self-type idiom):
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self(); // covariant return β no cast needed by subclasses
}
abstract Pizza build();
protected abstract T self(); // subclass returns this
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static final class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) { this.size = Objects.requireNonNull(size); }
@Override public NyPizza build() { return new NyPizza(this); }
@Override protected Builder self() { return this; }
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
// Usage: covariant return β no cast needed
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE)
.addTopping(ONION)
.build();Why This Works
- Immutability: the target object is constructed all at once; no partially-constructed state is visible.
- Readability: each setter call is a named parameter.
- Safety: validation happens in
build(), before the object is ever created. - Fluent API: method chaining fits naturally with modern IDE support.
When to Apply
- Four or more constructor parameters, especially if many are optional.
- When the class will be subclassed (hierarchical builders).
- When you need to enforce invariants across parameters.
When NOT to Apply
- Simple classes with one or two parameters β a constructor is fine.
- Performance-critical code where object allocation is a measured bottleneck (though the builder itself is small and short-lived).
Java 17 Update
For simple data carriers, records (Java 16+) eliminate the boilerplate entirely β you get a canonical constructor, accessors, equals, hashCode, and toString for free:
// Java 16+: record replaces value class with builder entirely for simple cases
public record Point(double x, double y) {}
// If you need a builder for a record (e.g., optional fields),
// use Lombok @Builder or write it manually β but often just use multiple
// static factories or a compact canonical constructor with defaults.For complex domain objects that are not simple data carriers, the Builder pattern remains the right choice in Java 17+.
Lombok shortcut (common in production codebases):
@Builder
@Value // immutable
public class NutritionFacts {
int servingSize;
int servings;
@Builder.Default int calories = 0;
@Builder.Default int fat = 0;
}Item 3: Enforce the Singleton Property with a Private Constructor or an Enum Type
The Problem
A singleton is a class that is instantiated exactly once. Naive implementations can be broken via reflection (setAccessible(true)) or serialization (which creates a new instance on deserialization).
// BAD β public field singleton: reflection can break it
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
// BAD β static factory singleton: same reflection vulnerability,
// and serialization creates a second instance unless you add readResolve()
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
}The Solution
Use a single-element enum β it is concise, handles serialization automatically, and provides an iron-clad guarantee against multiple instantiation even in the face of reflection.
// BEST β enum singleton
public enum Elvis {
INSTANCE;
private String name = "Elvis Presley";
public void leaveTheBuilding() {
System.out.println(name + " has left the building.");
}
}
// Usage:
Elvis.INSTANCE.leaveTheBuilding();If you must use the static factory approach (e.g., the singleton needs to extend a superclass), add readResolve() to preserve the singleton guarantee during deserialization:
// Static factory singleton with serialization safety
public class Elvis implements Serializable {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
// Preserve singleton property during deserialization
private Object readResolve() {
return INSTANCE;
}
}And to guard against reflection attacks on the private constructor:
private Elvis() {
if (INSTANCE != null) {
throw new IllegalStateException("Singleton already constructed");
}
}Why This Works
- The JVM guarantees that each enum value is instantiated exactly once per classloader.
- Enum serialization is handled specially by the platform β enum constants are serialized by name and deserialized to the same constant;
readResolveis automatic. - Reflection cannot be used to call an enum constructor β the JVM prevents it.
When to Apply
- Application-wide services with no state or immutable state (logging facade, configuration holder, registry).
- When the singleton must be serializable.
When NOT to Apply
- When testability matters β singletons are hard to mock. Prefer dependency injection (ch01-creating-destroying-objects > Item 5).
- When the class needs to extend a superclass (enums cannot extend classes, only implement interfaces).
- When the singleton concept depends on context (e.g., βone per tenantβ) β this is not a singleton, use a factory or registry.
Java 17 Update
No fundamental change. The enum singleton idiom remains the canonical recommendation. However, modern applications overwhelmingly use dependency injection frameworks (Spring, Guice, Micronaut) to manage object lifecycle, which makes hand-rolled singletons less common β the container manages the single instance.
Item 4: Enforce Noninstantiability with a Private Constructor
The Problem
Utility classes (classes that group static methods and static fields, like java.util.Arrays or java.util.Collections) should not be instantiated. Without action, the compiler provides a public no-argument constructor, and the class appears instantiable.
// BAD β utility class with no enforcement: user can write new MathUtils()
public class MathUtils {
public static int square(int n) { return n * n; }
// Compiler silently adds: public MathUtils() {}
}
// BAD β abstract class does not prevent instantiation (subclass can be instantiated)
public abstract class MathUtils {
public static int square(int n) { return n * n; }
}The Solution
Include a private constructor that throws an exception. A comment explains the intent to future readers.
// GOOD β utility class, noninstantiable
public final class MathUtils {
// Suppress default constructor β this class is not meant to be instantiated.
private MathUtils() {
throw new AssertionError("MathUtils is a utility class and cannot be instantiated");
}
public static int square(int n) { return n * n; }
public static double hypotenuse(double a, double b) { return Math.sqrt(a*a + b*b); }
}Why This Works
- A private constructor cannot be called from outside the class.
- The
AssertionErrorguards against accidental calls from within the class. - Making the class
final(optional) prevents subclassing, which is a secondary concern. - The thrown exception also appears in any accidental reflection-based call.
When to Apply
Any class whose purpose is grouping static utilities. Common examples: Arrays, Collections, Objects, Math, Files, Paths.
When NOT to Apply
If the class has any instance state or instance methods, it is not a utility class and should have a normal constructor.
Java 17 Update
No change β the idiom is identical. Note that static methods on interfaces (Java 8+) and private interface methods (Java 9+) offer an alternative for grouping related utilities when an interface is already central to the design:
// Java 9+: private method in interface (for internal reuse)
public interface Validator<T> {
boolean validate(T t);
static Validator<String> nonEmpty() {
return s -> !s.isEmpty();
}
// private helper β not part of the public API
private static void logFailure(Object value) { ... }
}Item 5: Prefer Dependency Injection to Hardwiring Resources
The Problem
Classes that depend on underlying resources (spell checker β dictionary, HTTP client β host) should not create those resources themselves, and should not use static utility methods or singletons to access them. Hardwiring makes classes untestable, inflexible, and not thread-safe when different callers need different configurations.
// BAD β static utility: hardwired to a single dictionary, untestable
public class SpellChecker {
private static final Lexicon dictionary = new EnglishDictionary();
private SpellChecker() {} // noninstantiable
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
// BAD β singleton: same problem as above
public class SpellChecker {
public static final SpellChecker INSTANCE = new SpellChecker();
private final Lexicon dictionary = new EnglishDictionary();
private SpellChecker() {}
public boolean isValid(String word) { ... }
}The Solution
Use constructor injection β pass the resource into the constructor. Optionally, accept a factory (a Supplier<T>) for lazy or repeated creation.
// GOOD β constructor injection
public class SpellChecker {
private final Lexicon dictionary;
// Any Lexicon implementation can be injected: English, French, test stub
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
// GOOD β factory injection using Supplier (bounded wildcard for flexibility)
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Supplier<? extends Lexicon> dictionaryFactory) {
this.dictionary = dictionaryFactory.get();
}
}
// Usage
Lexicon english = new EnglishDictionary();
SpellChecker checker = new SpellChecker(english);
// Test usage with a mock
SpellChecker testChecker = new SpellChecker(new MockDictionary());Why This Works
- The class is decoupled from the resource implementation.
- Testing is trivial β inject a mock or stub.
- Thread safety is achievable β each thread can have its own instance with its own resource.
- Conforms to the Dependency Inversion Principle (depend on abstractions, not concretions).
When to Apply
- Any class whose behavior depends on one or more underlying resources.
- Any class you intend to unit test.
- Any class that might need different resource configurations in different contexts.
When NOT to Apply
- Truly stateless utility classes with no external resource dependencies.
- When DI frameworks (Spring, Guice) are present β let the framework manage injection rather than doing it manually.
Java 17 Update
No language change, but the ecosystem has matured around this pattern. Springβs @Autowired, Guiceβs @Inject (JSR-330), and CDI all implement constructor injection as the preferred style. Records (Java 16+) make constructor injection even cleaner, since the canonical constructor is the only constructor and takes all fields as parameters:
// Java 16+: record with injected dependency
public record SpellChecker(Lexicon dictionary) {
public SpellChecker {
Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
}Item 6: Avoid Creating Unnecessary Objects
The Problem
Creating objects is not free. Some objects are cheap, but repeatedly creating equivalent immutable objects, reusing a pattern-based check without compiling the pattern, or (worst) autoboxing primitives in a hot loop, carries real cost.
// BAD β creates a new String object every time (though JVM may intern it)
String s = new String("bikini"); // DON'T DO THIS β "bikini" is already a String literal
// BAD β Pattern is compiled on every call β hideously expensive
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
// BAD β autoboxing penalty β creates ~2^31 Long objects!
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i; // autoboxes i to Long on every iteration
}The Solution
Reuse immutable objects and avoid inadvertent autoboxing.
// GOOD β string literal is interned; same object returned from the pool
String s = "bikini"; // no 'new' needed
// GOOD β compile the Pattern once as a static final field
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches(); // reuses compiled pattern
}
}
// GOOD β use primitive long, not boxed Long
long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i; // no boxing
}Caching with a lazy-initialized field:
// Cache expensive computation β initialize on first use
public class CheapDate {
private final Date start;
private Date period; // lazily initialized
private static final Date START_OF_PERIOD;
static {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
START_OF_PERIOD = gmtCal.getTime();
}
boolean isInvalid() {
return start.compareTo(START_OF_PERIOD) < 0;
}
}Why This Works
String.matches()internally creates aPatternobject on every call. Pre-compiling and caching thePatternamortizes the cost.- Autoboxing converts between primitives and their boxed equivalents silently β prefer primitives everywhere, especially in loops.
- Immutable objects (strings, boxed primitives via
valueOf) are safe to share and cache.
Important Distinction
This item is about avoiding unnecessary creation, not βavoid object creation at allβ. Creating necessary objects is fine β modern JVMs handle short-lived objects efficiently via generational GC. Do not create object pools for cheap objects just to save allocation.
When to Apply
- Hot code paths where profiling confirms allocation pressure.
- Expensive-to-create objects (compiled patterns, database connections, parsed config).
- Autoboxed primitives in loops.
When NOT to Apply
- Defensive copies β always make them for mutable objects (Item 50). Do not sacrifice correctness for performance.
- When readability would suffer meaningfully.
Java 17 Update
List.of(), Set.of(), Map.of() (Java 9+) return cached unmodifiable collections for small sizes, so prefer them over manually constructed equivalents:
// Java 9+: no need to create mutable then wrap
List<String> old = Collections.unmodifiableList(Arrays.asList("a", "b", "c"));
List<String> modern = List.of("a", "b", "c"); // cached, unmodifiableSwitch expressions (Java 14+) and instanceof pattern matching (Java 16+) can reduce temporary object creation in branching logic.
Item 7: Eliminate Obsolete Object References
The Problem
Javaβs garbage collector does not free objects that are still referenced. A subtle class of bugs β memory leaks β arises when a class manages its own memory or caches objects without a policy for eviction, keeping obsolete references alive.
// BAD β naive stack implementation with a memory leak
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
return elements[--size]; // BUG: elements[size] still holds a reference!
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}After a pop, elements[size] still holds a reference to the popped object. The GC can never collect it.
The Solution
Null out obsolete references explicitly when you know an element slot is no longer live.
// GOOD β null out obsolete reference on pop
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}Common Sources of Memory Leaks
| Source | Fix |
|---|---|
| Self-managed memory (arrays, etc.) | Null out stale elements |
| Caches | Use WeakHashMap, or background thread eviction, or LinkedHashMap.removeEldestEntry() |
| Listeners and callbacks | Store weak references, or deregister explicitly |
| Static fields holding large objects | Review lifecycle; use scoped containers |
// Cache that doesn't prevent GC β use WeakHashMap if keys have natural lifecycle
Map<CacheKey, ExpensiveObject> cache = new WeakHashMap<>();
// Cache with time-based eviction using LinkedHashMap
private static final int MAX_ENTRIES = 100;
Map<K, V> cache = new LinkedHashMap<>(MAX_ENTRIES, 0.75f, true) {
@Override protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > MAX_ENTRIES;
}
};Why This Works
- Nulling out the reference allows the GC to collect the object.
WeakHashMapstores keys as weak references β when the key has no other strong references, the entry is collected automatically.- This should be done judiciously β nulling out references is an exception, not a rule. When a variable goes out of scope, its reference is eliminated automatically.
When to Apply
- Classes that manage their own memory (arrays, object pools).
- Classes that maintain caches with no external eviction policy.
- Classes that register listeners or callbacks.
When NOT to Apply
- Normal local variables β let them go out of scope naturally.
- Routinely nulling every reference is noise that obscures real intent.
Java 17 Update
No language change. However, java.lang.ref.Cleaner (Java 9+) provides a better alternative to finalizers for cleaning up resources when objects are GCβd. For cache management, libraries like Caffeine (with weak/soft keys and time-based eviction) are the standard production tool.
Item 8: Avoid Finalizers and Cleaners
The Problem
Finalizers (finalize() method) are unpredictable, often dangerous, and generally unnecessary. The JVM makes no guarantee about when β or even whether β a finalizer runs. Cleaners (java.lang.ref.Cleaner, Java 9+) are less dangerous than finalizers (they run in a dedicated thread and donβt risk exceptions propagating), but they are still unreliable for time-sensitive resource release.
// BAD β relying on finalizer to close a file
public class ResourceHolder {
private final FileInputStream stream;
public ResourceHolder(String path) throws IOException {
stream = new FileInputStream(path);
}
@Override
protected void finalize() throws Throwable {
try {
stream.close(); // may never run, or run 10 GC cycles later
} finally {
super.finalize();
}
}
}Problems with finalizers and cleaners:
- No timing guarantee: the JVM schedules them when it wishes, which may be long after the object becomes unreachable.
- Performance cost: objects with finalizers take much longer to be collected (two GC cycles minimum).
- Exceptions swallowed: an exception thrown during finalization is ignored with no stack trace.
- Security attack vector: a malicious subclass can override
finalize()to resurrect the object. - Thread uncertainty: cleaner threads are daemon threads and may not run before JVM exits.
The Solution
Have your class implement AutoCloseable, provide an explicit close() method, and expect clients to use try-with-resources.
// GOOD β AutoCloseable with explicit close
public class ResourceHolder implements AutoCloseable {
private final FileInputStream stream;
private boolean closed = false;
public ResourceHolder(String path) throws IOException {
stream = new FileInputStream(path);
}
@Override
public void close() {
if (!closed) {
try {
stream.close();
} catch (IOException e) {
throw new RuntimeException("Failed to close stream", e);
} finally {
closed = true;
}
}
}
// Optionally add a cleaner as a safety net (not primary release mechanism)
// See Java 9+ Cleaner API for the pattern
}
// Client code
try (ResourceHolder rh = new ResourceHolder("data.txt")) {
// use rh
} // close() is called automaticallyLegitimate uses for cleaners (safety net only):
// ACCEPTABLE β cleaner as safety net, not primary close mechanism
public class Room implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
// State must not reference Room (to avoid preventing GC)
private static class State implements Runnable {
int numJunkPiles;
State(int numJunkPiles) { this.numJunkPiles = numJunkPiles; }
@Override public void run() {
System.out.println("Cleaning room"); // only if close() wasn't called
numJunkPiles = 0;
}
}
private final State state;
private final Cleaner.Cleanable cleanable;
public Room(int numJunkPiles) {
state = new State(numJunkPiles);
cleanable = cleaner.register(this, state); // state must NOT reference this
}
@Override public void close() {
cleanable.clean(); // primary path
}
}Why This Works
AutoCloseable+ try-with-resources is deterministic βclose()is called immediately when the block exits, whether normally or via exception.- Cleaners as a safety net emit a log warning if the client forgot to call
close()β helpful during development.
When to Apply
- Any class that holds a native resource (file handle, socket, DB connection) must implement
AutoCloseable.
When NOT to Apply
- Do not use finalizers as the primary cleanup mechanism ever.
- Cleaners are acceptable only as a safety net (belt-and-suspenders after
close()).
Java 17 Update
finalize() is deprecated for removal since Java 9. In Java 18, Object.finalize() was deprecated with forRemoval = true. Use java.lang.ref.Cleaner (Java 9+) as a safety net if needed, but AutoCloseable + try-with-resources is the primary idiom.
Item 9: Prefer try-with-resources to try-finally
The Problem
When multiple resources must be closed, try-finally blocks nest awkwardly, and the exception from finally can suppress and hide the more useful exception from the try block β making debugging very difficult.
// BAD β try-finally: exception from close() silently suppresses exception from read()
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine(); // if this throws...
} finally {
br.close(); // ...and this also throws, the readLine exception is lost forever
}
}
// MUCH WORSE β two resources nested: ugly, exception-suppressing, hard to read
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}The Solution
Use try-with-resources. Any object implementing AutoCloseable (or its subinterface Closeable) can be used. Resources are closed in reverse order of declaration, and if both the body and the close throw, the close exception is suppressed (accessible via Throwable.getSuppressed()) and the original exception propagates β exactly what you want.
// GOOD β try-with-resources: clean, exception-preserving
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine(); // original exception propagates; close() exception is suppressed
}
}
// GOOD β multiple resources: declared in order, closed in reverse
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
// With catch and finally β still works
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}Accessing suppressed exceptions:
try (MyResource r = new MyResource()) {
r.doSomething(); // throws IOException A
} catch (IOException e) {
// e is IOException A
Throwable[] suppressed = e.getSuppressed(); // contains close() exception if any
for (Throwable t : suppressed) {
System.err.println("Suppressed: " + t);
}
}Why This Works
- The compiler desugars try-with-resources into bytecode that properly handles the exception suppression logic.
- Resources are always closed, even when the body throws.
- The most informative exception (from the body) is the one that propagates.
- Code is significantly more readable and less nested.
When to Apply
Always β when working with any AutoCloseable resource, try-with-resources is always preferable to try-finally.
When NOT to Apply
There is no case where try-finally is preferable to try-with-resources when AutoCloseable resources are involved.
Java 17 Update
Java 9 introduced effectively-final variables in try-with-resources β you can use a variable declared outside the try block as long as it is effectively final:
// Java 9+: effectively final variable in try-with-resources
void processResource(BufferedReader reader) throws IOException {
// reader is effectively final β can use directly without re-declaring
try (reader) {
System.out.println(reader.readLine());
}
}This avoids the syntactic redundancy of re-declaring a variable just to use it in try-with-resources.
Interview Questions & Exercises
Q1: What are the advantages of static factory methods over constructors, and what are the disadvantages?
Context: Asked at senior Java interviews, API design discussions, and library design reviews.
Answer:
Advantages:
- Named methods β convey intent (e.g.,
BigInteger.probablePrimevsnew BigInteger(...)). - Instance control β can return cached instances, singletons, or immutable equivalents.
- Return subtype β can return any subtype, enabling interface-based APIs. The returned type can vary by call (
EnumSet.ofreturnsRegularEnumSetorJumboEnumSet). - Input-type flexibility β can accept objects of any type and coerce them.
- No requirement to create a new object on each invocation.
Disadvantages:
- Subclassing is impossible if the class has no public/protected constructors. (Debatably good β it forces composition.)
- Hard to discover β IDEs and javadoc highlight constructors specially; static factories do not stand out. Mitigate with naming conventions (
from,of,valueOf,newInstance,getInstance).
Follow-up: βHow does List.of() use static factory methods?β β It returns an implementation-private class (e.g., List12 for 2-element lists), which is more memory-efficient than ArrayList. The caller only sees List<E>.
Q2: When would you use a Builder, and how does it differ from a telescoping constructor or JavaBeans?
Context: Very common at mid-to-senior level; tests knowledge of API design and immutability.
Answer:
- Telescoping constructor: requires one constructor per combination of optional parameters; callers must pass positional arguments β no semantic meaning at the call site; error-prone when parameters have the same type.
- JavaBeans: setter-based; allows inconsistent state during construction; objects cannot be made immutable; not thread-safe during construction.
- Builder: construction is a single atomic step (
build()); named βparametersβ via method chaining; enforces invariants before the object exists; the result can be immutable; scales well with optional parameters and class hierarchies.
// Builder catches misuse at build() time, not runtime
new NutritionFacts.Builder(240, 8).calories(-5).build(); // throws IllegalArgumentExceptionFollow-up: βHow would you implement a Builder for a class hierarchy?β β Use the recursive type parameter Builder<T extends Builder<T>> and the abstract self() method to enable covariant return types and avoid unchecked casts in subclasses.
Q3: What is the best way to implement a singleton in Java, and why?
Context: Classic Java interview question; tests knowledge of enum, reflection, and serialization.
Answer:
The single-element enum is the best approach:
public enum MySingleton {
INSTANCE;
public void doWork() { ... }
}Reasons:
- JVM guarantees a single instance per enum value per classloader.
- Reflection cannot instantiate enum constants β
Constructor.newInstance()on an enum throwsIllegalArgumentException. - Serialization is handled by the platform β
readObjectreturns the existing constant. - No
readResolve()method needed.
The public static final field approach and the static factory approach are vulnerable to reflection attacks (can be hardened by throwing in the constructor on second invocation) and require explicit readResolve() for serialization safety.
Follow-up: βWhen canβt you use an enum singleton?β β When the singleton must extend a non-Enum superclass (enums implicitly extend java.lang.Enum and cannot extend another class). In that case, use the static factory with a readResolve() method and a reflective guard.
Q4: What is a memory leak in Java, and how does the Stack example in Item 7 demonstrate it?
Context: Asked in performance interviews, GC tuning discussions, and senior engineering interviews.
Answer:
A memory leak in Java occurs when objects are kept alive by references that the program will never use again, preventing the GC from collecting them. Unlike C/C++, the leak is not a raw pointer issue β itβs an object reference that outlasts its logical lifetime.
In the Stack example: the elements array always grows but only the slice [0, size) is logically active. When an element is popped, size decrements but elements[size] still holds the reference. The GC cannot collect the popped object because the array (which is long-lived) still references it.
Fix: elements[size] = null; after decrement. The GC can then collect the object.
Three canonical sources of Java memory leaks: (1) self-managed memory, (2) caches without eviction, (3) listener/callback registrations without deregistration.
Follow-up: βWhat tools would you use to diagnose a memory leak?β β VisualVM, JProfiler, Eclipse MAT (Memory Analyzer Tool), jmap -histo, heap dumps analyzed with jhat or MAT, or async-profiler for allocation profiling.
Q5: Why are finalizers dangerous? What should you use instead?
Context: Asked frequently in senior Java interviews; tests GC knowledge and resource management.
Answer:
Finalizers are dangerous because:
- No timing guarantee β the JVM does not guarantee when or whether
finalize()runs. If GC doesnβt run, finalizers donβt run. - Performance β finalized objects take two GC cycles to collect (one to discover, one after finalization).
- Exception suppression β exceptions thrown in
finalize()are silently ignored. - Finalizer attack β a malicious subclass can override
finalize()to resurrect a partially-constructed object, bypassing constructor validation. - Priority inversion β the finalizer thread runs at low priority; a backlog of unfinalized objects can cause
OutOfMemoryError.
Use instead:
AutoCloseable+try-with-resourcesfor deterministic cleanup.java.lang.ref.Cleaner(Java 9+) as a safety net only β log a warning ifclose()was not called.
Follow-up: βWhat is the Cleaner API and how is it better than finalizers?β β Cleaner runs the cleaning action on a dedicated daemon thread when the registered object becomes phantom-reachable. Unlike finalizers, exceptions are contained to the cleaner thread, it has no lock-out issue, and the State object (what gets cleaned) is separate from the referent (preventing resurrection). But it still has no timing guarantee β hence βsafety net only.β
Q6: Explain try-with-resources. What happens when both the body and the close() method throw exceptions?
Context: Asked to verify understanding of exception handling subtleties in Java.
Answer:
try-with-resources (Java 7+) automatically calls close() on any AutoCloseable declared in the resource specification, in reverse declaration order, when the try block exits (normally, via return, or via exception).
When both the body throws exception A and close() throws exception B:
- Exception A propagates (the body exception takes priority β itβs usually more informative).
- Exception B is suppressed and attached to A.
- Callers can retrieve B via
e.getSuppressed().
With try-finally, the behavior is the opposite and worse: B propagates and A is completely lost β it cannot be retrieved, making debugging very difficult.
// To access suppressed exceptions:
try (Resource r = new Resource()) { r.doWork(); }
catch (Exception e) {
Arrays.stream(e.getSuppressed()).forEach(System.err::println);
}Follow-up: βCan you use try-with-resources with resources declared outside the try block?β β Yes, since Java 9, if the variable is effectively final.
Q7: What is the difference between static factory methods and the Factory Method design pattern?
Context: Tests precision of language; many candidates conflate the two.
Answer:
These are different things:
- Static factory method (Blochβs Item 1): a static method on the class itself that returns an instance of that class (or subtype).
Boolean.valueOf(true),List.of(1,2,3). It is a naming and API design convention, not a GoF pattern. - Factory Method pattern (GoF): a virtual method on an abstract class or interface that subclasses override to provide different implementations. The method is instance-based and part of a class hierarchy (
Creator.createProduct()overridden byConcreteCreator).
The GoF Abstract Factory pattern is yet another thing β an interface providing a family of factory methods, allowing you to swap entire product families.
Bloch uses βstatic factory methodβ specifically to mean the simple static-method idiom β the GoF Factory Method pattern is a heavier architectural pattern.
Follow-up: βGive an example of the GoF Factory Method in the Java standard library.β β java.util.Iterator returned by Collection.iterator() is an instance-based factory method. Calendar.getInstance() is a static factory method (Blochβs sense).
Q8: How does autoboxing relate to Item 6, and what is the performance impact?
Context: Performance-focused interviews; tests understanding of Java primitive/object duality.
Answer:
Autoboxing silently converts primitives to their boxed equivalents (int β Integer, long β Long, etc.) when the context requires an object. This creates a new object on the heap for each conversion (unless the value is in the cache range: Integer caches -128 to 127).
In a hot loop with a boxed accumulator:
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i; // unboxes sum, adds i, autoboxes result β ~2 billion Long objects!
}Changing Long sum to long sum eliminates ~2 billion object allocations and can be orders of magnitude faster.
Rules of thumb:
- Prefer primitives over boxed primitives.
- Watch for unintentional autoboxing in loops, stream operations, and map operations.
- Use
int[]overList<Integer>,long[]overList<Long>in performance-critical code. - The compiler and JIT can optimize some autoboxing, but should not be relied upon in tight loops.
Follow-up: βWhen must you use boxed primitives?β β When a generic type is required (Map<String, Integer>, not Map<String, int>); when null must be representable; when reflection is used; when calling APIs that require objects.
Key Takeaways
- Static factory methods should be the default for complex instantiation β they are more expressive, more flexible, and enable instance control. Learn the naming conventions.
- The Builder pattern is the right answer to telescoping constructors and the JavaBeans pattern when there are many (4+) parameters, especially optional ones. Builders also compose well with class hierarchies.
- Enum singletons are the safest way to implement a singleton β they survive reflection and serialization attacks automatically.
- Utility classes should always have a private constructor that throws
AssertionErrorto prevent accidental instantiation. - Constructor injection (dependency injection) makes classes flexible, testable, and decoupled β never hardwire resources via static fields or singletons in classes that need to be tested.
- Avoid unnecessary object creation, especially: compiled patterns created on every call, autoboxing in loops, and
new String("...")on string literals. Pre-compute and cache. - Memory leaks in Java come from obsolete references in self-managed arrays, unbounded caches, and un-deregistered listeners. Null out stale references; use
WeakHashMapfor caches. - Never use finalizers as the primary cleanup mechanism. They are unpredictable, slow, and swallow exceptions. Use
AutoCloseable+ try-with-resources instead. - Cleaners (Java 9+) are marginally safer than finalizers but still non-deterministic β use them only as a safety net behind
AutoCloseable.close(). - try-with-resources is always preferable to try-finally for
AutoCloseableresources β it is more concise, exception-preserving (suppresses close exceptions, keeps body exception), and correct.
Last Updated: 2026-05-10