The main difference between an explicit termination method and finalize()
is that the second one is not guaranteed to be called. It's called eventually during garbage collection which might to be honest never occur. Lets consider the following three classes.
class Foo {
@Override
public void finalize() {
System.out.println("Finalize Foo");
}
}
class Bar implements Closeable {
@Override
public void close() {
System.out.println("Close Bar");
}
}
class Baz implements AutoCloseable {
@Override
public void close() {
System.out.println("Close Baz");
}
}
The first one overrides the finalize()
method inherited from Object
. Foo
and Bar
implement both interfaces which are handled by ARM (Automatic Resource Management).
Foo foo = new Foo();
new Foo();
try (Bar bar = new Bar(); Baz baz = new Baz()) { // this is ARM
System.out.println("termination example");
}
Bar bar = null;
try {
bar = new Bar();
// ...
} finally {
if (bar != null) {
bar.close();
}
}
This example should return:
termination example
Close Baz
Close Bar
Close Bar
The finalize()
method of Foo
gets never called because Foo
is not garbage collected. The JVM has available resources, so for performance optimization it does not perform garbage collecting. Furthermore - if a resource isn't garbage collected despite the fact of finishing the Application. Even the second created instance of Foo
is not Garbage Collected, because there is plenty of resources for the JVM to thrive.
The second one with ARM is a lot better, because it creates both the resources (one implementing java.io.Closeable
and one implementing java.lang.AutoCloseable
, it's worth mentioning that Closeable
extends AutoCloseable
, that's why it's available for ARM). ARM guaranties for both of these resources to be closed, to close one when the other throws and so on. The second one presents something similar to ARM, but saving a lot of unnecessary boilerplate code.
Something making you a better developer:
But it's still not perfect. There is still a burden on the programmer to remember closing the object. The absence of destructors in Java forces the developer either to remember closing the resource, remember to use ARM and so on. There is a good design pattern (good explained by Venkat Subramaniam) - the Loan Pattern
. A simple example of the loan pattern:
class Loan {
private Loan() {
}
public Loan doSomething(int m) {
System.out.println("Did something " + m);
if (new Random().nextBoolean()) {
throw new RuntimeException("Didn't see that commming");
}
return this;
}
public Loan doOtherThing(int n) {
System.out.println("Did other thing " + n);
return this;
}
private void close() {
System.out.println("Closed");
}
public static void loan(Consumer<Loan> toPerform) {
Loan loan = new Loan();
try {
toPerform.accept(loan);
} catch (Exception e) {
e.printStackTrace();
} finally {
loan.close();
}
}
}
You can use it like that:
class Main {
public static void main(String[] args) {
Loan.loan(loan -> loan.doOtherThing(2)
.doSomething(3)
.doOtherThing(3));
}
}
It relieves the developer of the burden of closing the resource, because it has already been handled for him. If one of these methods throws, then it's handled and the developer does not have to bother. The close method and constructor are private to not tempt the developer to use them.