Hello! In our last lesson, we built a powerful NotificationPriority enum, equipping it with fields, a constructor, and methods. You might have noticed that we made its internal fields final to ensure that once a priority level like HIGH was created, its associated data (level and colorCode) could never be changed.
This lesson builds directly on that idea. We'll take a deep dive into the final keyword, moving it from a detail in our enum to a central tool in our design arsenal. We'll systematically explore its three main applications: creating unchangeable variables, preventing methods from being overridden, and stopping classes from being inherited. Mastering final is a key step toward writing the robust, secure, and predictable code required in the enterprise backend systems you aim to build.

Part 1: Final Variables - Creating Constants and Write-Once Fields
The most frequent use of final is with variables. A final variable can be assigned a value only once. After that, its value is locked in.
Final Local Variables and Reference Variables
When you declare a local variable (one inside a method) as final, the compiler ensures you cannot reassign it.
A crucial distinction arises with object references. When you declare a reference variable as final, it's the reference that cannot be changed, not the object it points to. You can't make the final variable point to a new object, but you can still modify the internal state of the object it already points to.
Let's read the Baeldung article's excellent explanation of this concept.
The "final" Keyword in Java | Baeldung
This article from Baeldung provides a thorough breakdown of the final keyword. We'll start with its application to variables.
Please read section 4 of the article, starting with "Final Variables." Focus on subsections 4.1 and 4.2. Pay close attention to the distinction between a final primitive and a final reference variable, as demonstrated with the Cat example.
This concept is vital. For example, in the backend systems you'll build, you might have a final variable holding a database connection object. You can't reassign that variable to a different connection object, but you can still use the connection to execute queries and transactions, which modifies the database's state.
Creating Constants with static final
While final local variables are useful, the combination of static final is where you'll see the most power in backend applications. When you declare a field as public static final, you are creating a true constant β a single, unchanging value that is shared across all instances of a class and can be accessed from anywhere.
These are perfect for defining application-wide values like default configuration, API keys, or role names (e.g., public static final String ROLE_ADMIN = "ROLE_ADMIN";).
The "Coding with John" video provides a great walkthrough on creating and using these constants, including the standard naming convention.
Final Keyword in Java Full Tutorial - Final Classes, Methods, and Variables
This video offers clear, practical examples of the final keyword. We'll focus on the segment covering variables and constants.
Watch the section on final variables from this segment. Pay special attention to the transition from a simple final variable to a public static final constant (pi). Notice the naming convention (all uppercase with underscores) and how the constant can be accessed from other classes.
Part 2: Final Methods - Preventing Overriding
Moving from data to behavior, we can also apply final to methods. A final method cannot be overridden by a subclass. This is a powerful tool for an architect because it allows you to "lock down" the implementation of a specific behavior.
You use this when a method's implementation is critical for the correct and secure functioning of your class, and you don't want any subclass to change it. For instance, a method that performs a crucial security check or a core calculation should be a candidate for being final.
Let's turn back to the Baeldung article for a formal explanation.
The "final" Keyword in Java | Baeldung
Now, let's explore how final applies to methods to control inheritance.
Please read Section 3, Final Methods. The example with the Dog class clearly demonstrates the compiler error you get when trying to override a final method.
By marking a method final, you are guaranteeing its behavior will be consistent for all objects of that type, no matter how far down the inheritance chain they are.
Part 3: Final Classes - Preventing Inheritance
The ultimate restriction is applying final to an entire class. A final class cannot be extended at all. No other class can be its subclass.
Why would you do this? The primary reasons are security and immutability. The most famous example in the JDK is the String class. It's final for a very good reason. String objects are used everywhere, including for security-sensitive information like file paths, database credentials, and network addresses. If you could create a subclass of String and override its methods, you could potentially inject malicious behavior, breaking the assumptions that the entire Java platform is built upon. By making String final, the Java designers guaranteed its behavior is always predictable and secure.
Let's read about this in the Baeldung article.
The "final" Keyword in Java | Baeldung
Finally, let's look at what happens when you apply the final keyword to an entire class.
Read Section 2, Final Classes. The article uses the String class as a prime example and clearly illustrates how a final class prevents extension.
Your Turn: Applying final to the Notification System
Let's refactor our notification system to use the final keyword effectively, making it more robust and explicit in its design.
Task:
-
Constants for Service Types:
- In your
EmailServiceclass, add a constant:public static final String SERVICE_TYPE = "EMAIL"; - In your
SMSServiceclass, add a constant:public static final String SERVICE_TYPE = "SMS"; - Modify the
sendmethod in each class to use this constant in its output. For example:System.out.println("Sending " + SERVICE_TYPE + " with priority level: " + priority.getLevel());
- In your
-
Locking Down a Method:
- Create a new abstract class
public abstract class BaseNotificationService implements NotificationService. - Inside this class, implement a
finalmethod that provides a consistent logging format.public final void log(String recipient, String message, NotificationPriority priority) { System.out.println( String.format("Log Entry: Attempting to send to %s. Priority: %s.", recipient, priority.name()) ); } - Change
EmailServiceandSMSServicetoextend BaseNotificationServiceinstead ofimplements NotificationService. - In the
sendmethod of bothEmailServiceandSMSService, make the very first line a call to thelogmethod you just created:log(recipient, message, priority);. This ensures every notification send attempt is logged uniformly, and no subclass can change this logging behavior.
- Create a new abstract class
-
Finalizing the
Configuration:- Let's imagine our system has a configuration object. Create a new, simple class:
public final class SystemConfiguration { private final String appVersion = "1.0"; public String getAppVersion() { return appVersion; } } - By making this class
final, you are declaring that this is the one and only implementation for system configuration. No one can extend it to create a "special" configuration with potentially insecure overrides. TheappVersionfield is alsofinaland private, making it an immutable property of the configuration.
- Let's imagine our system has a configuration object. Create a new, simple class:
After completing these steps, run your main method. The output should now include the service type from your constants and the log entry from your final method for each notification sent.
Conclusion
Today we've unpacked the final keyword and seen how it serves as a critical tool for creating deliberate constraints in your code. These constraints are not limitations; they are design decisions that lead to more secure, predictable, and maintainable applications.
Key Takeaways:
- Final Variables: Can be assigned only once. Use them for write-once fields or, with
static final, to create true constants that are central to your application's logic. - Final Methods: Cannot be overridden. Use them to lock down critical algorithms or security-related behaviors that must not be altered by subclasses.
- Final Classes: Cannot be inherited. Use them for fundamental, immutable types (like
String) or when you need to guarantee that a class's implementation will not be extended or modified.
In our next lesson, we will shift our focus from the details of a single class to how we organize many classes together. We'll explore packages and the import statement, learning how to structure our code for better organization and to control visibility between different parts of our application.