72 What Are the Three Uses of Final

What are the three uses of final? #

In this lesson, we will mainly discuss the three uses of final.

Purpose of final #

final is a keyword in Java that means “this is unchangeable”. However, since final can be used to modify variables, methods, or classes, and the effect, meaning, and focus will be different in each case, we need to introduce these three cases separately.

Let’s first look at the use of final to modify variables.

final to modify variables #

Purpose #

The purpose of using the final keyword to modify variables is clear, which means that once a variable is assigned, it cannot be changed again. In other words, it can only be assigned once and will never “change its mind” even until the end of the world. If we try to assign a value again to a variable that has already been assigned a final, a compilation error will occur.

Let’s take a look at the following code example:

/**
 * Description: Once assigned, a final variable cannot be modified.
 */
public class FinalVarCantChange {
    public final int finalVar = 0;

    public static void main(String[] args) {
        FinalVarCantChange finalVarCantChange = new FinalVarCantChange();
//        finalVarCantChange.finalVar=9; // Compilation error, modifying a final member variable is not allowed
    }
}

In this example, we have a final modified int variable called finalVar. In the main method, we create an instance of this class and attempt to modify its value. This will result in a compilation error, demonstrating the main purpose of finalizing a variable: once assigned, it cannot be modified.

Purpose #

After understanding its purpose, let’s take a look at the purpose of using final, that is, why we need to add the final keyword to a variable. There are mainly two purposes.

The first purpose is from a design perspective. For example, if we want to create a value that cannot be changed once assigned, we can use the final keyword. For example, when declaring constants, we usually use final:

public static final int YEAR = 2021;

At this time, YEAR is fixed, so we add the final keyword to prevent it from being modified. This makes the constant clearer and less prone to errors.

The second purpose is from the perspective of thread safety. Immutable objects are inherently thread-safe, so we do not need additional synchronization or handling, avoiding extra overhead. If final modifies a primitive data type, it naturally ensures immutability, ensuring thread safety. This makes us more confident in using it in the future.

These are the two purposes of using final to modify variables.

Assignment Timing #

Next, let’s look at the assignment timing of variables modified by final. Variables can be divided into the following three types:

  • Member variables, non-static attributes in a class;
  • Static variables, attributes in a class modified by static;
  • Local variables, variables in a method.

For these three types of variables, when they are modified by final, the timing of assignment is different. Let’s take a look at each of them.

(1) Member Variables

Member variables refer to non-static properties in a class. For these member variables, when modified by final, there are three ways (or assignment methods).

  • The first way is to assign a value directly after the equal sign when declaring the variable. For example:
public class FinalFieldAssignment1 {
    private final int finalVar = 0;
}

In this class, we have private final int finalVar = 0, which assigns a value when declaring the variable.

  • The second way is to assign a value in a constructor. For example:
class FinalFieldAssignment2 {
    private final int finalVar;

    public FinalFieldAssignment2() {
        finalVar = 0;
    }
}

The finalVar is assigned in the constructor. In this example, we first declare a variable, which is private final int finalVar, without assigning a value to it. Then, we assign a value to it in the constructor of this class, which is also allowed.

  • The third method is to assign a value in the block of code within the class constructor (not commonly used), for example:
class FinalFieldAssignment3 {

    private final int finalVar;

    {

        finalVar = 0;

    }

}

We also declare a variable, private final int finalVar, without assigning a value to it. Then, in the block of code enclosed by curly braces below, we assign a value to the variable. This is also a valid time to assign a value.

It should be noted that we must choose one of these three methods to assign a value to a final variable. For non-final ordinary variables, of course, it is not necessary to assign a value during these three cases; it is completely possible to assign a value at other times. Or if you don’t intend to use the variable, it is also possible to not assign a value. However, for member variables modified by final, it is necessary to choose one of the three methods to assign a value, and it is not allowed to not choose any of them or not assign a value at all. This is specified by the final syntax.

Blank final #

Next, let’s discuss a concept: “blank final”. If we declare a final variable but do not immediately assign a value to it on the right-hand side of the equals sign, this is called “blank final”. The advantage of doing this is to increase the flexibility of the final variable. For example, it can be assigned different values in the constructor based on different conditions. In this way, the final variable modified by final will not become rigid and can be ensured to remain unchanged after assignment. We use the following code to illustrate:

/**
 * Description: Blank final provides flexibility
 */
public class BlankFinal {

    // Blank final
    private final int a;

    // If no argument is provided, assign a a default value 0
    public BlankFinal() {
        this.a = 0;
    }

    // If an argument is provided, assign a the value of the argument
    public BlankFinal(int a) {
        this.a = a;
    }
}

In this code, we have a private final int variable named a, and the class has two constructors. The first constructor assigns a a value of 0, and the second constructor assigns a the value of the parameter passed in. So when you call different constructors, different assignment scenarios will occur. By using this rule, we can design more flexible assignment logic for final variables based on business requirements. Therefore, one major advantage of using blank final is that the value of the final variable is not completely rigid and fixed, but can be flexibly assigned based on the situation. However, once assigned, it cannot be changed anymore.

(2) Static Variables

Static variables are static attributes in a class. After being modified by final, there are only two possible times for assignment.

The first method is to assign a value directly on the right-hand side of the equals sign when declaring the variable. For example:

/**
 * Description: Demonstrating the assignment time of final static class variables
 */
public class StaticFieldAssignment1 {

    private static final int a = 0;

}

The second method is to assign a value in a static initializer block. This method is not commonly used. For example:

class StaticFieldAssignment2 {

    private static final int a;

    static {

        a = 0;

    }

}

In this class, there is a variable private static final int a, followed by the keyword static, and then a pair of curly braces. This is the syntax of a static initializer block. We assign a value to a within this block, which is also allowed. These are the two possible times for assigning a value to a static final variable.

It should be noted that we cannot use a non-static initializer block to assign a value to a static final variable. Another important point is that a static final variable cannot be assigned a value in a constructor.

(3) Local Variables

Local variables refer to variables within a method. If you modify it as final, its meaning remains the same - it cannot be changed once assigned. However, its assignment timing is different from the first two variables because it is defined in a method. Therefore, it does not have a constructor or an initial block, so the corresponding timing for assignment does not exist. In fact, for final local variables, they do not specify the specific timing of assignment, only requiring that we assign it before using it.

This requirement is the same as for non-final variables in a method. For a non-final variable with the final modifier in a method, it actually requires that it be assigned before it is used. Let’s take a look at the following code examples:

/**

 * Description: Timing of local variable assignment: assignment before use

 */

public class LocalVarAssignment1 {

    public void foo() {

        final int a = 0; //assigned directly on the right side of the equals sign

    }

}

class LocalVarAssignment2 {

    public void foo() {

        final int a; //this is allowed because a is not used

    }

}

class LocalVarAssignment3 {

    public void foo() {

        final int a;

        a = 0; //assigned before use

        System.out.println(a);

    }

}

First, let’s look at the first class, LocalVarAssignment1. In the foo() method, there is a final int a, and here it is assigned directly on the right side of the equals sign.

Next, look at the second class. Since we did not use the final modifier local variable a, it was actually not assigned at all. Even though it is final, it can be left unassigned, and this behavior is allowed by the syntax.

The third case is to first create a final int a, and not assign it on the right side of the equals sign, and then assign a before using it, and finally use it. This is also allowed.

To summarize, for final local variables, the timing of assignment is to require assignment before use, otherwise using an unassigned variable will naturally result in an error.

Special Usage: final Modifier on Parameters #

The final keyword can also be used to modify parameters in a method. It is allowed to declare a parameter as final in the method’s parameter list, which means that we cannot modify this parameter within the method. For example:

/**

 * Description: Final parameter

 */

public class FinalPara {

    public void withFinal(final int a) {

        System.out.println(a); //we can read the value of the final parameter

//      a = 9; //compile error, modifying the value of a final parameter is not allowed

    }

}

In this code, there is a method called withFinal, and the parameter a of this method is modified by the final modifier. Next, let’s print the value of the parameter a, which is allowed, meaning that we can read its value. But next, let’s assume that we want to modify a within the method, for example, change it to a = 9. This will result in a compilation error, so modifying the value of a final parameter is not allowed.

So far, we have covered all the cases of final variable modifiers, and their core can be summarized in one sentence: once assigned, they cannot be modified.

final Modifier on Methods #

Next, let’s take a look at the case of using the final modifier on methods. One reason to choose final to modify a method is to improve efficiency, because in earlier versions of Java, final methods were converted into inline calls, which could eliminate the overhead of method calls and improve program runtime efficiency. However, in later versions of Java, the JVM will automatically optimize for this, so we don’t need to use final to modify methods for these optimizations, and even if we do, it will not bring performance improvements.

Currently, the only reason we use final to modify a method is to lock it, which means that no subclass can modify the meaning of this method, that is, the method cannot be overridden, cannot be overridden. Let’s give an example of a code:

/**

 * Description: Final methods cannot be overridden

 */

public class FinalMethod {

    public void drink() {

    }

    public final void eat() {

    }
}
}
class SubClass extends FinalMethod {

    @Override

    public void drink() {

        // Non-final methods can be overridden

    }

//    public void eat() {}// Compilation error, cannot override final method

//    public final SubClass() {} // Compilation error, constructor cannot be final

}

In this code, there are two classes. The first class is FinalMethod, which has a drink method and an eat method. The eat method is declared as final. The second class, SubClass, extends the FinalMethod class.

We then attempt to override the drink method. This is allowed because it is a non-final method. However, if we try to override the eat method in the SubClass, we will encounter a compilation error because it is not allowed to override a final method.

There is also an interesting point to note. At the bottom of the code, there is a declaration for a public final SubClass() constructor. This will also result in a compilation error because constructors cannot be marked as final.

Special case: final private methods

There is a special case involving final and private methods. Let's take a look at the following example:

/**
 * Description: Private methods implicitly final
 */
public class PrivateFinalMethod {

    private final void privateEat() {
    }

}

class SubClass2 extends PrivateFinalMethod {

    private final void privateEat() { // Compilation passes, but this is not a true override
    }

}

Final Modifier #

In this code example, we first have a class called PrivateFinalMethod which has a final method that is private. Next, we have SubClass2 which extends the first PrivateFinalMethod class, meaning it inherits from the first class. In the subclass, there is another private final void privateEat() method. Surprisingly, this code compiles successfully, which means that the subclass has a method named privateEat that is final. Does this mean that the subclass SubClass2 successfully overrides the privateEat method in the parent class? Does it challenge the previous conclusion that “final methods cannot be overridden”?

In fact, our previous conclusion still holds true. All private methods in a class are implicitly defined as final, so explicitly adding the final keyword will have no effect. Since this method is private, the subclass cannot access this method in the parent class, let alone override it. In the above code example, the subclass SubClass2 does not truly override the privateEat method in the parent class. The two privateEat methods in the subclass and parent class are independent of each other and happen to have the same method name.

To prove this point, let’s try adding the @Override annotation to the privateEat method in the subclass. This will result in a message saying “Method does not override method from its superclass”, indicating that this method does not truly override the parent class method.

This concludes the explanation of final methods.

Final Classes #

Now let’s take a look at final classes. When a class is marked as final, it means that it cannot be inherited. Let’s consider the following code example:

/**
 * Description: Test the effect of final class
 */
public final class FinalClassDemo {
    // code
}
// class A extends FinalClassDemo {} // compilation error, cannot inherit from a final class

Here, a class called FinalClassDemo is marked as final. If we attempt to write class A extends FinalClassDemo, it will result in a compilation error because it is not allowed to inherit from a final class. By marking a class as final, we indicate that not only will we not inherit from this class ourselves, but we also do not allow others to inherit from it. Therefore, the existence of subclasses is impossible, which to some extent ensures thread safety.

For example, the widely used String class is marked as final, and that’s why we have never seen a class that inherits from the String class. This is essential for maintaining the immutability of String, and we will discuss this further in Lesson 74.

However, there is an important note here. If we mark a class as final, it does not automatically mark its member variables as final. In fact, these two concepts are independent of each other. This means that a class being final does not automatically make its attributes final.

However, we do know that when a method is marked as final, it means it cannot be overridden. Now, if we mark a class as final, it means it cannot have any subclasses, so there is no possibility of method overriding. Therefore, within a final class, all methods, whether they are public, private, or have any other access modifier, are implicitly marked as final.

Providing an Explanation for Using Final Methods or Classes #

There is an important point to consider when using final classes or methods, which is to provide an explanation for doing so. Why is this necessary? Because future code maintainers may not understand why we used final in a particular place, and it can have implications for them. For example, if a method is marked as final, it cannot be overridden. Similarly, if a class is marked as final, it cannot be inherited. Therefore, it is necessary or even obligatory to explain the reasons behind the use of final to prevent confusion for future maintainers.

In many cases, there is no immediate need to declare a class or method as final. It is better to decide this later in the development process. This way, we can have a clearer understanding of the interaction between classes or the relationship between methods. You may find that there is no need to use the final modifier, or it may not need to be applied to a larger scope. We can refactor the code and apply final to smaller classes or methods, resulting in a smaller impact.

Summary #

In this lesson, we have discussed the significance of the final modifier when applied to variables, methods, or classes. Each of these cases has different meanings, so we have explained them one by one. When applied to variables, final means that once assigned, the variable cannot be modified. When applied to methods, final means that the method cannot be overridden. When applied to classes, final means that the class cannot be inherited.

When explaining the final modifier for variables, we also analyzed three different cases: instance variables, static variables, and local variables. It can be seen that they have different timing of assignment. If we use a blank final, we can make the variable more flexible. There is also a special case where the final modifier is used for method parameters, which means that the parameter cannot be changed.

Finally, we emphasized the importance of providing an explanation for using final methods or classes. This is to prevent confusion for future maintainers who may not understand the reasons behind these choices. In many cases, there is no immediate need to use the final modifier or expand its scope too much. We can refactor the code and apply final to smaller classes or methods, resulting in a smaller impact.