skip to content
everything.effectiveJava.creatingAndDestroyingObjects

creating and destroying objects

items 1–9: factories, builders, singletons, and not leaking.

chapter
chapter 16.01
reading time
12 min read

items 1 to 9 are about how objects come into the world and how they leave it. give creation a name (static factories), give long parameter lists a builder, don't create what you can reuse, and let go of references you're done with. cleanup is try-with-resources, never a finalizer.

the nine items, at a glance. amber marks the ones modern java changed. click a card to jump to it.

making objects

item 1 · named factories beat constructors

a constructor has one name (the class's) and always returns a brand new object of exactly that class. a static method that returns an instance has none of those limits: it can say what it does, hand back a cached object, or return any subtype it likes.

don'twhat does true mean?
java
var t = new Temp(70.7, true);
dothe name says it
java
var t = Temp.fahrenheit(70.7);
var zero = Temp.FREEZING; // cached

the JDK is full of these. and the "can return any subtype" part isn't theory, you can watch it happen:

Main.java
void main() {
    IO.println(List.of(1).getClass().getSimpleName());       // a tiny class for 1-2 items
    IO.println(List.of(1, 2, 3).getClass().getSimpleName()); // a general one for more
    IO.println(Integer.valueOf(127) == Integer.valueOf(127)); // same cached object
}
output
List12
ListN
true

same method, two hidden classes, and you never had to know. the cost: a class with only private constructors can't be subclassed, and factories don't stand out in the javadoc the way constructors do (hence the naming habits: of, from, valueOf, newInstance, getInstance).

item 2 · long parameter lists want a builder

four or more parameters, several optional? telescoping constructors (one per combination) force positional arguments nobody can read. setters (the JavaBeans way) leave the object half-built and mutable. a builder gives you named, optional arguments and an immutable result.

you want =
350ml
telescoping constructor5 args
new Coffee(350, 1, "none", 0, true)
3 values you had to pass anyway, in exactly the right order. the compiler only checks the types.
builder1 call
Coffee.ofSize(350)
.iced()
.build();
toggle the options you care about. with a telescoping constructor you pay for every argument up to the last one you want, including the ones you didn't ask for. the builder only mentions your choices.
Main.java
final class Coffee {
    private final int ml, shots;
    private final String milk;
    private final boolean iced;
 
    private Coffee(Builder b) { ml = b.ml; shots = b.shots; milk = b.milk; iced = b.iced; }
 
    static Builder ofSize(int ml) { return new Builder(ml); }
 
    static final class Builder {
        private final int ml;          // required
        private int shots = 1;         // optional, with defaults
        private String milk = "none";
        private boolean iced;
 
        private Builder(int ml) { this.ml = ml; }
        Builder shots(int n) { shots = n; return this; }
        Builder milk(String m) { milk = m; return this; }
        Builder iced() { iced = true; return this; }
 
        Coffee build() {
            if (shots < 1 || shots > 4) throw new IllegalArgumentException("shots: " + shots);
            return new Coffee(this); // validate once, on the finished set of values
        }
    }
 
    @Override public String toString() {
        return ml + "ml, " + shots + " shot(s), milk=" + milk + (iced ? ", iced" : "");
    }
}
 
public class Main {
    public static void main(String[] args) {
        System.out.println(Coffee.ofSize(350).shots(2).milk("oat").iced().build());
    }
}
output
350ml, 2 shot(s), milk=oat, iced

the JDK's own: HttpClient.newBuilder(), HttpRequest.newBuilder(), Thread.ofVirtual().name("w-", 0), StringBuilder (a builder for strings, sort of).

item 3 · singletons: a private constructor or an enum

two classic shapes (a public static final field, or a static getInstance()), both with a private constructor. the catch: reflection can call a private constructor, and plain serialization creates a second copy on read. a single-element enum shuts both doors.

Main.java
import java.lang.reflect.Constructor;
 
class Registry {
    static final Registry INSTANCE = new Registry();
    private Registry() {}
}
 
enum Config { INSTANCE }
 
public class Main {
    public static void main(String[] args) throws Exception {
        Constructor<Registry> c = Registry.class.getDeclaredConstructor();
        c.setAccessible(true);
        System.out.println(c.newInstance() == Registry.INSTANCE); // a second "singleton"
 
        Constructor<Config> e = Config.class.getDeclaredConstructor(String.class, int.class);
        e.setAccessible(true);
        e.newInstance("INSTANCE", 0); // the JVM refuses
    }
}
output, from jdk 25
false
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects

this one needs a real JVM (reflection), so it doesn't run in the browser. copy it into java Main.java to try it.

the enum way can't extend a class (it already extends Enum), which is the only reason not to use it. lazy versions, double-checked locking and the holder idiom are in singletons and DCL.

item 4 · utility classes get a private constructor

a class that's just static methods (Math, Collections, Arrays) should not be instantiable. making it abstract doesn't work: anyone can subclass it and instantiate the subclass. a private constructor does.

java
public final class Units {
    private Units() { throw new AssertionError("no instances"); }
 
    public static double kmToMiles(double km) { return km * 0.621371; }
}

item 5 · pass dependencies in, don't hardwire them

a class that builds its own resources (a dictionary, a clock, a database) can only ever use that one, and can't be tested without it. take the resource in the constructor instead. that's all dependency injection is; frameworks just automate the wiring.

don'twelded to one dictionary
java
class SpellChecker {
    final Dictionary dict =
        new EnglishDictionary();
 
    boolean ok(String w) {
        return dict.has(w);
    }
}
doany dictionary, including a fake one in tests
java
class SpellChecker {
    final Dictionary dict;
 
    SpellChecker(Dictionary dict) {
        this.dict = dict;
    }
 
    boolean ok(String w) {
        return dict.has(w);
    }
}

a variant: pass a factory, typically a Supplier<? extends Dictionary>, when the class needs to make several. the JDK's version of this lesson is java.time.Clock: code that takes a Clock can be tested at any time of day.

not making objects

item 6 · don't make objects you don't need

some objects are expensive to make and safe to reuse. String.matches(regex) compiles the regex into a Pattern on every call. accidental autoboxing (Long total instead of long total) can make a new Long per iteration.

1M × s.matches(re)56 ms
1M × compiled Pattern16 ms
100M × Long total += i38 ms
100M × long total += i26 ms
best of 7 runs, jdk 25 on an arm mac, after warm-up. plain System.nanoTime, not JMH, so read the ratios, not the digits. red is the version that makes objects it doesn't need.
don'tcompiles the regex on every call
java
static boolean isHex(String s) {
    return s.matches("[0-9a-f]+");
}
docompile once, reuse forever
java
static final Pattern HEX =
    Pattern.compile("[0-9a-f]+");
 
static boolean isHex(String s) {
    return HEX.matcher(s).matches();
}
one level deeper:why the Long loop was only 1.5x slower+

because the JIT saw that the boxed total never escapes the method and removed most of the boxing (escape analysis, see JIT optimizations). run the same loop with -Xint (interpreter only) and it's 6,779 ms vs 449 ms, about 15x. the JIT can save you in a tight loop. it can't when the box is stored in a field, a collection, or passed somewhere it can't see.

the flip side matters just as much: don't pool small objects to "help the GC". allocation is a pointer bump in a thread-local buffer (TLABs) and short-lived garbage is nearly free to collect. pool only what's truly expensive, like database connections.

item 7 · let go of references you're done with

garbage collection doesn't mean memory leaks are impossible. if your code keeps a reference to something it will never use again (an "obsolete reference"), the GC has to assume it's live. the classic spot: a class that manages its own array of references.

pop() =
Object[] itemssize = 0null0null1null2null3null4null5new Stack<>()

an empty stack: an array of 6 slots and size = 0.

step 1/6
step through it, then flip pop(). the slots below size are the stack. a reference left above it is obsolete: logically dead, but it keeps its object alive.
Main.java
import java.lang.ref.WeakReference;
import java.util.Arrays;
 
class Stack<T> {
    private Object[] items = new Object[8];
    private int size;
 
    void push(T x) {
        if (size == items.length) items = Arrays.copyOf(items, size * 2);
        items[size++] = x;
    }
 
    @SuppressWarnings("unchecked")
    T pop() {
        T x = (T) items[--size];
        // forgot: items[size] = null;
        return x;
    }
}
 
public class Main {
    public static void main(String[] args) {
        var stack = new Stack<byte[]>();
        stack.push(new byte[1 << 20]);
        var watch = new WeakReference<>(stack.pop()); // we drop our only strong ref
        System.gc();
        System.out.println("still alive: " + (watch.get() != null));
    }
}
output
still alive: true

add items[size] = null; and it prints still alive: false. don't go nulling every local, though. a local goes out of scope on its own. null out only when a class manages its own memory, and watch the other two usual suspects: caches (use a size bound or WeakHashMap) and listeners you register but never remove. more in memory leaks.

destroying objects

item 8 · finalizers and cleaners are not destructors

a finalizer might run late, in some random thread, or never. it slows allocation, it can resurrect objects, and an exception inside one is silently dropped. cleaners (java.lang.ref.Cleaner, java 9) fix some of that, but they're still unpredictable. neither is how you close things.

the right answer: implement AutoCloseable and let callers use try-with-resources. a cleaner is acceptable only as a safety net for when a caller forgets, typically around native memory.

Main.java
import java.lang.ref.Cleaner;
 
final class NativeBuffer implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();
 
    // the cleanup action must not reference the NativeBuffer itself,
    // or the buffer can never become unreachable
    private record Release(long address) implements Runnable {
        public void run() { System.out.println("freed " + address); }
    }
 
    private final Cleaner.Cleanable cleanable;
 
    NativeBuffer(long address) { cleanable = CLEANER.register(this, new Release(address)); }
 
    @Override public void close() { cleanable.clean(); } // runs Release at most once
}
 
public class Main {
    public static void main(String[] args) {
        try (var buf = new NativeBuffer(0xCAFE)) {
            System.out.println("using it");
        }
        System.out.println("done");
    }
}
output
using it
freed 51966
done

item 9 · try-with-resources, always

try/finally gets ugly with two resources, and it has a real bug: if the body throws and then close() throws too, the second exception replaces the first. you get "close failed" in the logs and lose the reason it actually broke.

predict the output
java
class Conn implements AutoCloseable {
    void read() { throw new IllegalStateException("read failed"); }
    public void close() { throw new IllegalStateException("close failed"); }
}
 
void main() {
    Conn c = new Conn();
    try {
        c.read();
    } finally {
        c.close();
    }
}
Main.java
class Conn implements AutoCloseable {
    void read() { throw new IllegalStateException("read failed"); }
    public void close() { throw new IllegalStateException("close failed"); }
}
 
public class Main {
    public static void main(String[] args) {
        try (Conn c = new Conn()) {
            c.read();
        } catch (IllegalStateException e) {
            System.out.println("caught: " + e.getMessage());
            for (Throwable s : e.getSuppressed()) System.out.println("suppressed: " + s.getMessage());
        }
    }
}
output
caught: read failed
suppressed: close failed

resources close in reverse order of opening, even when one of the closes throws. the whole mechanism is in try-with-resources.

keylines
  • a static factory can have a name, cache, and return any subtype. constructors can't.
  • many optional parameters: builder. all required, plain data: record.
  • the enum singleton is the one reflection and serialization can't break.
  • reuse what's expensive (compiled regexes), never pool what's cheap (small objects).
  • the GC can't free what you still point at. clear slots in classes that manage their own arrays.
  • close with try-with-resources. cleaners are a safety net, finalizers are on their way out.