Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 28

Understanding the `final` Keyword

Recommended resources

Welcome to our next lesson. In our previous discussion, we established that Java is always pass-by-value, focusing on the crucial distinction between a variable holding a primitive value and a variable holding an object reference. This distinction is fundamental to predicting how data behaves when passed to methods.

Today, we will build on that foundation by exploring the final keyword. This keyword is a powerful tool for imposing constraints on your code, which enhances robustness, security, and clarity. Our goal is to understand the precise semantics of final when applied to the three main elements of the language: variables, methods, and classes. This will not only clarify its use in code design but also reveal some of its deeper implications for performance and concurrency.

A Quick Tour of the final Keyword

The final keyword, at its core, means "this cannot be changed." However, what "this" and "changed" mean depends on the context. To get a quick overview of these three contexts, let's start with a short, clear video tutorial.

#57 Final keyword in java

The Telusko channel provides a concise walkthrough of how the final keyword is used with variables, classes, and methods.

Watch the video from the beginning until the summary at the end. Pay attention to the three distinct use cases presented: Final Variable (constant): How it prevents a variable's value from being changed. Final Class (no inheritance): How it prevents a class from being extended. Final Method (no overriding): How it prevents a method from being overridden in a subclass.


Deconstructing final: A Detailed Look

The video gave you a solid high-level understanding. Now, let's dive into the specifics and nuances of each application. We'll use a comprehensive article from Baeldung as our guide.

The "final" Keyword in Java | Baeldung

This article from Baeldung provides excellent, detailed explanations with code examples for each use of final. We will read it in parts.

First, read the sections on final variables, covering both primitive and reference variables. Also, review the short section on final parameters.

final Variables: The Write-Once Rule

As you just read, the rule for a final variable is simple: it can be assigned a value only once.

  • For primitive types, this means the variable becomes a constant. Once final int x = 10; is declared and initialized, x will always be 10.
  • For reference types, the nuance we discussed in the last lesson is critical. The final keyword makes the reference constant, not the object it points to.

Let's clarify with an analogy from your C background. A final object reference in Java is like a const pointer in C (e.g., MyClass* const ptr). You cannot change where ptr points (ptr = &anotherObject; is illegal), but you can still modify the object it points to (ptr->someField = newValue;), assuming the object's fields are not themselves const.

So, for a final reference final Cat myCat = new Cat();:

  • myCat = new Cat(); is illegal. You cannot reassign the reference.
  • myCat.setWeight(5); is perfectly legal (assuming Cat has a setWeight method and is mutable). You are modifying the state of the single object myCat refers to.

This distinction is vital for writing predictable code. If you want true immutability, you must ensure the object's class itself is designed to be immutable (e.g., by making all its fields final and not providing any setters).

final Methods and Classes: Controlling Extensibility

Now, let's look at how final affects inheritance.

The "final" Keyword in Java | Baeldung

Continue with the same Baeldung article.

Please read section 2, Final Classes, and section 3, Final Methods.

  • final Methods: As the article states, a final method cannot be overridden. This is a deliberate design choice to enforce a specific implementation. When you want to guarantee that a method's behavior is consistent across all subclasses, you make it final. A prime example from the JDK is Thread.isAlive(), a native method whose core functionality must not be altered by subclasses.

  • final Classes: A final class cannot be extended (subclassed). This is the ultimate restriction on extensibility. It's used when a class's implementation is considered complete, secure, and should not be tampered with. The most famous example is java.lang.String. Its immutability and finality are critical for security, performance (e.g., hashcode caching), and overall system stability. By making the String class final, the Java designers prevented developers from creating malicious or buggy subtypes that could violate the core contract of how strings behave.

The following table provides a quick visual summary of where the final modifier can be applied.

This table shows which Java language elements (classes, variables, methods) can have the `final` modifier applied to them. As shown, `final` can be used on classes, class variables (static fields), instance variables, methods, and method parameters (local variables).

Beyond Syntax: final, Performance, and the JVM

While the primary purpose of final is to enforce design constraints and improve code clarity, it also has important—though often subtle—implications for performance and the Java Memory Model. This is the "inner information" that distinguishes a proficient developer.

The common advice is "don't use final for performance," and while you shouldn't sprinkle final everywhere hoping for a speed boost, understanding where it does help reveals a lot about how the Java compiler and JVM work.

Let's explore this through a highly-regarded Stack Overflow discussion.

Does use of final keyword in Java improve the performance?

This Stack Overflow thread contains several excellent, in-depth answers that analyze the performance impact of final. We'll focus on a few key insights.

First, read the answer by rustyx that begins with "Short answer: don't worry about it!". Notice the benchmark results for string concatenation. The final local variables allow javac to perform the concatenation at compile time, creating a single constant string "ab". The non-final version requires runtime concatenation. Next, read the answer by mel3kings that starts with "YES it can.". This demonstrates conditional compilation. Because the final static boolean is a compile-time constant, the if (false) block and all its code are completely removed by the compiler, resulting in smaller and faster bytecode. Finally, read the two brief but crucial points at the end of the answer by Eugene, beginning with "There are two more finals that need to be addressed." This introduces two advanced topics: Monomorphic Calls: A final method call is monomorphic (it has only one potential target). This allows the JIT compiler to bypass virtual method lookups and aggressively inline the method's code, which is a significant optimization. Memory Model Guarantees: final fields have special visibility guarantees in the Java Memory Model (JMM). When a constructor finishes, the values of all its final fields are guaranteed to be visible to all other threads without any additional synchronization. This is essential for safely publishing objects in a concurrent environment.

These examples demonstrate that while final's main role is in software design, it serves as a powerful hint to the compiler and JIT engine, enabling optimizations that would otherwise be impossible. The JMM guarantee for final fields is especially important for building correct concurrent applications, a topic we will begin exploring soon.


Conclusion

In this lesson, we have thoroughly examined the final keyword and its role in Java. You now understand its precise meaning in three different contexts and its deeper implications for the underlying system.

Key Takeaways:

  • final variables can be assigned only once. For primitives, this creates a constant. For references, this makes the reference itself constant, but the object it points to can still be modified.
  • final methods cannot be overridden by subclasses, ensuring their behavior is fixed and cannot be altered down the inheritance chain.
  • final classes cannot be extended, preventing inheritance entirely. This is used for classes like String where stability and security are paramount.
  • The primary motivation for using final is design and safety. It makes your code more robust, readable, and predictable.
  • As a secondary effect, final can enable significant compile-time and JIT optimizations (e.g., constant folding, dead code elimination, method inlining) and provides crucial memory visibility guarantees for concurrent programming.

Our exploration of the final keyword's memory visibility guarantees provides a perfect segue into our next major topic. In the next module, we will begin our deep dive into Java's concurrency model, starting with the fundamental building block: the Java thread and its lifecycle.