env.dev

Constants in Java: static final, Enums & List.of

Java constants are static final fields by convention. Compile-time inlining bites consumers when library values change. Java 9+ List.of / Map.of / Set.of are truly immutable.

Last updated:

Java declares constants with two modifiers: static final. The static half attaches the field to the class, not to instances; the final half blocks reassignment. Combined, you get the canonical Java constant. There is one detail every Java developer eventually trips on: when a static final field is a primitive or a String initialized with a constant expression, javac inlines the literal value at every call site. Change the constant in your library, and consumers keep the old value until they recompile. Java 9 (September 2017) added genuine immutable factory methods (List.of, Map.of, Set.of), Java 14 (March 2020) introduced exhaustive switch expressions, and Java 16 (March 2021) shipped records. This guide covers the lot.

What does static final actually guarantee?

static places the field on the class object, so all instances share a single copy. final means the reference cannot be reassigned after initialization. Names use SCREAMING_SNAKE_CASE by convention, since the official Code Conventions document of 1999.

java
public final class AppConfig {
    public static final int    MAX_RETRIES   = 3;
    public static final String API_BASE_URL  = "https://api.example.com/v1";
    public static final long   TIMEOUT_MS    = 30_000L;
    public static final double TAX_RATE      = 0.0825;

    private AppConfig() {} // prevent instantiation
}

final blocks reassignment of the binding, not mutation of the object. A static final List<String> can still accept .add() and .remove() if it is a regular ArrayList. Use the immutable factories below for real constants.

Why does compile-time constant inlining matter?

When a static final field is a primitive or String initialized to a constant expression, javac writes the literal value into every class that references it — not a reference to the field. This is fast at runtime, but it has a binary-compatibility consequence: if you ship a library with public static final int MAX_SIZE = 1024 and later change it to 2048, every consuming class must be recompiled for the new value to take effect.

java
// Compile-time constant — javac inlines the literal at every use site
public static final int    MAX_SIZE = 1024;
public static final String VERSION  = "2.0";

// NOT a compile-time constant — looked up at class-load time
public static final long   STARTUP_TIME = System.currentTimeMillis();
public static final String HOME         = System.getenv("HOME");

For values that may change across library versions, prefer a static method or an enum: public static int maxSize() { return 1024; } is not inlined, so consumers see the new value as soon as the new JAR is on the classpath.

When should you use enum instead?

Java enums are full classes — they can have fields, methods, constructors, and implement interfaces. They are the right choice for any closed set of related values: HTTP statuses, days of the week, finite-state machines. Each instance is a singleton guaranteed by the JVM, comparable with ==, and switch expressions (Java 14+) verify exhaustiveness at compile time:

java
public enum HttpStatus {
    OK(200, "OK"),
    CREATED(201, "Created"),
    BAD_REQUEST(400, "Bad Request"),
    NOT_FOUND(404, "Not Found"),
    INTERNAL_SERVER_ERROR(500, "Internal Server Error");

    private final int    code;
    private final String reason;

    HttpStatus(int code, String reason) {
        this.code   = code;
        this.reason = reason;
    }

    public int    code()   { return code; }
    public String reason() { return reason; }

    public boolean isSuccess() { return code >= 200 && code < 300; }
    public boolean isError()   { return code >= 400; }
}

// Java 14+ switch expression — exhaustiveness verified at compile time
String describe(HttpStatus s) {
    return switch (s) {
        case OK, CREATED                             -> "success";
        case BAD_REQUEST, NOT_FOUND                  -> "client error";
        case INTERNAL_SERVER_ERROR                   -> "server error";
    };
}

How do you build immutable collections?

Java 9 (JEP 269) introduced List.of, Set.of, and Map.of factory methods. They return collections that throw UnsupportedOperationException on every modifying call, hold no nulls, and are space-efficient compared to Collections.unmodifiableList(new ArrayList<>(...)).

java
// Java 9+ — preferred
public static final List<String>          SUPPORTED_FORMATS =
    List.of("png", "jpg", "webp", "gif");
public static final Set<String>           RESERVED_WORDS    =
    Set.of("class", "interface", "enum", "record");
public static final Map<String, Integer>  STATUS_CODES      = Map.of(
    "OK",        200,
    "NOT_FOUND", 404,
    "ERROR",     500
);

// Pre-Java 9 fallback
public static final List<String> LEGACY = Collections.unmodifiableList(
    Arrays.asList("a", "b", "c")
);

For records of constant data, Java 16's record keyword gives you an immutable value type with auto-generated equals, hashCode, toString, and accessors:

java
public record Point(int x, int y) {}

public static final Point ORIGIN     = new Point(0, 0);
public static final Point UNIT_RIGHT = new Point(1, 0);

Which built-in constants should you know?

Java spreads its constants across wrapper classes and the Math class. The integer and floating-point limit constants on Integer, Long, Double et al. are the ones you reach for most:

java
Math.PI               // 3.141592653589793
Math.E                // 2.718281828459045

Integer.MAX_VALUE     // 2_147_483_647
Integer.MIN_VALUE     // -2_147_483_648
Long.MAX_VALUE        // 9_223_372_036_854_775_807L

Double.MAX_VALUE      // ~1.8e308
Double.MIN_VALUE      // ~4.9e-324  (smallest positive — not most negative)
Double.POSITIVE_INFINITY
Double.NEGATIVE_INFINITY
Double.NaN            // use Double.isNaN(x), never == Double.NaN

Boolean.TRUE
Boolean.FALSE

Collections.EMPTY_LIST
Collections.EMPTY_MAP
Collections.EMPTY_SET

Practical patterns and gotchas

  • Constants don't go on interfaces by default anymore. The old "constants interface" antipattern (an interface containing only static final fields) is officially discouraged — see Effective Java Item 22. Put constants on a final, non-instantiable class.
  • List.of rejects nulls: passing a null element throws NullPointerException at construction. Use the older Arrays.asList wrapped in Collections.unmodifiableList if you genuinely need nullable entries.
  • Recompile dependents on constant changes: if you change a public static final primitive or String in a library, every downstream class must be rebuilt to pick up the new value. Use a static getter for values that may evolve.
  • Switch over enums is exhaustive (Java 14+): switch expressions force you to handle every variant, eliminating the "added an enum case but forgot to update the switch" bug class.

For Java's environment variable story, including Spring Boot binding, see environment variables in Java. For language-agnostic naming and immutability, see constants best practices.

Was this helpful?

Read next

Constants in Python: typing.Final, Enum & SCREAMING_SNAKE

Python has no const keyword — constants are SCREAMING_SNAKE_CASE by PEP 8 convention. typing.Final (Python 3.8) adds static-checker enforcement; the enum module handles closed sets.

Continue →

Frequently Asked Questions

Why does javac inline static final constants?

When a static final field is a primitive or String initialized with a constant expression, javac writes the literal value at every reference, not a lookup. Fast at runtime, but with a binary-compatibility catch: if you change the value in a library, every consuming class must be recompiled to pick up the new value.

Should I put constants on an interface?

No. The "constants interface" antipattern is officially discouraged — see Effective Java Item 22. Put constants on a final, non-instantiable class with a private constructor. Implementing an interface to inherit constants pollutes the API of every implementer.

What is the difference between List.of and Collections.unmodifiableList?

List.of (Java 9+, JEP 269) returns a truly unmodifiable, space-efficient list that rejects nulls. Collections.unmodifiableList wraps a mutable backing list — modifications via the original reference still affect the "unmodifiable" view. Prefer List.of for new code.

When should I use Java records for constants?

Records (Java 16+) give you immutable value types with auto-generated equals, hashCode, toString, and accessors. Use them for constant value objects: record Point(int x, int y) {} replaces a 30-line class with one line. Pair with public static final instances for named constants of the record type.

Stay up to date

Get notified about new guides, tools, and cheatsheets.