03 Discussing the Differences Between Final, Finally, and Finalize

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 as final.
  • 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 the final 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 #

  1. 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 and final, 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.

  1. 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.

  1. 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.