Back to Blog
Java

Java Final Class: Preventing Inheritance

java final class: Learn how the final keyword on a Java class prevents inheritance, enforces design invariants, and affects runtime optimization.

Javafinal keywordinheritanceimmutabilityclass designsealed classes
Illustration of a sealed brick wall representing a Java final class, with ghost-like subclass shapes bouncing off it.

A java final class is one that cannot be subclassed. When you write public final class Configuration, the compiler rejects any attempt to create a subclass with extends Configuration. This is a compile-time rule, not a runtime check, so the restriction is enforced before any code executes.

The most familiar example is java.lang.String. The standard library declares String as public final class String, which is why you cannot write class MyString extends String. The designers made this choice so that string behavior stays consistent across the entire platform.

Declaring a final class

The syntax is straightforward: place final between the access modifier and the class name.

public final class Configuration { private final String host; private final int port; public Configuration(String host, int port) { this.host = host; this.port = port; } public String host() { return host; } public int port() { return port; } }

The final modifier on the class and the final modifier on the fields serve different purposes. The class-level final prevents inheritance. The field-level final prevents reassignment after construction. A final class does not require final fields, and a class with all final fields is not automatically final. The two concepts are independent.

What happens when a subclass tries to extend a final class

The compiler produces an error at the point of the extends clause.

// Compile error: cannot inherit from final Configuration public class CustomConfiguration extends Configuration { }

The error message from javac is:

error: cannot inherit from final Configuration

This fails at compile time, so there is no way to accidentally create a subclass at runtime through reflection either. The final restriction is part of the class's binary representation, and the JVM enforces it during class loading. Even code compiled against an older version of the class will fail to load if the class has since become final.

Final class, final method, and final field compared

The final keyword appears in three distinct positions in Java, and each has a different effect.

PositionEffectExample
ClassCannot be subclassedpublic final class String
MethodCannot be overridden in a subclasspublic final void connect()
FieldCannot be reassigned after constructionprivate final int port

A final class does not need to mark its methods as final. Since the class cannot be subclassed, no method in it can be overridden. The compiler treats every instance method of a final class as effectively final for dispatch purposes.

A common mistake is writing final on every method of a final class. That is redundant. The class-level modifier already guarantees the methods cannot be overridden.

Design reasons for making a class final

The primary reason is to prevent extension that would break invariants. A class that validates its state in the constructor, such as an immutable value object, can be corrupted by a subclass that adds mutable fields or overrides methods to bypass validation.

public final class Money { private final long amount; private final String currency; public Money(long amount, String currency) { if (amount < 0) { throw new IllegalArgumentException("Amount cannot be negative"); } if (currency == null || currency.isBlank()) { throw new IllegalArgumentException("Currency is required"); } this.amount = amount; this.currency = currency; } }

If Money were not final, a subclass could override behavior or introduce state that violates the invariant that amounts are non-negative. Marking the class final makes the contract enforceable.

Security-sensitive classes also benefit. If a class performs authorization checks, a subclass could override a method and remove the check. The standard library marks many security-related classes as final for this reason.

Runtime and performance characteristics

The JVM's JIT compiler can apply optimizations to final classes that are not possible with extensible classes. When a method is called on a reference whose static type is a final class, the JIT can determine the concrete implementation without a virtual dispatch lookup. This is called devirtualization.

The practical effect is that calls to methods of a final class can be inlined more aggressively. The exact speedup depends on the workload and the JVM version, so there is no universal number. What matters is that the JIT has more information when the class is final, and that information can translate into fewer dispatch overheads in hot code paths.

This is not a reason to make every class final. The performance benefit is usually small compared with the design implications. But in tight loops that call the same method millions of times, the difference can be observable.

Final classes, sealed classes, and records in modern Java

Java 17 introduced sealed classes, which offer a middle ground between fully extensible and fully final. A sealed class declares a fixed set of permitted subclasses.

public sealed class Shape permits Circle, Square { } public final class Circle extends Shape { } public final class Square extends Shape { }

Records, introduced in Java 16, are implicitly final. You cannot extend a record, and you cannot write extends on a record declaration. A record is a final class with generated accessors, equals, hashCode, and toString.

The choice between final and sealed depends on whether you need a closed set of subtypes. If no subclass should ever exist, use final. If a known, limited set of subclasses is part of the design, use sealed. Sealed classes give you the exhaustiveness checking of a closed hierarchy while still allowing multiple implementations.

One practical consideration: making a class final is a breaking change for downstream code that extends it. If you publish a library and later mark a previously extensible class as final, any consumer that subclassed it will fail to compile. For that reason, the decision to make a class final is best made early, before the class is released.

java final class: Practical Usage and Code Examples | RYUSLOG DEV