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(