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.
- item 1named factories beat constructorsList.of, Path.of, records
- item 2long parameter lists want a builderrecords, withers in amber
- item 3singletons: a private constructor or an enum
- item 4utility classes get a private constructor
- item 5pass dependencies in, don't hardwire them
- item 6don't make objects you don't needescape analysis, valhalla
- item 7let go of references you're done with
- item 8finalizers and cleaners are not destructorsfinalization deprecated (JEP 421)
- item 9try-with-resources, alwayseffectively final resources
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.
var t = new Temp(70.7, true);var t = Temp.fahrenheit(70.7);
var zero = Temp.FREEZING; // cachedthe JDK is full of these. and the "can return any subtype" part isn't theory, you can watch it happen:
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
}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.
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());
}
}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.
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
}
}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.
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.
class SpellChecker {
final Dictionary dict =
new EnglishDictionary();
boolean ok(String w) {
return dict.has(w);
}
}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.
System.nanoTime, not JMH, so read the ratios, not the digits. red is the version that makes objects it doesn't need.static boolean isHex(String s) {
return s.matches("[0-9a-f]+");
}static final Pattern HEX =
Pattern.compile("[0-9a-f]+");
static boolean isHex(String s) {
return HEX.matcher(s).matches();
}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.
an empty stack: an array of 6 slots and size = 0.
pop(). the slots below size are the stack. a reference left above it is obsolete: logically dead, but it keeps its object alive.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));
}
}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.
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");
}
}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.
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();
}
}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());
}
}
}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.
- 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.