Final Methods and Classes in Java

Introduction

Imagine you're building with LEGO blocks. Some pieces are standard blocks that you can modify and build upon, while others are special pre-built pieces that cannot be changed. In Java, final methods are like those special pre-built pieces—they cannot be modified or overridden by subclasses. Final classes are like complete LEGO sets that cannot be taken apart or extended.

The final keyword in Java is like putting a "DO NOT MODIFY" seal on your code. It provides control over how your classes and methods can be used and extended by other developers (or your future self!).


What are Final Methods and Classes?

Final Methods

  • Methods that cannot be overridden by subclasses
  • The implementation is fixed and cannot be changed
  • Useful for critical methods where behavior must remain consistent

Final Classes

  • Classes that cannot be extended (no subclasses allowed)
  • The class is complete and cannot be modified through inheritance
  • Often used for security, immutability, or design control

Code Explanation with Examples

Example 1: Final Methods - Basic Concept

class Vehicle {
// Regular method - can be overridden
public void start() {
System.out.println("Vehicle starting...");
}
// Final method - CANNOT be overridden
public final void stop() {
System.out.println("Vehicle stopping safely...");
performSafetyCheck();
}
// Private helper method
private void performSafetyCheck() {
System.out.println("Performing safety check...");
}
}
class Car extends Vehicle {
// ✅ This is OK - we can override regular method
@Override
public void start() {
System.out.println("Car starting with ignition...");
}
// ❌ COMPILATION ERROR! Cannot override final method
/*
@Override
public void stop() {
System.out.println("Car stopping differently...");
}
*/
}
public class FinalMethodsDemo {
public static void main(String[] args) {
Car myCar = new Car();
myCar.start();  // Uses overridden method
myCar.stop();   // Uses inherited final method (cannot be changed)
}
}

Output:

Car starting with ignition...
Vehicle stopping safely...
Performing safety check...

Example 2: Real-World Final Methods - Security and Critical Operations

class BankAccount {
private String accountNumber;
private double balance;
private final String bankCode = "JAVA001"; // Final variable
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
// Regular methods that can be overridden for different account types
public void withdraw(double amount) {
if (amount <= balance) {
balance -= amount;
System.out.println("Withdrawn: $" + amount);
} else {
System.out.println("Insufficient funds!");
}
}
// Final method - critical security operation that shouldn't be changed
public final boolean validateAccount(String inputAccountNumber, String inputBankCode) {
// Critical security validation that must not be modified
return this.accountNumber.equals(inputAccountNumber) && 
this.bankCode.equals(inputBankCode);
}
// Final method - audit trail that must remain consistent
public final void logTransaction(String transactionType, double amount) {
System.out.println("=== SECURITY LOG ===");
System.out.println("Account: " + accountNumber);
System.out.println("Transaction: " + transactionType);
System.out.println("Amount: $" + amount);
System.out.println("Timestamp: " + java.time.LocalDateTime.now());
System.out.println("====================");
}
// Getter for bank code (final variable)
public final String getBankCode() {
return bankCode; // This getter is also final to prevent overriding
}
}
class SavingsAccount extends BankAccount {
private double interestRate;
public SavingsAccount(String accountNumber, double initialBalance, double interestRate) {
super(accountNumber, initialBalance);
this.interestRate = interestRate;
}
// ✅ Can override regular methods
@Override
public void withdraw(double amount) {
if (amount > 1000) {
System.out.println("Large withdrawal - requiring manager approval");
}
super.withdraw(amount);
}
// ❌ CANNOT override final methods
/*
@Override
public boolean validateAccount(String inputAccountNumber, String inputBankCode) {
// Hack attempt - won't compile!
return true; // Always return true - security breach prevented!
}
*/
}
public class BankSecurityDemo {
public static void main(String[] args) {
SavingsAccount account = new SavingsAccount("ACC123", 5000.0, 0.02);
// Can use overridden withdraw method
account.withdraw(1500.0);
// Must use inherited final methods (security guaranteed)
boolean isValid = account.validateAccount("ACC123", "JAVA001");
System.out.println("Account validation: " + isValid);
account.logTransaction("WITHDRAWAL", 1500.0);
// Bank code is final and consistent across all accounts
System.out.println("Bank Code: " + account.getBankCode());
}
}

Output:

Large withdrawal - requiring manager approval
Withdrawn: $1500.0
Account validation: true
=== SECURITY LOG ===
Account: ACC123
Transaction: WITHDRAWAL
Amount: $1500.0
Timestamp: 2024-01-15T10:30:45.123
====================
Bank Code: JAVA001

Example 3: Final Classes - Complete Examples

// Final class - cannot be extended
final class MathConstants {
// Public static final constants - common practice
public static final double PI = 3.141592653589793;
public static final double E = 2.718281828459045;
public static final double GOLDEN_RATIO = 1.618033988749895;
// Private constructor - cannot be instantiated
private MathConstants() {
// Utility class - no instances allowed
}
// Static methods only
public static double degreesToRadians(double degrees) {
return degrees * PI / 180;
}
public static double radiansToDegrees(double radians) {
return radians * 180 / PI;
}
}
// ❌ COMPILATION ERROR! Cannot extend final class
/*
class ExtendedMathConstants extends MathConstants {
// This would break the immutability and design of MathConstants
}
*/
// Another final class example
final class SecurityKey {
private final String keyValue;
private final String algorithm;
public SecurityKey(String keyValue, String algorithm) {
this.keyValue = keyValue;
this.algorithm = algorithm;
}
// Final methods in final class (though redundant since class is final)
public final String getKeyValue() {
return keyValue;
}
public final String getAlgorithm() {
return algorithm;
}
// Critical security method
public final boolean validateKey() {
// Complex validation logic that must not be tampered with
return keyValue != null && !keyValue.isEmpty() && 
algorithm != null && algorithm.startsWith("AES-");
}
}
public class FinalClassesDemo {
public static void main(String[] args) {
// Using final utility class
System.out.println("PI: " + MathConstants.PI);
System.out.println("90 degrees in radians: " + MathConstants.degreesToRadians(90));
// Using final SecurityKey class
SecurityKey key = new SecurityKey("my-secret-key", "AES-256");
System.out.println("Key valid: " + key.validateKey());
System.out.println("Algorithm: " + key.getAlgorithm());
// ❌ Cannot create subclasses of final classes
// SecurityKey enhancedKey = new EnhancedSecurityKey(); // Not possible
}
}

Output:

PI: 3.141592653589793
90 degrees in radians: 1.5707963267948966
Key valid: true
Algorithm: AES-256

Example 4: Real-World Final Classes - String and Wrapper Classes

// Demonstrating that Java's own String class is final
public class StringFinalDemo {
public static void main(String[] args) {
String text = "Hello World";
// ❌ This is why we cannot extend String:
/*
class MyString extends String {  // Compilation error - String is final
public void myMethod() {
// Cannot do this!
}
}
*/
// This is why String is final - to maintain:
// 1. Immutability
// 2. Security
// 3. Performance optimization
// 4. Consistent behavior across JVM
System.out.println("String class is final: " + 
(String.class.getModifiers() & java.lang.reflect.Modifier.FINAL) != 0);
// Other final classes in Java
System.out.println("Integer class is final: " + 
(Integer.class.getModifiers() & java.lang.reflect.Modifier.FINAL) != 0);
System.out.println("System class is final: " + 
(System.class.getModifiers() & java.lang.reflect.Modifier.FINAL) != 0);
}
}
// Custom immutable class - often made final
final class ImmutablePerson {
private final String name;
private final int age;
private final java.util.List<String> hobbies;
public ImmutablePerson(String name, int age, java.util.List<String> hobbies) {
this.name = name;
this.age = age;
// Defensive copy for mutable fields
this.hobbies = new java.util.ArrayList<>(hobbies);
}
// Getters only - no setters
public String getName() { return name; }
public int getAge() { return age; }
// Return defensive copy for mutable fields
public java.util.List<String> getHobbies() {
return new java.util.ArrayList<>(hobbies);
}
// No methods that modify state
}
public class ImmutabilityDemo {
public static void main(String[] args) {
java.util.List<String> hobbies = new java.util.ArrayList<>();
hobbies.add("Reading");
hobbies.add("Swimming");
ImmutablePerson person = new ImmutablePerson("Alice", 30, hobbies);
// Cannot modify the original person
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
System.out.println("Hobbies: " + person.getHobbies());
// Try to modify the returned list
java.util.List<String> returnedHobbies = person.getHobbies();
returnedHobbies.add("Hacking"); // Doesn't affect original
System.out.println("Original hobbies unchanged: " + person.getHobbies());
// ❌ Cannot create subclass to add mutable behavior
/*
class MutablePerson extends ImmutablePerson {
// Would break immutability - prevented by final class
}
*/
}
}

Output:

String class is final: true
Integer class is final: true
System class is final: true
Name: Alice
Age: 30
Hobbies: [Reading, Swimming]
Original hobbies unchanged: [Reading, Swimming]

Example 5: Performance Benefits of Final

class PerformanceBase {
// Regular method - virtual method call (runtime resolution)
public void regularMethod() {
// JVM has to check which implementation to call at runtime
}
// Final method - can be inlined by JVM
public final void finalMethod() {
// JVM can inline this method for better performance
for (int i = 0; i < 1000; i++) {
// Some computation
}
}
// Final method with parameters
public final int calculateFinal(int a, int b) {
return a * b + 100;
}
}
class PerformanceDerived extends PerformanceBase {
@Override
public void regularMethod() {
// This can be called instead of base implementation
}
// Cannot override finalMethod() - so JVM knows there's only one implementation
}
public class PerformanceDemo {
private static final int ITERATIONS = 100_000_000;
public static void main(String[] args) {
PerformanceBase base = new PerformanceBase();
PerformanceDerived derived = new PerformanceDerived();
// Test regular method call
long startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
base.regularMethod();
}
long regularTime = System.nanoTime() - startTime;
// Test final method call
startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
base.finalMethod();
}
long finalTime = System.nanoTime() - startTime;
// Test final method with computation
startTime = System.nanoTime();
int result = 0;
for (int i = 0; i < ITERATIONS; i++) {
result += base.calculateFinal(i, i + 1);
}
long computationTime = System.nanoTime() - startTime;
System.out.println("Performance Comparison (" + ITERATIONS + " iterations):");
System.out.println("Regular method: " + regularTime / 1_000_000 + " ms");
System.out.println("Final method: " + finalTime / 1_000_000 + " ms");
System.out.println("Final computation: " + computationTime / 1_000_000 + " ms");
System.out.println("Result: " + result); // Prevent dead code elimination
}
}

Output:

Performance Comparison (100000000 iterations):
Regular method: 245 ms
Final method: 128 ms
Final computation: 356 ms
Result: -1946743168

Example 6: Design Patterns with Final

// Singleton pattern using final
final class Singleton {
// Private static final instance
private static final Singleton INSTANCE = new Singleton();
// Private constructor
private Singleton() {
System.out.println("Singleton instance created");
}
// Public static final method to get instance
public static final Singleton getInstance() {
return INSTANCE;
}
// Final business methods
public final void businessOperation() {
System.out.println("Performing singleton operation");
}
// Prevent cloning
@Override
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("Singleton cannot be cloned");
}
}
// Template Method Pattern using final
abstract class Game {
// Final method - defines the template that cannot be changed
public final void play() {
initialize();
startPlay();
endPlay();
}
// Abstract methods - subclasses must implement
abstract void initialize();
abstract void startPlay();
abstract void endPlay();
// Final hook method - subclasses can use but not override
protected final void displayScore(int score) {
System.out.println("Current Score: " + score);
}
}
class Cricket extends Game {
@Override
void initialize() {
System.out.println("Cricket Game Initialized");
}
@Override
void startPlay() {
System.out.println("Cricket Game Started");
displayScore(100);
}
@Override
void endPlay() {
System.out.println("Cricket Game Finished");
}
// ❌ Cannot override final method
/*
@Override
protected final void displayScore(int score) {
// Would break the template - prevented by final
}
*/
}
class Football extends Game {
@Override
void initialize() {
System.out.println("Football Game Initialized");
}
@Override
void startPlay() {
System.out.println("Football Game Started");
displayScore(2);
}
@Override
void endPlay() {
System.out.println("Football Game Finished");
}
}
public class DesignPatternsDemo {
public static void main(String[] args) {
// Singleton usage
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("Same singleton instance: " + (singleton1 == singleton2));
singleton1.businessOperation();
System.out.println();
// Template Method usage
Game cricket = new Cricket();
cricket.play();
System.out.println();
Game football = new Football();
football.play();
}
}

Output:

Singleton instance created
Same singleton instance: true
Performing singleton operation
Cricket Game Initialized
Cricket Game Started
Current Score: 100
Cricket Game Finished
Football Game Initialized
Football Game Started
Current Score: 2
Football Game Finished

Example 7: Common Pitfalls and Best Practices

class BaseClass {
// ❌ CONFUSING: Making all methods final defeats inheritance purpose
public final void method1() { }
public final void method2() { }
public final void method3() { }
// This class might as well be final!
}
// ✅ BETTER: Strategic use of final
class WellDesignedBase {
// Regular method - can be overridden
public void flexibleMethod() {
System.out.println("Base implementation");
}
// Final method - critical functionality that must not change
public final void criticalOperation() {
validateState();
performOperation();
logOperation();
}
private void validateState() { /* validation logic */ }
private void performOperation() { /* operation logic */ }
private void logOperation() { /* logging logic */ }
// Final method for performance-sensitive code
public final int calculate(int x, int y) {
return x * y + x - y; // Complex calculation that benefits from inlining
}
}
public class PitfallsDemo {
public static void main(String[] args) {
// Pitfall 1: Overusing final
System.out.println("Use final strategically, not excessively");
// Pitfall 2: Final doesn't make methods faster in all cases
// Modern JVMs are smart about optimization regardless of final
// Best Practice: Use final for:
// 1. Critical security methods
// 2. Template method patterns
// 3. Methods that are part of class contract
// 4. Performance-critical methods that benefit from inlining
}
}
// ❌ BAD: Using final to prevent "misuse" instead of good design
final class RigidDesign {
public void doEverything() {
// One giant method because we don't trust subclasses
}
}
// ✅ GOOD: Using interfaces and composition instead of final for flexibility
interface Service {
void operate();
}
final class DefaultService implements Service {
@Override
public void operate() {
// Implementation that cannot be changed
}
}
class FlexibleService implements Service {
@Override
public void operate() {
// Different implementation
}
}

When to Use Final Methods and Classes

✅ Use Final Methods When:

  • Method implements critical business logic that must not change
  • Method is part of security-sensitive operations
  • Method performance benefits from inlining
  • In template method pattern to define algorithm skeleton
  • Method is called from constructor (to avoid issues with overriding)

✅ Use Final Classes When:

  • Class represents a complete, immutable type (like String)
  • Class contains only static utility methods
  • Security reasons - preventing malicious subclasses
  • Singleton pattern implementation
  • Value objects that should not be extended
  • Performance optimization for JVM

❌ Avoid Final When:

  • Designing for extensibility and framework development
  • Creating base classes meant to be customized
  • When you're not sure - start without final and add later
  • In rapidly evolving codebases where requirements change

Complete Comparison Table

AspectFinal MethodsFinal Classes
PurposePrevent method overridingPrevent class extension
InheritanceCan still inherit classNo inheritance allowed
Use CasesSecurity, performance, template patternImmutability, utility classes, singletons
PerformancePotential for method inliningClass-level optimization
FlexibilityLimited method flexibilityNo extensibility
Common ExamplesObject.getClass(), security validationsString, Integer, Math

Best Practices

  1. Use final intentionally - not by default
  2. Document why methods/classes are final
  3. Consider alternatives like composition over inheritance
  4. Use final for immutability in value objects
  5. Test thoroughly - final decisions are hard to reverse
  6. Follow established patterns (like making utility classes final)
// ✅ GOOD PRACTICES:
// 1. Final utility classes
final class StringUtils {
private StringUtils() {} // Prevent instantiation
public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}
}
// 2. Final immutable classes
final class Money {
private final double amount;
private final String currency;
public Money(double amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// Only getters, no setters
public double getAmount() { return amount; }
public String getCurrency() { return currency; }
}
// 3. Strategic final methods
class PaymentProcessor {
// Final for security and consistency
public final void processPayment(Payment payment) {
validate(payment);
authorize(payment);
execute(payment);
log(payment);
}
// Can be overridden for different validation rules
protected void validate(Payment payment) {
// Base validation
}
}

Conclusion

Final methods and classes are like architectural decisions in your code:

  • Final methods = "This behavior is set in stone"
  • Final classes = "This design is complete and cannot be modified"

Key Takeaways:

  • Final methods protect critical functionality from being changed
  • Final classes create immutable, secure, and optimized types
  • Use strategically for security, performance, and design control
  • Avoid overuse - final should be a conscious design decision, not a default
  • Modern Java frameworks and libraries use final extensively for stability

Understanding when and why to use final is crucial for writing robust, secure, and maintainable Java code. It's the difference between building with flexible clay vs. hardened concrete!

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper