Static and Final keywords




1. What is the static Keyword?

The static keyword in Java means “belonging to the class itself, not to any particular instance.” When you declare a variable, method, block, or inner class as static, it is associated with the class as a whole rather than with individual objects created from that class.

Real-world analogy: Think of a school classroom. Every student (instance) has their own notebook (instance variable), but the whiteboard on the wall (static variable) is shared by everyone in the class. There is only one whiteboard, and if the teacher writes something on it, every student can see it. You do not need a specific student to access the whiteboard — you just walk into the classroom and look at it.

This is exactly how static works in Java. A static member belongs to the classroom (the class), not to any individual student (object). It exists in memory once, it is loaded when the class is first loaded by the JVM, and it can be accessed without creating any object.

Here are the key characteristics of the static keyword:

  • Shared across all instances — there is only one copy in memory, regardless of how many objects you create
  • Loaded when the class is loaded — static members exist before any object is created
  • Accessed via the class name — you do not need an object reference (though Java allows it, it is bad practice)
  • Cannot access instance members directly — static context has no this reference

The static keyword can be applied to four things in Java:

Applied To What It Creates Example
Variable Class variable (shared by all instances) static int count;
Method Class method (callable without an object) static void printInfo()
Block Static initializer (runs once at class loading) static { ... }
Inner Class Static nested class (no outer instance needed) static class Entry { }



2. Static Variables (Class Variables)

A static variable (also called a class variable) is a variable declared with the static keyword inside a class but outside any method or constructor. Unlike instance variables, which get their own copy for each object, a static variable has exactly one copy shared across all instances of the class.

Static variables are stored in the method area (or metaspace in modern JVMs), not on the heap with individual objects. They are initialized when the class is first loaded, and they persist for the entire lifetime of the application.

When to use static variables:

  • Counting how many instances have been created
  • Storing configuration values shared by all instances
  • Defining constants (combined with final)
  • Caching data that all objects should share

Let us start with a simple example that demonstrates the difference between static and instance variables:

public class Employee {

    // Instance variable -- each Employee gets its own copy
    private String name;

    // Static variable -- shared across ALL Employee objects
    private static int employeeCount = 0;

    // Static variable -- company name is the same for all employees
    private static String companyName = "TechCorp";

    public Employee(String name) {
        this.name = name;
        employeeCount++;  // Increment the shared counter
    }

    public String getName() {
        return name;
    }

    public static int getEmployeeCount() {
        return employeeCount;
    }

    public static String getCompanyName() {
        return companyName;
    }

    public static void main(String[] args) {
        System.out.println("Employees before: " + Employee.getEmployeeCount());
        // Output: Employees before: 0

        Employee emp1 = new Employee("Alice");
        Employee emp2 = new Employee("Bob");
        Employee emp3 = new Employee("Charlie");

        System.out.println("Employees after: " + Employee.getEmployeeCount());
        // Output: Employees after: 3

        // All employees share the same company name
        System.out.println(emp1.getName() + " works at " + Employee.getCompanyName());
        // Output: Alice works at TechCorp
        System.out.println(emp2.getName() + " works at " + Employee.getCompanyName());
        // Output: Bob works at TechCorp
    }
}

In the example above, employeeCount is incremented every time a new Employee is created. Because it is static, all three objects share the same counter. If it were an instance variable, each object would have its own counter stuck at 1.

Here is a memory visualization to make this concrete:

Memory Area Variable Value
Method Area (shared) Employee.employeeCount 3
Method Area (shared) Employee.companyName “TechCorp”
Heap (emp1 object) name “Alice”
Heap (emp2 object) name “Bob”
Heap (emp3 object) name “Charlie”

A common real-world use of static variables is generating unique IDs:

public class Order {

    private static int nextOrderId = 1000;  // Shared counter for ID generation

    private final int orderId;
    private final String product;
    private final double amount;

    public Order(String product, double amount) {
        this.orderId = nextOrderId++;  // Assign and increment atomically
        this.product = product;
        this.amount = amount;
    }

    @Override
    public String toString() {
        return "Order{id=" + orderId + ", product='" + product + "', amount=$" + amount + "}";
    }

    public static void main(String[] args) {
        Order order1 = new Order("Laptop", 999.99);
        Order order2 = new Order("Mouse", 29.99);
        Order order3 = new Order("Keyboard", 79.99);

        System.out.println(order1);  // Order{id=1000, product='Laptop', amount=$999.99}
        System.out.println(order2);  // Order{id=1001, product='Mouse', amount=$29.99}
        System.out.println(order3);  // Order{id=1002, product='Keyboard', amount=$79.99}
    }
}

Important caveat: In a multi-threaded application, the nextOrderId++ pattern above is not thread-safe because the increment operation is not atomic. In production code, you would use AtomicInteger instead:

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadSafeOrder {

    // Thread-safe static counter
    private static final AtomicInteger nextOrderId = new AtomicInteger(1000);

    private final int orderId;

    public ThreadSafeOrder() {
        this.orderId = nextOrderId.getAndIncrement();
    }

    public int getOrderId() {
        return orderId;
    }
}



3. Static Methods

A static method belongs to the class, not to any instance. You call it using the class name, and it does not have access to this or any instance variables/methods. Static methods can only directly access other static members.

Rules for static methods:

  • Can be called without creating an object: ClassName.methodName()
  • Cannot use this or super keywords
  • Cannot access instance variables or instance methods directly
  • Can access other static variables and static methods
  • Cannot be overridden (they can be hidden, but that is different from polymorphic overriding)

You use static methods every day in Java without realizing it. Math.sqrt(), Integer.parseInt(), Collections.sort(), Arrays.asList() — these are all static methods. You never create a Math object to calculate a square root.

Utility Class Pattern

One of the most common uses of static methods is the utility class — a class that contains only static methods and has no state. Utility classes are never instantiated.

public final class StringUtils {

    // Private constructor prevents instantiation
    private StringUtils() {
        throw new UnsupportedOperationException("Utility class cannot be instantiated");
    }

    /**
     * Checks if a string is null or empty (after trimming whitespace).
     */
    public static boolean isBlank(String str) {
        return str == null || str.trim().isEmpty();
    }

    /**
     * Capitalizes the first letter of a string.
     */
    public static String capitalize(String str) {
        if (isBlank(str)) {
            return str;
        }
        return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase();
    }

    /**
     * Reverses a string.
     */
    public static String reverse(String str) {
        if (str == null) {
            return null;
        }
        return new StringBuilder(str).reverse().toString();
    }

    /**
     * Truncates a string to the specified length, adding "..." if truncated.
     */
    public static String truncate(String str, int maxLength) {
        if (str == null || str.length() <= maxLength) {
            return str;
        }
        return str.substring(0, maxLength) + "...";
    }

    public static void main(String[] args) {
        System.out.println(StringUtils.isBlank(""));         // true
        System.out.println(StringUtils.isBlank("  "));       // true
        System.out.println(StringUtils.isBlank("hello"));    // false

        System.out.println(StringUtils.capitalize("hello")); // Hello
        System.out.println(StringUtils.reverse("Java"));     // avaJ
        System.out.println(StringUtils.truncate("Hello, World!", 5)); // Hello...
    }
}

Notice the key patterns in the utility class above: the class is final (cannot be extended), the constructor is private (cannot be instantiated), and all methods are static.

Static Factory Methods

Another important use of static methods is the factory method pattern. Instead of calling new directly, you call a static method that creates and returns an object. This has several advantages over constructors: you can give the method a descriptive name, you can return a cached instance, and you can return a subclass.

public class DatabaseConnection {

    private final String host;
    private final int port;
    private final String database;
    private final boolean ssl;

    // Private constructor -- clients cannot call new DatabaseConnection()
    private DatabaseConnection(String host, int port, String database, boolean ssl) {
        this.host = host;
        this.port = port;
        this.database = database;
        this.ssl = ssl;
    }

    // Static factory method -- descriptive name tells you what it creates
    public static DatabaseConnection forProduction(String database) {
        return new DatabaseConnection("prod-db.company.com", 5432, database, true);
    }

    // Another static factory method
    public static DatabaseConnection forDevelopment(String database) {
        return new DatabaseConnection("localhost", 5432, database, false);
    }

    // Another static factory method
    public static DatabaseConnection forTesting() {
        return new DatabaseConnection("localhost", 5432, "test_db", false);
    }

    @Override
    public String toString() {
        return String.format("Connection{host='%s', port=%d, db='%s', ssl=%s}",
                host, port, database, ssl);
    }

    public static void main(String[] args) {
        DatabaseConnection prod = DatabaseConnection.forProduction("users_db");
        DatabaseConnection dev  = DatabaseConnection.forDevelopment("users_db");
        DatabaseConnection test = DatabaseConnection.forTesting();

        System.out.println(prod);
        // Connection{host='prod-db.company.com', port=5432, db='users_db', ssl=true}
        System.out.println(dev);
        // Connection{host='localhost', port=5432, db='users_db', ssl=false}
        System.out.println(test);
        // Connection{host='localhost', port=5432, db='test_db', ssl=false}
    }
}

Why Static Methods Cannot Access Instance Members

This is one of the most common compiler errors beginners encounter. A static method exists without any object, so there is no this -- and therefore no way to know which object's instance variables to access.

public class StaticAccessDemo {

    private String instanceVar = "I belong to an instance";
    private static String staticVar = "I belong to the class";

    // STATIC method
    public static void staticMethod() {
        System.out.println(staticVar);     // OK -- accessing static from static

        // System.out.println(instanceVar); // COMPILE ERROR: non-static variable
        //                                  // cannot be referenced from a static context

        // System.out.println(this);        // COMPILE ERROR: 'this' cannot be
        //                                  // referenced from a static context
    }

    // INSTANCE method -- can access everything
    public void instanceMethod() {
        System.out.println(instanceVar);   // OK -- accessing instance from instance
        System.out.println(staticVar);     // OK -- accessing static from instance
        staticMethod();                    // OK -- calling static from instance
    }

    public static void main(String[] args) {
        // Static method -- called on the class
        StaticAccessDemo.staticMethod();

        // Instance method -- requires an object
        StaticAccessDemo obj = new StaticAccessDemo();
        obj.instanceMethod();
    }
}



4. Static Blocks (Static Initializers)

A static block (also called a static initializer) is a block of code enclosed in static { } that runs exactly once when the class is first loaded by the JVM. It runs before any constructor, before any static method is called, and before any object is created.

Static blocks are used for complex initialization of static variables -- things that cannot be done in a single line, such as loading configuration files, registering database drivers, populating lookup tables, or handling exceptions during initialization.

Key facts about static blocks:

  • Execute exactly once, when the class is loaded
  • Execute in the order they appear in the source code
  • Run before any constructor or instance initializer
  • Cannot throw checked exceptions (must handle them internally)
  • A class can have multiple static blocks -- they run in order
import java.util.*;

public class CountryLookup {

    // Static map that will be populated in the static block
    private static final Map COUNTRY_CODES;
    private static final Map COUNTRY_NAMES;

    // Static block -- runs once when CountryLookup class is loaded
    static {
        System.out.println("Static block 1: Initializing country codes...");

        Map codes = new HashMap<>();
        codes.put("US", "United States");
        codes.put("GB", "United Kingdom");
        codes.put("DE", "Germany");
        codes.put("FR", "France");
        codes.put("JP", "Japan");
        codes.put("AU", "Australia");
        codes.put("BR", "Brazil");
        codes.put("IN", "India");

        COUNTRY_CODES = Collections.unmodifiableMap(codes);
    }

    // Second static block -- runs after the first one
    static {
        System.out.println("Static block 2: Building reverse lookup...");

        Map names = new HashMap<>();
        for (Map.Entry entry : COUNTRY_CODES.entrySet()) {
            names.put(entry.getValue().toLowerCase(), entry.getKey());
        }

        COUNTRY_NAMES = Collections.unmodifiableMap(names);
    }

    public static String getCountryName(String code) {
        return COUNTRY_CODES.getOrDefault(code.toUpperCase(), "Unknown");
    }

    public static String getCountryCode(String name) {
        return COUNTRY_NAMES.getOrDefault(name.toLowerCase(), "Unknown");
    }

    public static void main(String[] args) {
        // The static blocks run BEFORE main executes
        System.out.println("--- main() starts ---");

        System.out.println(getCountryName("US"));         // United States
        System.out.println(getCountryName("JP"));         // Japan
        System.out.println(getCountryCode("Germany"));    // DE
        System.out.println(getCountryCode("Brazil"));     // BR
    }
}

// Output:
// Static block 1: Initializing country codes...
// Static block 2: Building reverse lookup...
// --- main() starts ---
// United States
// Japan
// DE
// BR

Notice in the output above that both static blocks ran before main() started executing. This is because main() is a static method of the class, and the class must be loaded before any of its methods can be called.

Here is a practical example showing a common real-world use case -- loading a JDBC driver and reading a configuration file:

import java.io.*;
import java.util.Properties;

public class AppConfig {

    private static final Properties CONFIG = new Properties();
    private static final String APP_VERSION;

    static {
        // Load configuration file
        try (InputStream input = AppConfig.class
                .getClassLoader()
                .getResourceAsStream("application.properties")) {

            if (input == null) {
                System.err.println("Warning: application.properties not found, using defaults");
                CONFIG.setProperty("db.host", "localhost");
                CONFIG.setProperty("db.port", "5432");
                CONFIG.setProperty("db.name", "myapp");
                CONFIG.setProperty("app.version", "1.0.0-default");
            } else {
                CONFIG.load(input);
            }
        } catch (IOException e) {
            throw new ExceptionInInitializerError("Failed to load configuration: " + e.getMessage());
        }

        APP_VERSION = CONFIG.getProperty("app.version", "unknown");
    }

    public static String get(String key) {
        return CONFIG.getProperty(key);
    }

    public static String get(String key, String defaultValue) {
        return CONFIG.getProperty(key, defaultValue);
    }

    public static String getVersion() {
        return APP_VERSION;
    }
}

Execution Order: Static Blocks, Instance Blocks, and Constructors

Understanding the execution order is critical for debugging initialization issues:

public class InitializationOrder {

    // 1. Static variable initialization (in order)
    private static String staticVar = initStaticVar();

    // 2. Static block (in order with other static blocks)
    static {
        System.out.println("2. Static block runs");
    }

    // 4. Instance variable initialization (in order)
    private String instanceVar = initInstanceVar();

    // 5. Instance initializer block
    {
        System.out.println("5. Instance initializer block runs");
    }

    // 6. Constructor
    public InitializationOrder() {
        System.out.println("6. Constructor runs");
    }

    private static String initStaticVar() {
        System.out.println("1. Static variable initialized");
        return "static";
    }

    private String initInstanceVar() {
        System.out.println("4. Instance variable initialized");
        return "instance";
    }

    public static void main(String[] args) {
        System.out.println("3. main() starts");
        System.out.println("--- Creating first object ---");
        new InitializationOrder();
        System.out.println("--- Creating second object ---");
        new InitializationOrder();
    }
}

// Output:
// 1. Static variable initialized
// 2. Static block runs
// 3. main() starts
// --- Creating first object ---
// 4. Instance variable initialized
// 5. Instance initializer block runs
// 6. Constructor runs
// --- Creating second object ---
// 4. Instance variable initialized
// 5. Instance initializer block runs
// 6. Constructor runs

Notice that the static members (steps 1 and 2) run only once, while the instance members (steps 4, 5, and 6) run every time a new object is created.



5. Static Inner Classes (Static Nested Classes)

A static inner class (formally called a static nested class) is a class defined inside another class with the static modifier. Unlike a regular inner class, it does not need an instance of the outer class to be created. It is essentially a top-level class that is logically grouped inside another class for organizational purposes.

Key differences between static and non-static inner classes:

Feature Static Nested Class Inner Class (Non-Static)
Outer instance required? No Yes
Can access outer instance members? No (only static members) Yes (all members)
Creation syntax new Outer.Inner() outer.new Inner()
Has reference to outer class? No Yes (hidden reference)
Memory leak risk? Low Higher (holds outer reference)

The most famous example in the Java standard library is Map.Entry. You do not need a Map instance to create an Entry -- it is a static nested interface inside Map.

One of the most common uses in application code is the Builder pattern:

public class HttpRequest {

    private final String url;
    private final String method;
    private final Map headers;
    private final String body;
    private final int timeoutMs;

    // Private constructor -- only the Builder can create HttpRequest objects
    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = Collections.unmodifiableMap(builder.headers);
        this.body = builder.body;
        this.timeoutMs = builder.timeoutMs;
    }

    // Static nested class -- does NOT need an HttpRequest instance to exist
    public static class Builder {
        // Required
        private final String url;

        // Optional with defaults
        private String method = "GET";
        private Map headers = new HashMap<>();
        private String body = null;
        private int timeoutMs = 30000;

        public Builder(String url) {
            this.url = url;
        }

        public Builder method(String method) {
            this.method = method;
            return this;
        }

        public Builder header(String key, String value) {
            this.headers.put(key, value);
            return this;
        }

        public Builder body(String body) {
            this.body = body;
            return this;
        }

        public Builder timeout(int ms) {
            this.timeoutMs = ms;
            return this;
        }

        public HttpRequest build() {
            return new HttpRequest(this);
        }
    }

    @Override
    public String toString() {
        return method + " " + url + " (timeout=" + timeoutMs + "ms, headers=" + headers + ")";
    }

    public static void main(String[] args) {
        // Builder is created without an HttpRequest instance
        HttpRequest request = new HttpRequest.Builder("https://api.example.com/users")
                .method("POST")
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer token123")
                .body("{\"name\": \"Alice\"}")
                .timeout(5000)
                .build();

        System.out.println(request);
        // POST https://api.example.com/users (timeout=5000ms, headers={Authorization=Bearer token123, Content-Type=application/json})
    }
}

Here is another practical example -- a linked list node as a static nested class:

public class LinkedList {

    private Node head;
    private int size;

    // Static nested class -- Node does not need access to LinkedList's instance members
    // The type parameter is independent of LinkedList's type parameter
    private static class Node {
        E data;
        Node next;

        Node(E data) {
            this.data = data;
            this.next = null;
        }
    }

    public void addFirst(T element) {
        Node newNode = new Node<>(element);
        newNode.next = head;
        head = newNode;
        size++;
    }

    public T getFirst() {
        if (head == null) {
            throw new NoSuchElementException("List is empty");
        }
        return head.data;
    }

    public int size() {
        return size;
    }

    public static void main(String[] args) {
        LinkedList list = new LinkedList<>();
        list.addFirst("Charlie");
        list.addFirst("Bob");
        list.addFirst("Alice");

        System.out.println("First: " + list.getFirst());  // First: Alice
        System.out.println("Size: " + list.size());        // Size: 3
    }
}



6. Static Import

A static import allows you to use static members (fields and methods) of a class without qualifying them with the class name. Instead of writing Math.PI and Math.sqrt(), you can write PI and sqrt() directly.

Static imports were introduced in Java 5. They are useful for reducing verbosity when you frequently use static constants or utility methods from a specific class.

Syntax:

  • import static java.lang.Math.PI; -- imports a single static member
  • import static java.lang.Math.*; -- imports all static members from the class
// Without static import
public class CircleCalcVerbose {
    public static void main(String[] args) {
        double radius = 5.0;
        double area = Math.PI * Math.pow(radius, 2);
        double circumference = 2 * Math.PI * radius;
        double diagonal = Math.sqrt(Math.pow(radius, 2) + Math.pow(radius, 2));

        System.out.println("Area: " + area);
        System.out.println("Circumference: " + circumference);
        System.out.println("Diagonal: " + diagonal);
    }
}

// With static import -- much cleaner for math-heavy code
import static java.lang.Math.PI;
import static java.lang.Math.pow;
import static java.lang.Math.sqrt;

public class CircleCalcClean {
    public static void main(String[] args) {
        double radius = 5.0;
        double area = PI * pow(radius, 2);
        double circumference = 2 * PI * radius;
        double diagonal = sqrt(pow(radius, 2) + pow(radius, 2));

        System.out.println("Area: " + area);
        System.out.println("Circumference: " + circumference);
        System.out.println("Diagonal: " + diagonal);
    }
}

Static imports are also very common in unit tests with frameworks like JUnit and AssertJ:

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class CalculatorTest {

    @Test
    void testAddition() {
        Calculator calc = new Calculator();
        // Without static import: Assertions.assertEquals(5, calc.add(2, 3));
        assertEquals(5, calc.add(2, 3));
        assertTrue(calc.add(2, 3) > 0);
    }

    @Test
    void testDivisionByZero() {
        Calculator calc = new Calculator();
        assertThrows(ArithmeticException.class, () -> calc.divide(10, 0));
    }
}

Readability trade-offs: Static imports improve readability when the origin of the method is obvious (like PI, assertEquals), but they hurt readability when it is unclear where a method comes from. Use them sparingly and avoid wildcard imports (*) in production code.

Good Use Bad Use
import static java.lang.Math.PI import static com.utils.StringHelper.*
import static org.junit.Assert.* (in tests) import static java.util.stream.Collectors.*
Constants everyone recognizes Methods that could come from multiple places



7. What is the final Keyword?

The final keyword in Java means "this cannot be changed." It is a restriction that prevents modification, and it can be applied to three things: variables, methods, and classes. Each application has a different meaning, but they all share the same core idea -- once something is declared final, it is locked down.

Real-world analogy: Think of writing in permanent marker versus pencil. A pencil (non-final) lets you erase and rewrite. A permanent marker (final) means once you write something, it stays. You cannot change the text -- you can only read it.

Applied To What It Prevents Example
Variable Reassignment (value cannot change) final int MAX = 100;
Method Overriding (subclasses cannot override) final void process()
Class Inheritance (cannot be extended) final class String

The final keyword is one of the most important tools for writing robust, predictable, and thread-safe Java code. Experienced developers use it liberally because it communicates intent, catches bugs at compile time, and enables compiler optimizations.



8. Final Variables

A final variable can only be assigned once. After the assignment, any attempt to reassign it results in a compile-time error. There are several forms of final variables, each with its own rules.

8.1 Final Local Variables

A local variable declared final must be assigned exactly once before it is used. It does not need to be assigned at declaration -- it just cannot be reassigned.

public class FinalLocalVariables {
    public static void main(String[] args) {

        // Assigned at declaration -- cannot be changed
        final int maxRetries = 3;
        // maxRetries = 5;  // COMPILE ERROR: cannot assign a value to final variable

        // Blank final -- assigned later, but only once
        final String greeting;
        boolean isMorning = true;

        if (isMorning) {
            greeting = "Good morning!";
        } else {
            greeting = "Good afternoon!";
        }

        System.out.println(greeting);  // Good morning!
        // greeting = "Hello!";        // COMPILE ERROR: variable greeting might already have been assigned

        // Final in a loop -- a new variable is created each iteration
        for (int i = 0; i < 3; i++) {
            final int value = i * 10;   // This is fine -- each iteration creates a new scope
            System.out.println(value);  // 0, 10, 20
        }
    }
}

8.2 Final Instance Variables (Blank Finals)

A final instance variable must be assigned by the time the constructor finishes. It can be assigned in the declaration, in an instance initializer block, or in the constructor -- but exactly once.

public class ImmutableUser {

    // Final instance variables -- set once, never changed
    private final String username;
    private final String email;
    private final long createdAt;

    // Assigned at declaration
    private final String role = "USER";

    public ImmutableUser(String username, String email) {
        this.username = username;
        this.email = email;
        this.createdAt = System.currentTimeMillis();
        // After the constructor finishes, these can NEVER be reassigned
    }

    // Only getters -- no setters for final fields
    public String getUsername() { return username; }
    public String getEmail() { return email; }
    public long getCreatedAt() { return createdAt; }
    public String getRole() { return role; }

    public static void main(String[] args) {
        ImmutableUser user = new ImmutableUser("alice", "alice@example.com");
        System.out.println(user.getUsername());   // alice
        System.out.println(user.getEmail());      // alice@example.com
        System.out.println(user.getRole());       // USER
    }
}

8.3 Final Parameters

Method parameters can be declared final to prevent reassignment inside the method body. This is considered good practice by many developers because it avoids accidental modification of input values.

public class FinalParameters {

    // The 'final' keyword prevents reassigning the parameter
    public static double calculateDiscount(final double price, final double discountRate) {
        // price = price * 0.9;  // COMPILE ERROR: final parameter cannot be assigned

        // You must create a new variable instead
        double discountedPrice = price * (1 - discountRate);
        return discountedPrice;
    }

    // Common in constructors -- ensures you do not accidentally swap or modify parameters
    public static class Product {
        private final String name;
        private final double price;

        public Product(final String name, final double price) {
            // These assignments are fine -- we are assigning TO the fields, not reassigning params
            this.name = name;
            this.price = price;
        }
    }

    public static void main(String[] args) {
        double result = calculateDiscount(100.0, 0.20);
        System.out.println("Discounted price: $" + result);  // Discounted price: $80.0
    }
}

8.4 Constants (static final) and Naming Conventions

The combination of static and final creates a constant -- a value that belongs to the class and can never be changed. Java convention is to name constants using UPPER_SNAKE_CASE.

public class HttpStatus {

    // Constants -- public static final with UPPER_SNAKE_CASE
    public static final int OK = 200;
    public static final int CREATED = 201;
    public static final int NO_CONTENT = 204;
    public static final int BAD_REQUEST = 400;
    public static final int UNAUTHORIZED = 401;
    public static final int FORBIDDEN = 403;
    public static final int NOT_FOUND = 404;
    public static final int INTERNAL_SERVER_ERROR = 500;

    public static final String DEFAULT_CONTENT_TYPE = "application/json";
    public static final long REQUEST_TIMEOUT_MS = 30_000L;

    public static String getStatusMessage(int code) {
        switch (code) {
            case OK:                   return "OK";
            case CREATED:              return "Created";
            case BAD_REQUEST:          return "Bad Request";
            case UNAUTHORIZED:         return "Unauthorized";
            case NOT_FOUND:            return "Not Found";
            case INTERNAL_SERVER_ERROR: return "Internal Server Error";
            default:                   return "Unknown";
        }
    }

    public static void main(String[] args) {
        System.out.println(HttpStatus.OK + " " + getStatusMessage(OK));
        // 200 OK
        System.out.println(HttpStatus.NOT_FOUND + " " + getStatusMessage(NOT_FOUND));
        // 404 Not Found

        // HttpStatus.OK = 201;  // COMPILE ERROR: cannot assign a value to final variable
    }
}



9. Final Methods

A final method cannot be overridden by subclasses. When you declare a method as final, you are saying: "This is the definitive implementation. No subclass is allowed to change this behavior."

When to use final methods:

  • Template Method Pattern -- the parent class defines the algorithm structure, and subclasses override specific steps, but the overall template is final
  • Critical algorithms -- security-sensitive methods like authentication or encryption that must not be altered
  • Framework code -- methods that enforce invariants or contracts that subclasses must not break

Performance note: In older Java versions, declaring a method as final allowed the JVM to inline the method for better performance. Modern JVMs (HotSpot) are smart enough to do this optimization automatically, so performance is no longer a reason to use final methods. Use them for design correctness, not performance.

public abstract class DataProcessor {

    // FINAL method -- defines the algorithm template. Subclasses CANNOT override this.
    public final void process(String data) {
        System.out.println("=== Starting data processing ===");
        String validated = validate(data);
        String transformed = transform(validated);
        save(transformed);
        System.out.println("=== Processing complete ===\n");
    }

    // These are the "hooks" that subclasses MUST implement
    protected abstract String validate(String data);
    protected abstract String transform(String data);
    protected abstract void save(String data);
}

public class CsvProcessor extends DataProcessor {

    // Cannot override process() -- it is final
    // public void process(String data) { }  // COMPILE ERROR

    @Override
    protected String validate(String data) {
        System.out.println("Validating CSV format...");
        if (!data.contains(",")) {
            throw new IllegalArgumentException("Not valid CSV: " + data);
        }
        return data;
    }

    @Override
    protected String transform(String data) {
        System.out.println("Transforming CSV to uppercase...");
        return data.toUpperCase();
    }

    @Override
    protected void save(String data) {
        System.out.println("Saving CSV: " + data);
    }
}

public class JsonProcessor extends DataProcessor {

    @Override
    protected String validate(String data) {
        System.out.println("Validating JSON format...");
        if (!data.startsWith("{")) {
            throw new IllegalArgumentException("Not valid JSON: " + data);
        }
        return data;
    }

    @Override
    protected String transform(String data) {
        System.out.println("Transforming JSON -- adding timestamp...");
        return data.replace("}", ", \"processed\": true}");
    }

    @Override
    protected void save(String data) {
        System.out.println("Saving JSON: " + data);
    }
}

public class Main {
    public static void main(String[] args) {
        DataProcessor csv = new CsvProcessor();
        csv.process("name,age,city");

        DataProcessor json = new JsonProcessor();
        json.process("{\"name\": \"Alice\"}");
    }
}

// Output:
// === Starting data processing ===
// Validating CSV format...
// Transforming CSV to uppercase...
// Saving CSV: NAME,AGE,CITY
// === Processing complete ===
//
// === Starting data processing ===
// Validating JSON format...
// Transforming JSON -- adding timestamp...
// Saving JSON: {"name": "Alice", "processed": true}
// === Processing complete ===

In the example above, the process() method is final. This guarantees that every subclass follows the same three-step algorithm: validate, transform, save. A subclass can customize each step, but it cannot skip a step, reorder them, or bypass the logging. This is the Template Method Pattern, and the final keyword is what makes it work.



10. Final Classes

A final class cannot be extended (subclassed). When you declare a class as final, no other class can inherit from it. This is the strongest form of the final keyword -- it locks down the entire class hierarchy.

Why would you make a class final?

  • Immutability -- If a class is designed to be immutable, making it final prevents subclasses from adding mutable state. This is why String, Integer, Double, and all wrapper classes are final.
  • Security -- Prevents malicious subclasses from overriding methods to bypass security checks.
  • Correctness -- If a class relies on specific invariants, subclasses might break those invariants.
  • Design intent -- The class is not designed for extension and would not work correctly as a base class.

Many well-known Java classes are final:

Final Class Reason
java.lang.String Immutability and security (used in class loading, security managers)
java.lang.Integer, Double, Boolean, etc. Wrapper classes must be immutable
java.lang.Math Utility class with only static methods
java.lang.System Core system operations must not be overridden
// This class is final -- nobody can extend it
public final class Money {

    private final long cents;       // Store as cents to avoid floating-point issues
    private final String currency;

    public Money(long cents, String currency) {
        if (cents < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }
        if (currency == null || currency.length() != 3) {
            throw new IllegalArgumentException("Currency must be a 3-letter ISO code");
        }
        this.cents = cents;
        this.currency = currency.toUpperCase();
    }

    // Factory methods for convenience
    public static Money dollars(double amount) {
        return new Money(Math.round(amount * 100), "USD");
    }

    public static Money euros(double amount) {
        return new Money(Math.round(amount * 100), "EUR");
    }

    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Cannot add different currencies: "
                    + this.currency + " and " + other.currency);
        }
        return new Money(this.cents + other.cents, this.currency);
    }

    public Money multiply(int factor) {
        return new Money(this.cents * factor, this.currency);
    }

    public double toDouble() {
        return cents / 100.0;
    }

    @Override
    public String toString() {
        return String.format("%s %.2f", currency, toDouble());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return cents == money.cents && currency.equals(money.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(cents, currency);
    }

    public static void main(String[] args) {
        Money price = Money.dollars(29.99);
        Money tax = Money.dollars(2.40);
        Money total = price.add(tax);

        System.out.println("Price: " + price);  // Price: USD 29.99
        System.out.println("Tax:   " + tax);    // Tax:   USD 2.40
        System.out.println("Total: " + total);  // Total: USD 32.39

        Money bulk = Money.euros(9.99).multiply(5);
        System.out.println("Bulk:  " + bulk);   // Bulk:  EUR 49.95
    }
}

// This would cause a COMPILE ERROR:
// class ExtendedMoney extends Money { }  // Cannot inherit from final 'Money'



11. Effectively Final (Java 8+)

Starting in Java 8, a local variable is considered effectively final if it is never reassigned after initialization, even if it is not explicitly declared with the final keyword. This matters because lambda expressions and anonymous inner classes can only capture local variables that are final or effectively final.

Before Java 8, you had to explicitly write final on every variable you wanted to use inside an anonymous class. Java 8 relaxed this rule -- if you never reassign the variable, the compiler treats it as final automatically.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class EffectivelyFinalDemo {

    public static void main(String[] args) {

        // This variable is "effectively final" -- never reassigned
        String prefix = "Hello, ";

        List names = Arrays.asList("Alice", "Bob", "Charlie");

        // Lambda can use 'prefix' because it is effectively final
        List greetings = names.stream()
                .map(name -> prefix + name)   // 'prefix' captured here
                .collect(Collectors.toList());

        System.out.println(greetings);
        // [Hello, Alice, Hello, Bob, Hello, Charlie]

        // ------------------------------------

        // This variable is NOT effectively final -- it is reassigned
        String suffix = "!";
        suffix = "!!";  // Reassignment makes it NOT effectively final

        // This would cause a COMPILE ERROR:
        // names.stream().map(name -> name + suffix);
        // Error: local variables referenced from a lambda expression
        //        must be final or effectively final
    }
}

Why does Java enforce this rule? When a lambda captures a local variable, it creates a copy of that variable's value. If the original variable could change after the lambda captured it, the lambda would have a stale copy, leading to confusing bugs. By requiring final or effectively final, Java guarantees consistency.

Here is a more practical example showing effectively final variables in event handling and callbacks:

import java.util.*;
import java.util.concurrent.*;

public class EffectivelyFinalPractical {

    public static void main(String[] args) throws Exception {

        // All of these are effectively final (never reassigned)
        String apiUrl = "https://api.example.com/data";
        int maxRetries = 3;
        long timeoutMs = 5000L;

        // They can all be used inside the lambda
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future future = executor.submit(() -> {
            // All three variables are captured as effectively final
            System.out.println("Fetching from: " + apiUrl);
            System.out.println("Max retries: " + maxRetries);
            System.out.println("Timeout: " + timeoutMs + "ms");
            return "data from " + apiUrl;
        });

        System.out.println("Result: " + future.get());
        executor.shutdown();

        // ------------------------------------
        // Workaround when you need to modify a "captured" variable:
        // Use an array or AtomicInteger

        List names = Arrays.asList("Alice", "Bob", "Charlie", "Diana");

        // WRONG -- this would not compile:
        // int count = 0;
        // names.forEach(name -> count++);

        // RIGHT -- use an array (the array reference is effectively final,
        // even though its contents change)
        int[] count = {0};
        names.forEach(name -> {
            if (name.length() > 3) {
                count[0]++;
            }
        });
        System.out.println("Names longer than 3 chars: " + count[0]);
        // Names longer than 3 chars: 3
    }
}

The table below summarizes the differences between final, effectively final, and non-final variables:

Type Explicitly declared final? Reassigned? Usable in lambdas?
final int x = 10; Yes No (compile error) Yes
int x = 10; (never reassigned) No No Yes (effectively final)
int x = 10; x = 20; No Yes No (compile error)



12. Static Final Constants

The combination of static and final is the standard way to define constants in Java. A static final variable belongs to the class (not any instance) and cannot be reassigned. It is the Java equivalent of const in other languages.

The Java compiler performs constant folding for static final primitive values and String literals -- it replaces references to the constant with the actual value at compile time. This means changing a constant requires recompiling all classes that reference it.

static final vs. enum

Before Java 5 introduced enums, developers used static final int constants to represent fixed sets of values (like days of the week, status codes, etc.). This pattern, known as the int enum pattern, has significant drawbacks that enums solve. Here is a comparison:

// OLD WAY: static final int constants (fragile, not type-safe)
public class PaymentStatusOld {
    public static final int PENDING    = 0;
    public static final int COMPLETED  = 1;
    public static final int FAILED     = 2;
    public static final int REFUNDED   = 3;

    // Problem 1: Any int is accepted -- no type safety
    public static void processPayment(int status) {
        if (status == COMPLETED) {
            System.out.println("Payment completed");
        }
    }

    public static void main(String[] args) {
        processPayment(COMPLETED);   // OK
        processPayment(42);          // Compiles fine! No error. Bug hiding in plain sight.
        processPayment(-1);          // Also compiles fine.
    }
}

// BETTER WAY: enum (type-safe, readable, extensible)
public enum PaymentStatus {
    PENDING("Awaiting processing"),
    COMPLETED("Payment successful"),
    FAILED("Payment declined"),
    REFUNDED("Payment refunded");

    private final String description;

    PaymentStatus(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}

public class PaymentProcessor {
    // Problem solved: Only valid PaymentStatus values are accepted
    public static void processPayment(PaymentStatus status) {
        System.out.println("Status: " + status + " - " + status.getDescription());
    }

    public static void main(String[] args) {
        processPayment(PaymentStatus.COMPLETED); // OK
        // processPayment(42);                   // COMPILE ERROR -- not a PaymentStatus
    }
}

When to use static final constants vs. enums:

Use static final Use enum
Mathematical constants (PI, E) Fixed set of related values (status, type, direction)
Configuration values (timeout, max retries) Values that need methods or behavior
String constants (API keys, URLs, messages) Values used in switch statements
Numeric limits (MAX_SIZE, MIN_AGE) Values that benefit from type safety



13. Common Mistakes and Pitfalls

Even experienced developers fall into these traps. Understanding these pitfalls will save you hours of debugging.

Mistake 1: Accessing Instance Members from Static Context

This is the single most common static-related error for beginners. A static method has no this reference, so it cannot access instance variables or methods.

public class StaticMistake1 {

    private String name = "Alice";         // Instance variable
    private static String company = "TechCorp";  // Static variable

    public static void main(String[] args) {
        // COMPILE ERROR: non-static variable 'name' cannot be referenced from a static context
        // System.out.println(name);

        // This works -- 'company' is static
        System.out.println(company);

        // Fix: Create an instance first
        StaticMistake1 obj = new StaticMistake1();
        System.out.println(obj.name);  // Now we can access 'name' through the object
    }
}

Mistake 2: final Does NOT Mean Immutable

This is the most dangerous misconception about final. When you declare a reference variable as final, it means the reference cannot point to a different object. But the object itself can still be modified.

import java.util.*;

public class FinalNotImmutable {

    public static void main(String[] args) {

        // final reference -- cannot reassign the variable
        final List names = new ArrayList<>();

        // But we CAN modify the contents of the list!
        names.add("Alice");
        names.add("Bob");
        names.add("Charlie");
        System.out.println(names);  // [Alice, Bob, Charlie]

        names.remove("Bob");
        System.out.println(names);  // [Alice, Charlie]

        names.clear();
        System.out.println(names);  // []

        // This is what final prevents -- reassigning the reference
        // names = new ArrayList<>();  // COMPILE ERROR: cannot assign a value to final variable

        // To make truly immutable, use Collections.unmodifiableList or List.of()
        final List immutableNames = List.of("Alice", "Bob", "Charlie");
        // immutableNames.add("Diana");     // RUNTIME ERROR: UnsupportedOperationException
        // immutableNames.remove("Alice");  // RUNTIME ERROR: UnsupportedOperationException

        System.out.println(immutableNames);  // [Alice, Bob, Charlie]

        // Same applies to final maps, sets, arrays...
        final int[] numbers = {1, 2, 3};
        numbers[0] = 99;                    // This is allowed! Array contents can change.
        System.out.println(Arrays.toString(numbers));  // [99, 2, 3]
        // numbers = new int[5];            // COMPILE ERROR: cannot reassign final array reference
    }
}

Mistake 3: Mutable Static Variables in Multi-Threaded Environments

Mutable static variables are shared across all threads. Without proper synchronization, concurrent modifications can cause data corruption, race conditions, and bugs that are extremely hard to reproduce.

import java.util.concurrent.atomic.AtomicInteger;

public class StaticThreadSafety {

    // DANGEROUS: Mutable static variable without synchronization
    private static int unsafeCounter = 0;

    // SAFE: Using AtomicInteger for thread-safe operations
    private static final AtomicInteger safeCounter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {

        // Simulate 1000 threads incrementing the counter
        Thread[] threads = new Thread[1000];

        for (int i = 0; i < 1000; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    unsafeCounter++;                  // NOT thread-safe (race condition)
                    safeCounter.incrementAndGet();     // Thread-safe
                }
            });
            threads[i].start();
        }

        // Wait for all threads to finish
        for (Thread t : threads) {
            t.join();
        }

        System.out.println("Expected:       100000");
        System.out.println("Unsafe counter: " + unsafeCounter);     // Often LESS than 100000
        System.out.println("Safe counter:   " + safeCounter.get()); // Always exactly 100000
    }
}

Mistake 4: Static Blocks Run in Declaration Order

If your static blocks depend on each other, order matters. A static variable or block cannot reference another static member that is declared later in the file.

public class StaticOrderMistake {

    // These run in order: first staticA, then static block, then staticB

    static int staticA = 10;

    static {
        // staticA is 10 here -- already initialized
        System.out.println("Static block: staticA = " + staticA);

        // staticB is 0 here -- default value, not yet initialized to 20
        // This is a subtle bug if you expect staticB to be 20
        System.out.println("Static block: staticB = " + staticB);  // Prints 0, not 20!
    }

    static int staticB = 20;  // This assignment happens AFTER the static block

    public static void main(String[] args) {
        System.out.println("main: staticA = " + staticA);  // 10
        System.out.println("main: staticB = " + staticB);  // 20
    }
}

// Output:
// Static block: staticA = 10
// Static block: staticB = 0     <-- Surprise! Not 20!
// main: staticA = 10
// main: staticB = 20



14. Best Practices

Here are the guidelines that experienced Java developers follow when working with static and final:

Best Practices for static

Practice Reason
Use static for utility methods that do not depend on object state Clearer intent, no unnecessary object creation
Prefer static factory methods over constructors for complex creation logic Descriptive names, can return cached instances or subclasses
Minimize mutable static state Shared mutable state causes threading bugs and makes testing harder
Make utility classes final with a private constructor Prevents instantiation and subclassing
Access static members via the class name, not an object reference Math.PI instead of mathObj.PI -- clearer and less misleading
Use static inner classes instead of inner classes when the inner class does not need the outer instance Avoids hidden reference to outer class, prevents memory leaks

Best Practices for final

Practice Reason
Use final for all fields that should not change after construction Enforces immutability, enables safe sharing between threads
Use static final for constants with UPPER_SNAKE_CASE Standard Java naming convention, recognized by all developers
Prefer enum over static final int for type-safe sets of values Compile-time type safety, readable in debugger and logs
Mark classes as final if they are not designed for inheritance Prevents accidental subclassing that could break invariants
Use final on method parameters in complex methods Prevents accidental reassignment, especially in long methods
Use effectively final variables in lambdas instead of adding final everywhere Reduces noise; the compiler checks for you

Combined Best Practices

  • Immutable objects are thread-safe by definition. Using final fields and no setters means you never need synchronization for reads.
  • Static factory methods + private constructor + final class = the gold standard for value classes (like Money, Color, Point).
  • Avoid static mutable collections. If you must have a static collection, make it static final and wrap it with Collections.unmodifiableList() or use List.of().
  • Use final liberally. Many coding standards (including Google Java Style Guide) recommend making every variable final unless there is a reason not to.



15. Complete Practical Example: Application Configuration System

Let us bring everything together with a real-world example. This AppSettings class demonstrates static final constants, static utility methods, static blocks for initialization, final parameters, effectively final variables in lambdas, a static nested class, and immutable design -- all in one cohesive example.

This is the kind of code you would see in a production Spring Boot or enterprise Java application:

import java.util.*;
import java.util.stream.Collectors;

/**
 * Application settings manager demonstrating static, final, and combined patterns.
 * This class is final (cannot be subclassed) and uses static members extensively.
 */
public final class AppSettings {

    // ========== STATIC FINAL CONSTANTS ==========
    // These are true constants -- known at compile time, never change

    public static final String APP_NAME = "MyApplication";
    public static final String APP_VERSION = "2.5.1";
    public static final int DEFAULT_PORT = 8080;
    public static final int MAX_CONNECTIONS = 100;
    public static final long SESSION_TIMEOUT_MS = 30 * 60 * 1000L;  // 30 minutes
    public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

    // ========== STATIC FINAL COLLECTIONS (immutable) ==========

    public static final List SUPPORTED_LANGUAGES;
    public static final Map DEFAULT_HEADERS;

    // ========== MUTABLE STATIC STATE (minimized, thread-safe) ==========

    private static final Map settings = new HashMap<>();
    private static boolean initialized = false;

    // ========== STATIC BLOCKS ==========
    // Complex initialization that cannot be done in a single line

    static {
        // Initialize supported languages
        SUPPORTED_LANGUAGES = List.of("en", "es", "fr", "de", "ja", "pt", "zh");
    }

    static {
        // Initialize default HTTP headers
        Map headers = new LinkedHashMap<>();
        headers.put("Content-Type", "application/json");
        headers.put("Accept", "application/json");
        headers.put("X-App-Name", APP_NAME);
        headers.put("X-App-Version", APP_VERSION);
        DEFAULT_HEADERS = Collections.unmodifiableMap(headers);
    }

    static {
        // Load default settings
        settings.put("db.host", "localhost");
        settings.put("db.port", "5432");
        settings.put("db.name", "myapp");
        settings.put("log.level", "INFO");
        settings.put("cache.enabled", "true");
        settings.put("cache.ttl.seconds", "300");
        initialized = true;
        System.out.println("[AppSettings] Initialized with " + settings.size() + " default settings");
    }

    // ========== PRIVATE CONSTRUCTOR ==========
    // Prevents instantiation -- this is a utility/configuration class

    private AppSettings() {
        throw new UnsupportedOperationException("AppSettings cannot be instantiated");
    }

    // ========== STATIC METHODS ==========

    /**
     * Gets a setting value by key.
     *
     * @param key the setting key (final -- cannot be reassigned in the method)
     * @return the value, or null if not found
     */
    public static String get(final String key) {
        Objects.requireNonNull(key, "Setting key cannot be null");
        return settings.get(key);
    }

    /**
     * Gets a setting value with a default fallback.
     */
    public static String get(final String key, final String defaultValue) {
        String value = settings.get(key);
        return value != null ? value : defaultValue;
    }

    /**
     * Gets a setting as an integer.
     */
    public static int getInt(final String key, final int defaultValue) {
        String value = settings.get(key);
        if (value == null) {
            return defaultValue;
        }
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            System.err.println("[AppSettings] Warning: '" + key + "' is not a valid integer: " + value);
            return defaultValue;
        }
    }

    /**
     * Gets a setting as a boolean.
     */
    public static boolean getBoolean(final String key, final boolean defaultValue) {
        String value = settings.get(key);
        return value != null ? Boolean.parseBoolean(value) : defaultValue;
    }

    /**
     * Sets a configuration value.
     */
    public static void set(final String key, final String value) {
        Objects.requireNonNull(key, "Setting key cannot be null");
        Objects.requireNonNull(value, "Setting value cannot be null");
        settings.put(key, value);
    }

    /**
     * Returns all settings whose keys start with the given prefix.
     * Demonstrates effectively final variables in lambda expressions.
     */
    public static Map getByPrefix(final String prefix) {
        // 'prefix' is effectively final (also explicitly final here)
        // so it can be used inside the lambda
        return settings.entrySet().stream()
                .filter(entry -> entry.getKey().startsWith(prefix))
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        Map.Entry::getValue,
                        (v1, v2) -> v1,
                        LinkedHashMap::new
                ));
    }

    /**
     * Prints a formatted summary of all settings.
     */
    public static void printSummary() {
        // 'separator' is effectively final -- used in lambda below
        String separator = "=".repeat(50);

        System.out.println(separator);
        System.out.println(APP_NAME + " v" + APP_VERSION + " Configuration");
        System.out.println(separator);

        settings.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEach(entry -> System.out.printf("  %-25s = %s%n",
                        entry.getKey(), entry.getValue()));

        System.out.println(separator);
        System.out.println("Supported languages: " + SUPPORTED_LANGUAGES);
        System.out.println("Default headers: " + DEFAULT_HEADERS);
        System.out.println(separator);
    }

    /**
     * Creates a snapshot of current settings.
     * Returns a SettingsSnapshot (static nested class).
     */
    public static SettingsSnapshot snapshot() {
        return new SettingsSnapshot(new HashMap<>(settings));
    }

    // ========== STATIC NESTED CLASS ==========
    // Does not need an AppSettings instance (which cannot be created anyway)

    /**
     * An immutable snapshot of settings at a point in time.
     * This is a static nested class -- it does not hold a reference to AppSettings.
     */
    public static final class SettingsSnapshot {

        private final Map data;
        private final long timestamp;

        private SettingsSnapshot(final Map data) {
            this.data = Collections.unmodifiableMap(data);
            this.timestamp = System.currentTimeMillis();
        }

        public String get(final String key) {
            return data.get(key);
        }

        public int size() {
            return data.size();
        }

        public long getTimestamp() {
            return timestamp;
        }

        @Override
        public String toString() {
            return "SettingsSnapshot{size=" + data.size() + ", timestamp=" + timestamp + "}";
        }
    }

    // ========== MAIN METHOD -- Demonstrates everything ==========

    public static void main(String[] args) {

        // Static final constants -- accessed via class name
        System.out.println("App: " + AppSettings.APP_NAME + " v" + AppSettings.APP_VERSION);
        System.out.println("Default port: " + AppSettings.DEFAULT_PORT);
        System.out.println("Session timeout: " + AppSettings.SESSION_TIMEOUT_MS + "ms");

        System.out.println();

        // Static methods -- called without creating an object
        System.out.println("DB Host: " + AppSettings.get("db.host"));
        System.out.println("DB Port: " + AppSettings.getInt("db.port", 3306));
        System.out.println("Cache enabled: " + AppSettings.getBoolean("cache.enabled", false));

        System.out.println();

        // Modify settings at runtime
        AppSettings.set("db.host", "prod-db.company.com");
        AppSettings.set("log.level", "DEBUG");
        AppSettings.set("feature.dark-mode", "true");

        // Get settings by prefix (uses lambda with effectively final variable)
        Map dbSettings = AppSettings.getByPrefix("db.");
        System.out.println("Database settings: " + dbSettings);

        System.out.println();

        // Static nested class -- created without AppSettings instance
        AppSettings.SettingsSnapshot snap = AppSettings.snapshot();
        System.out.println("Snapshot: " + snap);
        System.out.println("Snapshot db.host: " + snap.get("db.host"));

        // The snapshot is immutable -- changes to AppSettings do not affect it
        AppSettings.set("db.host", "new-host.example.com");
        System.out.println("Live db.host:     " + AppSettings.get("db.host"));
        System.out.println("Snapshot db.host: " + snap.get("db.host"));  // Still old value

        System.out.println();

        // Print full summary
        AppSettings.printSummary();
    }
}

// Output:
// [AppSettings] Initialized with 6 default settings
// App: MyApplication v2.5.1
// Default port: 8080
// Session timeout: 1800000ms
//
// DB Host: localhost
// DB Port: 5432
// Cache enabled: true
//
// Database settings: {db.host=prod-db.company.com, db.port=5432, db.name=myapp}
//
// Snapshot: SettingsSnapshot{size=7, timestamp=1709136000000}
// Snapshot db.host: prod-db.company.com
// Live db.host:     new-host.example.com
// Snapshot db.host: prod-db.company.com
//
// ==================================================
// MyApplication v2.5.1 Configuration
// ==================================================
//   cache.enabled             = true
//   cache.ttl.seconds         = 300
//   db.host                   = new-host.example.com
//   db.name                   = myapp
//   db.port                   = 5432
//   feature.dark-mode         = true
//   log.level                 = DEBUG
// ==================================================
// Supported languages: [en, es, fr, de, ja, pt, zh]
// Default headers: {Content-Type=application/json, Accept=application/json, X-App-Name=MyApplication, X-App-Version=2.5.1}
// ==================================================

Summary

Here is a quick reference of everything covered in this tutorial:

Keyword Applied To Effect
static Variable One copy shared by all instances (class variable)
static Method Belongs to the class; callable without an object
static Block Runs once when the class is loaded (static initializer)
static Inner Class Nested class that does not require an outer instance
static import Import Allows using static members without class name qualification
final Variable Cannot be reassigned after initialization
final Method Cannot be overridden by subclasses
final Class Cannot be extended (subclassed)
static final Variable Class-level constant (convention: UPPER_SNAKE_CASE)
Effectively final Local variable Never reassigned; usable in lambdas without final keyword

Key takeaways for your career:

  • Use static for things that belong to the class, not to instances -- utility methods, constants, counters, factory methods
  • Use final to prevent change -- immutable fields, constants, template methods, sealed classes
  • Remember that final prevents reassignment, not mutation -- a final List can still have elements added to it
  • Prefer enum over static final int for type-safe sets of values
  • Minimize mutable static state -- it causes concurrency bugs and makes testing difficult
  • Use static final with UPPER_SNAKE_CASE for constants -- this is the universal Java convention
  • Embrace effectively final variables in lambdas -- they keep your code clean without verbose final declarations



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *