Mastering Java’s final Keyword: Variables, Classes, Methods, and the String Pool

Variable Level

Primitvie Fields

When applied to primitives, final freezes the stored value at compile time. The compiler inlines the constant and forbids reassignment.

static final int DEFAULT_TIMEOUT = 30;
static final double PI_APPROX = 3.1415926;

Reference Fields

A final reference can never point to another object after initialization, yet the internal state of the referenced object remains mutable.

final List<String> tags = new ArrayList<>();
tags.add("java");           // allowed – mutating the list
tags = new ArrayList<>();   // compile-time error – re-binding forbidden

Initialization must occur exactly once—either at declaration, in every constructor, or in an instance-initializer block.

Class Level

Marking a class final prevents inhertiance. The compiler rejects any attempt to extend such a type.

public final class ConfigManager { }

// class ExtendedManager extends ConfigManager { } // ERROR

abstract and final are mutually exclusive; the former mandates subclassing, the latter forbids it.

Built-in examples include java.lang.System, java.lang.String, and wrapper types like Integer. Immutability and security concerns often motivate this design.

Method Level

A final method cannot be overridden in subclasses, though overloading is still permitted.

class Base {
    final void log(String msg) { /* impl */ }
    void log(String msg, Level l) { /* overload OK */ }
}

class Child extends Base {
    // @Override final void log(String msg) { } // ERROR
}

Again, abstract and final collide—an abstract method must be overridden, while a final one must not.

String Internals and Constant Folding

String is final, but its immutability is achieved through defensive copying and package-private fields, not merely the keyword.

public final class String {
    private final char[] value; // reference never changes
    /* … */
}

Compile-Time Constants

When a final variable holds a compile-time contsant, concatenations are resolved statically and may reuse the string pool.

public class PoolDemo {
    public static void main(String[] args) {
        String a = "wzh2";              // pool entry
        final String b = "wzh";         // constant
        String c = b + 2;               // folded to "wzh2"
        String d = "wzh";
        String e = d + 2;               // runtime concatenation

        System.out.println(a == c);     // true – same pool entry
        System.out.println(a == e);     // false – heap object
    }
}

Bytecode excerpt (simplified):

0: ldc           #2  // String wzh2
2: astore_1
3: ldc           #3  // String wzh
5: astore_2
6: ldc           #2  // String wzh2 (reused)\n8: astore_3
9: new           #4  // class StringBuilder
12: dup
13: invokespecial #5  // StringBuilder.<init>
16: ldc           #3  // String wzh
18: invokevirtual #6  // StringBuilder.append
21: iconst_2
22: invokevirtual #7  // StringBuilder.append
25: invokevirtual #8  // StringBuilder.toString
28: astore        4

Object Creation Count

final String b = "wzh";   // 1 pool object
String c = b + 2;         // 0 new objects (folded)
String e = new String("wzh"); // 1 heap object, 0 new pool objects (already present)
String f = new String(

Posted on Mon, 24 Aug 2026 16:12:39 +0000 by laurton