If a constructor throws, no reference to the partially-built object is returned, so under normal use the object is unreachable and gets garbage-collected — but resources acquired before the throw still leak, and a this-leak before the throw can let an attacker resurrect the half-built instance. The Java security folklore around the "finalizer attack" is the famous edge case.
The normal case: object is garbage
class Connection {
public Connection(String url) {
if (url == null) throw new IllegalArgumentException();
// ...
}
}
try { var c = new Connection(null); }
catch (IllegalArgumentException e) { /* c is never assigned */ }
Because new is atomic — either it returns a reference or it throws — c was never bound. No method can observe the half-built object; the JVM reclaims it on the next GC cycle.
The leak case: resources
If your constructor acquires a resource and then a later step throws, the resource leaks:
class FileSync {
private final FileInputStream in;
private final FileOutputStream out;
public FileSync(Path src, Path dst) throws IOException {
this.in = new FileInputStream(src.toFile()); // opens FD
this.out = new FileOutputStream(dst.toFile()); // may throw
// if 'out' throws, 'in' is open and unreachable -> FD leak
}
}
Two fixes:
// Option A: try/catch in the constructor, close on failure
public FileSync(Path src, Path dst) throws IOException {
FileInputStream tmpIn = new FileInputStream(src.toFile());
try {
this.out = new FileOutputStream(dst.toFile());
} catch (IOException e) {
tmpIn.close();
throw e;
}
this.in = tmpIn;
}
// Option B: static factory + try-with-resources
The this-leak hazard
Anything that publishes this before the constructor finishes is dangerous:
class Listener {
public Listener(EventBus bus) {
bus.register(this); // escape!
// if a later line throws here,
// bus still holds a reference to a half-built Listener
}
}
Even if the constructor throws, bus still has the reference — the object is not garbage. Any subsequent method call sees fields in unexpected states.
The finalizer attack (historical)
Before Java made it harder, an attacker could subclass a sensitive class, override finalize(), and trigger a partial construction:
class Account {
public Account() {
if (!securityCheck()) throw new SecurityException();
}
}
class Evil extends Account {
static Evil stolen;
@Override protected void finalize() { stolen = this; }
}
try { new Evil(); } catch (SecurityException e) { /* ignored */ }
System.gc(); // Evil.finalize runs, stolen now references a half-built Account
Mitigation: declare the class final, declare a final finalizer that does nothing in the parent, or — best — don't use finalizers. Java 9 deprecated them; Java 18 fully sealed off Object.finalize() from being called on objects whose constructor threw.