Constants #
A constant is a value that never changes while the program runs. In Java, a constant isn’t a special type of its own — it’s a combination of two keywords: static and final. final prevents the value from being changed after initialization, while static ensures there’s only one copy for the whole class. Using constants with meaningful names — instead of numbers or string literals scattered throughout the code — makes code easier to understand, modify, and test. This article covers how to declare and use constants, when enum is the better choice, anti-patterns to avoid, and the built-in constants already available in the Java standard library.
Declaring Constants #
Constants in Java are declared with the public static final combination and names written in UPPER_SNAKE_CASE. This naming convention is a universal signal to Java developers that the value won’t change.
public class AppConfig {
// Numeric constants
public static final int MAX_LOGIN_ATTEMPTS = 3;
public static final int CONNECTION_TIMEOUT_MS = 5_000;
public static final double TAX_RATE = 0.11; // VAT 11%
public static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
// String constants
public static final String DEFAULT_CHARSET = "UTF-8";
public static final String DATE_FORMAT = "yyyy-MM-dd";
public static final String API_VERSION = "v2";
// Boolean constants
public static final boolean DEBUG_MODE = false;
}
Usage in another class:
// Access via the class name — it's clear where the constant comes from
if (loginAttempts >= AppConfig.MAX_LOGIN_ATTEMPTS) {
lockAccount(username);
}
long fileSize = file.length();
if (fileSize > AppConfig.MAX_FILE_SIZE) {
throw new IllegalArgumentException("File size exceeds the limit");
}
Why Constants Instead of Magic Numbers #
A magic number is a numeric or string literal that appears directly in code without explanation. It’s one of the most common anti-patterns in Java — and constants are the solution.
// ANTI-PATTERN: magic numbers — what do 3, 5000, 0.11 mean?
public void processLogin(String user, String pass) {
if (failedAttempts >= 3) { // ✗ why 3?
lockAccount(user);
}
}
public double calculateTotal(double price) {
return price + (price * 0.11); // ✗ what is 0.11?
}
public boolean validateFile(File f) {
return f.length() <= 10485760; // ✗ 10485760 bytes = how many MB?
}
// CORRECT: constants with meaningful names
public void processLogin(String user, String pass) {
if (failedAttempts >= MAX_LOGIN_ATTEMPTS) { // ✓ clear
lockAccount(user);
}
}
public double calculateTotal(double price) {
return price + (price * TAX_RATE); // ✓ clear
}
public boolean validateFile(File f) {
return f.length() <= MAX_FILE_SIZE; // ✓ clear
}
The real advantages of constants over magic numbers:
| Aspect | Magic Number | Constant |
|---|---|---|
| Readability | Readers must guess the meaning of 0.11 | TAX_RATE explains itself |
| Maintenance | Must search-and-replace across the whole codebase | Change in one place, applies everywhere |
| Safety | Prone to typos (0.011 vs 0.11) | The compiler catches a wrong name |
| Documentation | No context | Name + Javadoc can explain the details |
Initialization: Three Ways #
There are three places where a final field can be initialized, each for a different need.
1. Directly at Declaration (Most Common) #
public class MathConstants {
public static final double PI = 3.141592653589793;
public static final double E = 2.718281828459045;
public static final double SQRT2 = 1.4142135623730951;
}
2. In a Static Initializer Block #
Used when the constant’s value requires logic or computation that can’t be written in a single expression:
public class SystemConfig {
public static final String HOSTNAME;
public static final int PORT;
static {
// Read from an environment variable, fall back to a default
String host = System.getenv("APP_HOST");
HOSTNAME = (host != null && !host.isEmpty()) ? host : "localhost";
String port = System.getenv("APP_PORT");
PORT = (port != null) ? Integer.parseInt(port) : 8080;
}
}
3. In the Constructor (Blank Final) #
A final instance field not initialized at declaration is called a blank final — it must be assigned in the constructor, and after that it can’t be changed. Useful for values that differ per object but never change for the object’s lifetime:
public class Token {
// Each token has a unique value that never changes
private final String value;
private final long createdAt;
private final long expiresAt;
public Token(String value, long durationMs) {
this.value = value;
this.createdAt = System.currentTimeMillis();
this.expiresAt = this.createdAt + durationMs;
}
public String getValue() { return value; }
public boolean isExpired() {
return System.currentTimeMillis() > expiresAt;
}
// No setter — a token's value can't be changed after creation
}
flowchart TD
A{"Does the constant value\nneed logic\nor computation?"} -- Yes --> B{"Depends on\nruntime conditions\nlike env vars?"}
A -- No --> C["Initialize directly\npublic static final X = value"]
B -- Yes --> D["Static initializer block\nstatic { ... }"]
B -- No --> C
A -- "Differs per object\nbut never changes" --> E["Blank final in the constructor\nprivate final X;\nthis.x = value"]
style C color:#fff,stroke:#16a34a,stroke-width:2px
style D color:#fff,stroke:#3b82f6,stroke-width:2px
style E color:#fff,stroke:#7c3aed,stroke-width:2pxenum as Structured Constants
#
For a group of related constants, enum is a far better choice than a set of static final fields. enum provides type safety — the compiler rejects values that aren’t enum members — and can carry data and methods.
Plain Constants vs Enum #
// ANTI-PATTERN: int constants for status — no type safety
public class OrderStatus {
public static final int PENDING = 1;
public static final int PROCESSING = 2;
public static final int SHIPPED = 3;
public static final int COMPLETED = 4;
public static final int CANCELLED = 5;
}
// Nothing prevents this from compiling:
void updateStatus(int status) { ... }
updateStatus(999); // ✗ invalid value, but compiles
updateStatus(-1); // ✗ same
// CORRECT: enum with type safety
public enum OrderStatus {
PENDING, PROCESSING, SHIPPED, COMPLETED, CANCELLED
}
void updateStatus(OrderStatus status) { ... }
updateStatus(OrderStatus.SHIPPED); // ✓
// updateStatus(999); // ✗ COMPILE ERROR — must be an OrderStatus
Enum with Data and Methods #
enum can carry data and logic directly tied to each constant:
public enum PriorityLevel {
LOW ("Low", 1, 72), // label, weight, sla hours
MEDIUM ("Medium", 2, 24),
HIGH ("High", 3, 4),
CRITICAL ("Critical", 4, 1);
private final String label;
private final int weight;
private final int slaHours;
PriorityLevel(String label, int weight, int slaHours) {
this.label = label;
this.weight = weight;
this.slaHours = slaHours;
}
public String getLabel() { return label; }
public int getWeight() { return weight; }
public int getSlaHours() { return slaHours; }
public boolean exceedsDeadline(long hoursElapsed) {
return hoursElapsed > slaHours;
}
}
// Usage
PriorityLevel p = PriorityLevel.CRITICAL;
System.out.println(p.getLabel()); // Critical
System.out.println(p.getSlaHours()); // 1
System.out.println(p.exceedsDeadline(2)); // true
// Iterate over all enum values
for (PriorityLevel level : PriorityLevel.values()) {
System.out.printf("%-8s weight=%d sla=%d hours%n",
level.getLabel(), level.getWeight(), level.getSlaHours());
}
Comparing Constant Approaches #
| Approach | Type Safety | Can Carry Data | Iteration | Best For |
|---|---|---|---|---|
static final primitives | ✗ | ✗ | ✗ | Unrelated single values |
static final String | ✗ | ✗ | ✗ | Configuration, string formats |
Simple enum | ✓ | ✗ | ✓ | Statuses, categories, options |
enum with fields | ✓ | ✓ | ✓ | Constants that have attributes |
The Constant Interface Anti-Pattern #
Older versions of this article showed constants inside an interface as something common. This is actually an anti-pattern known as the Constant Interface Anti-Pattern and has long been discouraged.
// ANTI-PATTERN: Constant Interface — don't do this
public interface GlobalConstants {
double PI = 3.14159; // implicitly public static final
int MAX_AGE = 100;
String CHARSET = "UTF-8";
}
// A class implements the interface only to access the constants
public class Calculator implements GlobalConstants {
public double calculateArea(double r) {
return PI * r * r; // ✗ looks like PI is a contract of this class
}
}
The problem: implements should declare a behavioral contract (an is-a relationship), not access to constants. A Calculator isn’t “a GlobalConstants”. Besides, all interface constants become part of the class’s public API — hard to remove in the future without a breaking change.
// CORRECT: use a utility class with a private constructor
public final class Constants {
private Constants() { } // prevent instantiation
// Group by domain
public static final class Http {
private Http() { }
public static final int TIMEOUT_MS = 5_000;
public static final int MAX_RETRY = 3;
public static final String USER_AGENT = "MyApp/1.0";
}
public static final class Validation {
private Validation() { }
public static final int MAX_NAME_LENGTH = 100;
public static final int MIN_PASSWORD_LENGTH = 8;
public static final String EMAIL_REGEX = "^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$";
}
}
// Usage — the origin is clear
if (name.length() > Constants.Validation.MAX_NAME_LENGTH) {
throw new IllegalArgumentException("Name is too long");
}
Immutability: final on Reference Types
#
It’s important to understand that final on a reference type only locks the reference, not the contents. This is a common source of confusion.
public class FinalReferenceExample {
// final on a List: the reference can't be reassigned, but the contents can change
public static final List<String> CITY_LIST = new ArrayList<>();
static {
CITY_LIST.add("Jakarta");
CITY_LIST.add("Surabaya");
}
public static void main(String[] args) {
// CITY_LIST = new ArrayList<>(); // ✗ COMPILE ERROR — the reference can't be reassigned
CITY_LIST.add("Bandung"); // ✓ contents can change — this might be undesired!
System.out.println(CITY_LIST); // [Jakarta, Surabaya, Bandung]
}
}
For collections that truly can’t be modified, use immutable collections:
public static final List<String> CITY_LIST = List.of(
"Jakarta", "Surabaya", "Bandung", "Medan"
);
// CITY_LIST.add("Bali"); // ✗ UnsupportedOperationException at runtime
public static final Map<String, Integer> PROVINCE_CODES = Map.of(
"DKI Jakarta", 31,
"West Java", 32,
"Central Java", 33
);
public static final Set<String> IMAGE_EXTENSIONS = Set.of(
".jpg", ".jpeg", ".png", ".webp", ".gif"
);
flowchart LR
subgraph "final List<String> list"
A["Variable list\n(reference)"] -->|"can't be reassigned"| B["ArrayList object\non the heap"]
B -->|"can be modified"| C["'Jakarta'\n'Surabaya'\n'Bandung'"]
end
subgraph "List.of(...)"
D["Variable list2\n(reference)"] -->|"can't be reassigned"| E["ImmutableList\non the heap"]
E -->|"can't be modified"| F["'Jakarta'\n'Surabaya'"]
end
style A color:#000,stroke:#f59e0b,stroke-width:2px
style D color:#fff,stroke:#16a34a,stroke-width:2pxBuilt-in Standard Library Constants #
Java provides many constants already defined in the standard library — no need to redefine them.
| Constant | Location | Value |
|---|---|---|
Math.PI | java.lang.Math | 3.141592653589793 |
Math.E | java.lang.Math | 2.718281828459045 |
Integer.MAX_VALUE | java.lang.Integer | 2,147,483,647 |
Integer.MIN_VALUE | java.lang.Integer | -2,147,483,648 |
Long.MAX_VALUE | java.lang.Long | 9,223,372,036,854,775,807 |
Double.MAX_VALUE | java.lang.Double | ~1.8 × 10³⁰⁸ |
Double.NaN | java.lang.Double | Not a Number |
Double.POSITIVE_INFINITY | java.lang.Double | ∞ |
Integer.MAX_VALUE | java.lang.Integer | Upper bound of int |
System.lineSeparator() | java.lang.System | \n or \r\n |
// ANTI-PATTERN: redefining constants that already exist
public static final double PI = 3.14; // ✗ less precise than Math.PI
public static final int MAX_INT = 2147483647; // ✗ use Integer.MAX_VALUE
// CORRECT: use what's already available
double circumference = 2 * Math.PI * radius; // ✓
int[] arr = new int[Integer.MAX_VALUE / 2]; // ✓ more readable than a literal
// Check NaN correctly
double result = 0.0 / 0.0;
// ANTI-PATTERN: comparing NaN with ==
if (result == Double.NaN) { } // ✗ always false, NaN != NaN
// CORRECT: use Double.isNaN()
if (Double.isNaN(result)) { } // ✓
Summary #
public static final+UPPER_SNAKE_CASE— the standard combination for class constants in Java;staticso there’s only one copy,finalso it can’t change, and capital letters as a visual signal.- Eliminate magic numbers — every numeric or string literal appearing more than once in code is a constant candidate; a meaningful name is far easier to understand and change than a literal value.
enumfor groups of related constants — useenuminstead ofstatic final intwhen constants represent a set of choices;enumprovides type safety and can carry data and methods.- Three initialization ways — directly at declaration (most common), in a static initializer block (for logic/env vars), or in the constructor as a blank final (for unique per-object values that never change).
- Constant Interface is an anti-pattern — don’t
implementsan interface just to access constants; use a final utility class with a private constructor and inner classes per domain.finalon a reference ≠ immutable — afinal Listdoesn’t prevent.add()or.remove(); useList.of(),Map.of(),Set.of()for collections that truly can’t be modified.- Don’t redefine standard library constants —
Math.PI,Integer.MAX_VALUE,Double.NaN, and others already exist; use them instead of defining less precise versions.