03 Discussing the differences between final, finally, and finalize #
Java has many language elements that may seem similar but have completely different purposes. These elements often become the starting point for interviewers to assess your knowledge mastery.
Today, I want to ask you a classic Java fundamental question: What are the differences between final, finally, and finalize?
Typical Answer #
The keyword final
can be used to modify classes, methods, and variables, each with different meanings. When a class is marked as final
, it means that it cannot be extended or subclassed. A final
variable cannot be modified, and a final
method cannot be overridden.
The keyword finally
, on the other hand, is a mechanism in Java that ensures certain critical code is always executed. We can use try-finally
or try-catch-finally
to perform actions such as closing JDBC connections or releasing locks.
The method finalize
is a method in the base class java.lang.Object
. Its design purpose is to ensure that certain resources of an object are properly disposed before it is garbage collected. However, the finalize
mechanism is now deprecated and not recommended for use, starting from JDK 9.
Analysis of Key Points #
This is a very classic Java basic question. The answer I provided above mainly focuses on syntax and practical usage. However, there are many aspects that can be explored in depth, and the interviewer may also test your understanding of performance, concurrency, object lifecycle, or garbage collection basic processes.
It is recommended to use the final
keyword to clearly indicate the semantics and logical intention of our code. This has been proven to be a good practice in many scenarios, for example:
- We can declare methods or classes as
final
to clearly inform others that these behaviors are not allowed to be modified.
If you have paid attention to the definition or source code of the Java core class library, have you noticed that a considerable part of the classes under the java.lang
package are declared as final class
? The same is true for some basic classes in third-party libraries. This effectively prevents API users from modifying basic functionality, to some extent, this is a necessary means to ensure platform security.
- Using
final
to modify parameters or variables can also clearly avoid programming errors caused by accidental assignment. Some people even recommend declaring all method parameters, local variables, and member variables asfinal
. - The
final
variable has a certain degree of immutability effect, so it can be used to protect read-only data, especially in concurrent programming. Because thefinal
variable cannot be assigned again, which is beneficial to reduce additional synchronization overhead and can also eliminate the need for some defensive copying.
final
may have performance benefits. Many articles or books have introduced that it can improve performance in specific scenarios. For example, using final
may help the JVM to inline methods and can improve the compiler’s ability to perform conditional compilation. To be honest, many similar conclusions are based on assumptions. For example, modern high-performance JVMs (such as HotSpot) do not necessarily rely on final
hints to determine inlining. Similarly, the impact of final
fields on performance is mostly unnecessary to consider.
From the perspective of development practices, I don’t want to overemphasize this point. It is closely related to the implementation of the JVM and is difficult to grasp without verification. My suggestion is that in daily development, unless there are special considerations, it is better not to rely on the so-called performance benefits brought by these small tricks. The program should reflect its semantic purpose. If you are really interested in this aspect, you can consult relevant materials, but don’t forget to validate it.
For finally
, it is enough to know how to use it correctly. It is recommended to use the try-with-resources
statement added in Java 7 for resources that need to be closed, as the Java platform can usually handle exceptional situations better and the amount of code is much smaller. Why not do it?
In addition, I noticed that there are some common questions about finally
(which are also relatively niche), and you should at least have an understanding of them. For example, what will the following code output?
try {
// do something
System.exit(1);
} finally {
System.out.println("Print from finally");
}
The code inside finally
will not be executed, this is a special case.
As for finalize
, we need to be clear that it is not recommended. Industry practices have repeatedly proved that it is not a good approach. In Java 9, Object.finalize()
is even explicitly marked as deprecated! Unless there are specific reasons, do not implement the finalize
method and do not expect to use it for resource reclamation.
Why? Simply put, you cannot guarantee when and how finalize
will be executed. Improper usage can affect performance and cause program deadlock or hang.
Usually, using the try-with-resources
or try-finally
mechanism mentioned above is a very good way to reclaim resources. If extra processing is really needed, you can consider the Cleaner
mechanism provided by Java or other alternative methods. Next, I will introduce more design considerations and practical details.
Knowledge Expansion #
- Note that
final
is not immutable!
In my previous introduction, I mentioned the benefits of final
in practice. It is important to note that final
is not equivalent to immutable. For example, consider the following code:
final List<String> strList = new ArrayList<>();
strList.add("Hello");
strList.add("world");
List<String> unmodifiableStrList = List.of("hello", "world");
unmodifiableStrList.add("again");
The final
keyword only restricts the strList
reference from being reassigned, but it does not affect the behavior of the strList
object. Adding elements or performing other operations is completely normal. If we really want the object itself to be immutable, we need a class that supports immutable behavior. In the example above, the List.of
method creates an immutable List, and attempting to add an element using add
will result in an exception at runtime.
Immutable is a great choice in many scenarios. In a sense, the Java language currently does not have native support for immutability. To implement an immutable class, we need to:
- Declare the class itself as
final
so that others cannot bypass the restrictions by extending it. - Declare all member variables as
private
andfinal
, and do not implement setter methods. - Typically, use deep copying to initialize member variables when constructing objects instead of assigning them directly. This is a defensive measure because you cannot assume that the input object will not be modified by others.
- If you really need to implement getter methods or other methods that may return internal state, use the copy-on-write principle to create private copies.
Are these principles often mentioned in concurrent programming practices? Indeed.
Regarding setter/getter methods, many people like to generate them all at once using an IDE, but it is better to implement them only when necessary.
- Is
finalize
really that terrible?
Earlier, I briefly introduced that finalize
is a widely recognized bad practice. So why does it cause those problems?
The execution of finalize
is associated with garbage collection. Once a non-empty finalize
method is implemented, the recycling of corresponding objects will become significantly slower, as benchmarked with a decrease of approximately 40-50 times.
This is because finalize
is designed to be called before the object is garbage collected, which means that objects with finalize
methods implemented are “special citizens” that the JVM must handle separately. finalize
fundamentally becomes an obstacle to fast recycling and may cause the object to go through multiple garbage collection cycles before it can be reclaimed.
Someone might ask, what if I use System.runFinalization()
to make the JVM more aggressive? It may have some effect, but the problem is that it is unpredictable and not guaranteed, so in essence, it cannot be relied upon. In practice, because finalize
slows down garbage collection, it can lead to a large accumulation of objects, which is a typical cause of OutOfMemoryError (OOM).
From another perspective, we want to ensure resource reclamation because resources are finite. The unpredictable nature of garbage collection time can greatly exacerbate resource consumption. This means that for resources that are highly consumed frequently, do not rely on finalize
to take on the main responsibility of resource release. It is best to either explicitly release resources when they are no longer needed or utilize resource pools for reusability.
finalize
can also mask error messages during resource reclamation. Let’s take a look at the following snippet from the JDK source code, excerpted from java.lang.ref.Finalizer
:
private void runFinalizer(JavaLangAccess jla) {
// ... omitted code
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
jla.invokeFinalize(finalizee);
}
// Clear stack slot containing this variable, to decrease
// the chances of false retention with a conservative GC
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
Combining with the exception handling practices introduced in my previous column, what problems do you think this code will cause?
Yes, you read it right, the Throwable
here is swallowed! This means that once an exception or error occurs, you won’t get any useful information. Besides, Java doesn’t have a good way to handle any information during the finalize phase, otherwise it would be even more unpredictable.
- Is there any mechanism to replace
finalize
?
Currently, the Java platform is gradually replacing the original finalize
implementation with java.lang.ref.Cleaner
. The implementation of Cleaner
uses a phantom reference (PhantomReference
), which is a common post-mortem cleanup mechanism. I will introduce various types of references in Java in the following columns. By using phantom references and reference queues, we can ensure that objects are thoroughly destroyed before performing similar resource cleanup tasks, such as closing file descriptors (limited resources in the operating system). It is lighter and more reliable than finalize
.
Learning from the lessons of finalize
, each operation of Cleaner
is independent, and it has its own running thread, so it can avoid unexpected deadlocks and other problems.
In practice, we can build a Cleaner
for our own module and implement the corresponding cleanup logic. The following is an example program provided by the JDK:
public class CleaningExample implements AutoCloseable {
// A cleaner, preferably one shared within a library
private static final Cleaner cleaner = <cleaner>;
static class State implements Runnable {
State(...) {
// initialize State needed for cleaning action
}
public void run() {
// cleanup action accessing State, executed at most once
}
}
private final State;
private final Cleaner.Cleanable cleanable
public CleaningExample() {
this.state = new State(...);
this.cleanable = cleaner.register(this, state);
}
public void close() {
cleanable.clean();
}
}
Note that from the perspective of predictability, the degree of improvement brought by Cleaner
or phantom references is still limited. If phantom references accumulate for various reasons, problems will still occur. Therefore, Cleaner
is suitable as a last resort measure, rather than relying solely on it for resource reclamation. Otherwise, we will have to go through the nightmare of finalize
again.
I have also noticed that many third-party libraries directly customize resource collection using phantom reference mechanisms. For example, the widely used MySQL JDBC driver mysql-connector-j
uses the phantom reference mechanism. Phantom references can also perform chain-like dependency actions, such as scenarios involving total control, ensuring that only when the connection is closed and the corresponding resources are reclaimed can the connection pool create new connections.
In addition, if this type of code accidentally adds a strong reference relationship to a resource, it will result in a circular reference relationship. The MySQL JDBC mentioned earlier has this problem in specific modes, resulting in memory leaks. In the example code above, State
is defined as static
to avoid the implicit strong reference to the external object that is typically present in ordinary inner classes, because that would prevent the external object from entering the phantom reachable state.
Today, I have analyzed final
, finally
, finalize
from the syntax perspective, and gradually delved into security, performance, garbage collection, and other aspects to discuss the precautions in practice. I hope this helps you.
Practice Exercise #
Have you grasped the topic we discussed today? Perhaps you have noticed that the Cleaner mechanism used by JDK itself still has some flaws. Do you have any better suggestions?
Please write your suggestions in the comment section, and I will select well-thought-out comments to reward you with a study incentive. Welcome to discuss with me.
Are your friends also preparing for interviews? You can “invite friends to read” and share today’s topic with them. Maybe you can help them.