Two small keywords that get confused with each other constantly. They are unrelated:
static means "belongs to the class, not to an object", and
final means "cannot be reassigned". Neither implies the other.
static fields: one copy, shared
class Counter {
static int totalCreated = 0; // ONE of these exists, ever
int instanceNumber; // one per object
Counter() {
totalCreated++;
instanceNumber = totalCreated;
}
}
class Demo {
void run() {
Counter a = new Counter();
Counter b = new Counter();
System.out.println(a.instanceNumber); // 1
System.out.println(b.instanceNumber); // 2
System.out.println(Counter.totalCreated); // 2 — shared by both
}
}
Access a static field through the class name — Counter.totalCreated. Java lets you
write a.totalCreated, but it is misleading and most linters flag it, because it looks
like per-object data and is not.
static methods
class MathUtils {
static int square(int n) { // works entirely from its arguments
return n * n;
}
}
class Demo {
void run() {
System.out.println(MathUtils.square(4)); // 16 — no object needed
}
}
The rule for whether a method should be static is simple: does it use any instance state? If it only works from its parameters, make it static and say so. If it reads or writes a field, it must be an instance method.
A static method cannot touch instance fields or call instance methods, because there is no object
for it to use — this is why main being static is the source of the beginner error
"non-static variable cannot be referenced from a static context":
class Program {
int count = 5;
static void broken() {
// System.out.println(count); // does not compile: which object's count?
}
static void works() {
Program p = new Program();
System.out.println(p.count); // 5 — an object to read it from
}
}
Static blocks and initialisation order
class Config {
static final Map<String, String> DEFAULTS;
static { // runs ONCE, when the class is first loaded
DEFAULTS = new HashMap<>();
DEFAULTS.put("region", "us-west-2");
DEFAULTS.put("retries", "3");
}
}
A static block is for initialisation that takes more than one expression. It runs once, on first
use of the class, before any object exists. Keep it small — an exception thrown from a static block
surfaces as ExceptionInInitializerError, which is a genuinely unpleasant thing to
debug.
final variables
final int maxRetries = 3;
// maxRetries = 5; // does not compile: cannot assign a value to final variable
final List<String> names = new ArrayList<>();
names.add("Folau"); // fine! The LIST is not final, the REFERENCE is
System.out.println(names); // [Folau]
// names = new ArrayList<>(); // this is what final forbids
This is the single most misunderstood thing about final. It freezes
the variable, not the object. A final reference to a mutable object gives you no
immutability at all — the name will always point at that list, and the list can change freely.
For a genuinely unchangeable collection, you need an immutable one:
final List<String> fixed = List.of("a", "b");
// fixed.add("c"); // throws UnsupportedOperationException at runtime
System.out.println(fixed.size()); // 2
final fields
class Account {
private final String id; // must be set exactly once
private double balance; // free to change
Account(String id, double balance) {
this.id = id; // the constructor is the last chance
this.balance = balance;
}
}
A final field must be assigned by the end of every constructor, and never again. This
is worth doing by default: a field that cannot change is one fewer thing to reason about, it is
automatically thread-safe, and the compiler enforces it rather than a comment.
A good habit is to make every field final and remove the keyword only where you have
a concrete reason. Most fields turn out not to need to change.
static final — constants
class HttpClientConfig {
static final int TIMEOUT_SECONDS = 30;
static final String USER_AGENT = "lovemesomecoding/1.0";
// A mutable object as a "constant" is a trap — the reference is fixed, the contents are not
static final List<String> SAFE = List.of("GET", "HEAD");
}
static final together means one shared value that never changes — a constant. By
convention these are named in UPPER_SNAKE_CASE, and it is the one place in Java where
that casing is used.
Note the comment on the last field. static final on a mutable collection is a
classic mistake: it looks like a constant and is globally writable by anything that can see it. Use
List.of, Set.of or Map.of.
final classes and methods
Here is the pattern in full, from the console bank app this site uses for examples — a
final class with a private constructor and only static members:
public final class Money {
public static final BigDecimal ZERO = of("0");
/** Two decimal places, the way currency is written. */
private static final int SCALE = 2;
private Money() {
// Utility class: nothing to construct.
}
public static BigDecimal of(String value) {
return new BigDecimal(value).setScale(SCALE, RoundingMode.HALF_UP);
}
public static boolean isPositive(BigDecimal value) {
return value.compareTo(BigDecimal.ZERO) > 0;
}
}
Every keyword there is doing a job. final on the class means nobody can subclass it.
The private constructor means nobody can instantiate it — together they say "this is a
bag of static helpers" in a way the compiler enforces. static final SCALE is a constant
used by the methods; public static final ZERO is one callers use.
class Service {
final void audit() { // subclasses cannot override this
System.out.println("audited");
}
}
String, Integer and the other wrappers are all final — their
immutability guarantees would be worthless if you could subclass them and break the rules.
Mark a method final when overriding it would break something the class depends on,
such as a security or auditing step.
Effectively final
One last piece, because it explains an error message you will meet with lambdas. A local variable that is never reassigned is effectively final, and only such variables can be captured by a lambda or anonymous class:
void demo() {
String prefix = "Order "; // never reassigned -> effectively final
List<String> ids = List.of("1", "2");
ids.forEach(id -> System.out.println(prefix + id)); // fine
int count = 0;
// ids.forEach(id -> count++); // "local variables referenced from a lambda
// expression must be final or effectively final"
}
The fix is almost never to fight the rule — it is to use the right tool, such as a stream's
count() or a collector, rather than mutating a counter from inside a lambda.
Next
Packages is next — how Java organises classes into namespaces, and the four access modifiers that decide who can see what.