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.
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.
// 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:
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 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:
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:
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_SETPractical patterns and gotchas
- Constants don't go on interfaces by default anymore. The old "constants interface" antipattern (an interface containing only
static finalfields) 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
NullPointerExceptionat construction. Use the olderArrays.asListwrapped inCollections.unmodifiableListif you genuinely need nullable entries. - Recompile dependents on constant changes: if you change a public
static finalprimitive 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.