Static Fields and Methods

Know when state belongs to the class rather than an object, use static factories and constants well, and avoid shared static state in servers.

Syntaxstatic final int MAX_SIZE = 100; static int counter; static ReturnType helper(Params) { ... } ClassName.helper(args);

A static field belongs to the class, not to any object. There is one copy, shared by every instance and every thread in the JVM. A static method runs without an object, so it has no this and cannot read instance fields directly.

Good uses

  • Constants: static final values that never change, such as MAX_PAGE_SIZE or a precompiled Pattern. Constant names use UPPER_SNAKE_CASE.
  • Pure utility methods that only use their arguments, such as Math.max or a slugify(String) helper.
  • Static factory methods: Money.ofCents(1250) or Money.parse("12.50"). Unlike constructors they have names, can return a cached instance instead of a new one, and can return a subtype. The JDK uses them everywhere: List.of, Path.of, Duration.ofSeconds.

A static { ... } initialiser block runs once, when the class is first initialised. It is handy for building a lookup table that a single expression cannot express; for a few fixed entries, Map.of(...) is simpler.

Where static hurts

Mutable static fields are global variables. In a web server every request thread sees the same one, so a static int counter loses increments under load, and a static SimpleDateFormat or ArrayList gets corrupted. If shared state is really needed, use thread-safe types such as AtomicLong or ConcurrentHashMap, or better, keep the state in a service object created once and passed to whoever needs it.

Static methods that do I/O or read the clock are hard to replace in tests. A method that calls Database.query(...) statically always hits that database; one that receives a repository through its constructor can be given a fake. This is why frameworks favour injected instances over static helpers for anything with side effects.

Small gotchas

  • You can call a static method through an instance variable, as in order.count(). It compiles, but reads as if it depended on that object. Always call it through the class name.
  • Static methods are not overridden. A subclass method with the same signature hides the parent's one, and which runs depends on the declared type, not the object.
  • A static field initialised from another class's static field can see a default value if the classes initialise each other in a cycle. Keep static initialisation simple.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Keep a precompiled Pattern for validating slugs in a static final field instead of compiling it per request.
  • Offer Money.parse and Money.ofCents factories so controllers cannot build an amount without validation.
  • Count handled requests across all server threads with a static AtomicLong instead of a plain long.

More examples

Named static factory methods

Two constructors taking a long and a String would be easy to mix up; ofCents and parse say what they expect. The private constructor forces every caller through a named factory, so rules added there cannot be bypassed.

Example · java
import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        IO.println(Money.ofCents(1250));
        IO.println(Money.parse("19.99"));
        try {
            Money.parse("1.999");
        } catch (ArithmeticException e) {
            IO.println("Too many decimals: " + e.getMessage());
        }
    }
}

class Money {
    private final long cents;

    private Money(long cents) {
        this.cents = cents;
    }

    static Money ofCents(long cents) {
        return new Money(cents);
    }

    static Money parse(String amount) {
        BigDecimal value = new BigDecimal(amount);
        return new Money(value.movePointRight(2).longValueExact());
    }

    @Override
    public String toString() {
        return BigDecimal.valueOf(cents, 2).toPlainString();
    }
}
// Output:
// 12.50
// 19.99
// Too many decimals: Rounding necessary

A static initialiser block

The static block runs once, the first time the class is used, not when the program starts. Wrapping the map as unmodifiable keeps the shared table safe to read from any thread. For fixed entries like these Map.of is enough; a static block earns its place when the table is built in a loop or with error handling.

Example · java
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

class HttpStatus {
    static final Map<Integer, String> REASONS;

    static {
        Map<Integer, String> m = new LinkedHashMap<>();
        m.put(200, "OK");
        m.put(201, "Created");
        m.put(404, "Not Found");
        m.put(409, "Conflict");
        REASONS = Collections.unmodifiableMap(m);
        IO.println("HttpStatus initialised");
    }

    static String reason(int code) {
        return REASONS.getOrDefault(code, "Unknown");
    }
}

void main() {
    IO.println("before first use");
    IO.println(HttpStatus.reason(201));
    IO.println(HttpStatus.reason(409));
    IO.println(HttpStatus.reason(418));
}
// Output:
// before first use
// HttpStatus initialised
// Created
// Conflict
// Unknown

Shared static state across threads

Four threads share one static counter, as request threads would in a server. With a plain static int and count++ the total would often come out below 40000, because increments from different threads overwrite each other.

Example · java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

class Metrics {
    static final AtomicInteger REQUESTS = new AtomicInteger();

    static void record() {
        REQUESTS.incrementAndGet();
    }
}

void main() throws InterruptedException {
    List<Thread> workers = new ArrayList<>();
    for (int t = 0; t < 4; t++) {
        Thread worker = Thread.ofPlatform().start(() -> {
            for (int i = 0; i < 10_000; i++) {
                Metrics.record();
            }
        });
        workers.add(worker);
    }
    for (Thread worker : workers) {
        worker.join();
    }
    IO.println("requests: " + Metrics.REQUESTS.get());
}
// Output:
// requests: 40000

Discussion

  • Be the first to comment on this lesson.