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:

AspectMagic NumberConstant
ReadabilityReaders must guess the meaning of 0.11TAX_RATE explains itself
MaintenanceMust search-and-replace across the whole codebaseChange in one place, applies everywhere
SafetyProne to typos (0.011 vs 0.11)The compiler catches a wrong name
DocumentationNo contextName + 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:2px

enum 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 #

ApproachType SafetyCan Carry DataIterationBest For
static final primitivesUnrelated single values
static final StringConfiguration, string formats
Simple enumStatuses, categories, options
enum with fieldsConstants 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&lt;String&gt; 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:2px

Built-in Standard Library Constants #

Java provides many constants already defined in the standard library — no need to redefine them.

ConstantLocationValue
Math.PIjava.lang.Math3.141592653589793
Math.Ejava.lang.Math2.718281828459045
Integer.MAX_VALUEjava.lang.Integer2,147,483,647
Integer.MIN_VALUEjava.lang.Integer-2,147,483,648
Long.MAX_VALUEjava.lang.Long9,223,372,036,854,775,807
Double.MAX_VALUEjava.lang.Double~1.8 × 10³⁰⁸
Double.NaNjava.lang.DoubleNot a Number
Double.POSITIVE_INFINITYjava.lang.Double
Integer.MAX_VALUEjava.lang.IntegerUpper 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; static so there’s only one copy, final so 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.
  • enum for groups of related constants — use enum instead of static final int when constants represent a set of choices; enum provides 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 implements an interface just to access constants; use a final utility class with a private constructor and inner classes per domain.
  • final on a reference ≠ immutable — a final List doesn’t prevent .add() or .remove(); use List.of(), Map.of(), Set.of() for collections that truly can’t be modified.
  • Don’t redefine standard library constantsMath.PI, Integer.MAX_VALUE, Double.NaN, and others already exist; use them instead of defining less precise versions.

← Previous: Variables   Next: Data Types →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact