16 Lazy Constants

A lazy constant is an object of type LazyConstant that holds a single data value, which is called its content. A lazy constant's content is immutable. The JVM treats lazy constants as constants, which enables the same performance optimizations as final fields. However, you have greater flexibility with regards to when you initialize a lazy constant's content compared to final fields.

Note:

This is a preview feature. A preview feature is a feature whose design, specification, and implementation are complete, but is not permanent. A preview feature may exist in a different form or not at all in future Java SE releases. To compile and run code that contains preview features, you must specify additional command-line options. See Preview Language and VM Features.

For background information about lazy constants, see ...

Consider the following example that declares and initializes a Logger as a final field:

public class Locations {
    private final Logger logger = 
        Logger.getLogger(Locations.class.getName());;
    
    Logger getLogger() {
        return logger;
    }
    
    public void printLocations() {
        getLogger().info("Printing locations..."); 
    }
}

Because logger is a final field, it must be initialized when an instance of Locations is created. This is an example of eager initialization. However, the example doesn't use logger until printLocations() is called. To initialize logger when this method is called, you can use lazy initialization, which means that a result is produced (in this example, the logger field is initialized) only when it's needed:

public class Customers {
    private Logger logger = null;
    
    synchronized Logger getLogger() {
        if (logger == null) {
            logger = Logger.getLogger(Customers.class.getName());
        }
        return logger;
    }
    
    public void printCustomers() {
        getLogger().info("Printing customers..."); 
    }
}

In this example, logger is initialized only when getLogger() is called and only if logger hasn't been initialized previously. However, there are several drawbacks with this approach:

  • Code must access the logger field through the getLogger() method. If not, a NullPointerException will be thrown if logger hasn't been initialized.
  • Thread contention can occur when multiple threads try to simultaneously initialize logger. This example declares getLogger() as synchronized. As a result, when a thread invokes this method, all other threads are blocked from invoking it. This can create bottlenecks and slow down your application. However, not declaring getLogger() as synchronized can result in multiple logger objects being created.
  • Because logger isn't declared as final, the compiler can't apply performance optimizations related to constants to it.

It would be ideal if logger were both lazily initialized and immutable once it has been initialized. In other words, it would be ideal to defer immutability. You can do this by using a lazy constant:

public class Orders {
    private final LazyConstant<Logger> logger =
        LazyConstant.of(
            () -> Logger.getLogger(Orders.class.getName())
        );

    public void printOrders() {
        logger.get().info("Printing orders..."); 
    }
}

The static factory method LazyConstant.of(Supplier) creates a lazy constant that initially holds no content:

private final LazyConstant<Logger> logger =
    LazyConstant.of(
        () -> Logger.getLogger(Orders.class.getName())
    );
The argument of LazyConstant.of(Supplier) specifies how the lazy constant's value is computed. This argument is the lazy constant's computing function. However, the lazy constant's content isn't initialized when LazyConstant.of(Supplier). It's initialized when LazyConstant.get() is called:
logger.get().info("Printing orders...");

If the lazy value's content has already been initialized, then LazyConstant.get() retrieves it instead.

Aggregating and Composing Lazy Constants

You can aggregate multiple lazy constants in an application, which can improve its startup time. In addition, you can compose lazy constants from other lazy constants. The previous example Orders shows you how to store a logger component in a lazy constant. The following example stores an Locations, Customers, and Orders component in their own lazy constant.

public class LoggingExample {
    
    static final LazyConstant<Locations> LOCATIONS = LazyConstant.of(Locations::new);
    static final LazyConstant<Customers> CUSTOMERS = LazyConstant.of(Customers::new);
    static final LazyConstant<Orders>       ORDERS = LazyConstant.of(Orders::new);
    
    public static Locations locations() {
        return LOCATIONS.get();
    }

    public static Customers customers() {
        return CUSTOMERS.get();
    }
    
    public static Orders orders() {
        return ORDERS.get();
    }
   
    public void main(String[] args) {
        locations().printLocations();
        customers().printCustomers();        
        orders().printOrders();
    }
}

By storing an application's components in lazy constants, you can significantly improve its startup time. The application no longer initializes all of its components up front. This example initializes its components on demand.

Note that the Orders component is using its own internal lazy constant for the logger. In particular, the lazy constant ORDERS is dependent on the lazy constant Orders.logger. The dependent Orders.logger will first be created if ORDERS does not already exist.

If there's a circular dependency, then an IllegalStateException is thrown, which the following example demonstrates:

public class LazyConstantCircularDependency {
    
    public static class A {
        static final LazyConstant<B> b = LazyConstant.of(B::new);
        A() {
            // java.lang.IllegalStateException: Recursive
            // invocation of a LazyConstant's computing function
            b.get();
        }
    }
    

    public static class B {
        static final LazyConstant<A> a = LazyConstant.of(A::new);
        B() {
            a.get();
        }
    }
        
    public void main(String[] args) {
        A myA = new A();
    }
}

An IllegalStateException is thrown when the constructor of class A tries to initialize the lazy constant A.b. This lazy constant depends on B.a, which is dependent on A.b.

Lazy Lists

A lazy list is an unmodifiable list, backed by an array of lazy constants and associated with an IntFunction. When you first access an element in a list, it's initialized with a value computed with the lazy list's IntFunction, which uses the element's index as its parameter. When you access the element with the same index an additional time, its value is retrieved instead of being computed again.

When you create a lazy list with the List.ofLazy(int, IntFunction) factory method, you specify its size in the first argument, You specify how its elements are computed in the second argument.

The following example creates a pool of Orders objects. This enables different Orders to serve different application requests, distributing the load across the pool. Instead of creating a lazy constant for each Orders object in the pool, the example creates a lazy list. To simulate different application requests, the example creates and starts two virtual threads. In each of these threads, a Orders object is initialized in the lazy list.

public class LazyListExample {
    
    private final static int POOL_SIZE = 20;    
    
    static final List<Orders> ORDERS
        = List.ofLazy(POOL_SIZE, _ -> new Orders());
     
    public static Orders orders() {
        long index = Thread.currentThread().threadId() % POOL_SIZE;
        return ORDERS.get((int)index);
    }        
    
    public static void main(String[] args) {
        Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
        Runnable task = () -> {
            System.out.println("Thread ID: " + Thread.currentThread().threadId());
            orders().printOrders();
        };
        
        try {
            // name "worker-0"
            Thread t1 = builder.start(task);   
            t1.join();
            System.out.println(t1.getName() + " terminated");

            // name "worker-1"
            Thread t2 = builder.start(task);   
            t2.join();  
            System.out.println(t2.getName() + " terminated");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

The example prints output similar to the following:

Thread ID: 23
Oct 01, 2025 6:39:15 PM Orders printOrders
INFO: Printing orders...
worker-0 terminated
Thread ID: 27
Oct 01, 2025 6:39:15 PM Orders printOrders
INFO: Printing orders...
worker-1 terminated

The following statement creates a lazy list that holds 20 Orders objects. Note that none of the lazy list's elements are initialized yet.

static final List<Orders> ORDERS
        = List.ofLazy(POOL_SIZE, _ -> new Orders());

The first invocation of ORDERS.get((int)index) initializes the lazy constant's contents located at index with the following lambda expression:

_ -> new Orders()

Tip:

The underscore character (_) is an unnamed variable, which represents a variable that's being declared but it has no usable name. It helps to indicate that a variable is not used after its declaration. See Unnamed Variables and Patterns in Java Platform, Standard Edition Java Language Updates.

Subsequent invocations of ORDERS.get((int)index) with the same index retrieve the element's contents immediately.

Lazy Maps

A lazy map is an unmodifiable map whose keys you specify when you create it. It's also associated with a Function. When you first access a key's value, it's initialized with a value computed with the lazy map's Function, which uses the key as its parameter. When you access the key's value an additional time, its value is retrieved instead of being computed again.

The following example calculates the log base 2 of an integer by counting the number of leading zeroes in its binary representation. Note that the example can only calculate the log base 2 of the first six powers of 2. When you create a lazy map with the Map.ofLazy(Set, Function) factory method, you must specify all of the possible parameters that the lazy map's Function can accept in the first argument. You specify how a key's value is computed in the second argument.

public class LazyMapExample {
    
    private static final Set<Integer> KEYS =
         Set.of(1, 2, 4, 8, 16, 32);
         
    private static final Function<Integer, Integer> LOG2_FUNCTION =
         i -> 31 - Integer.numberOfLeadingZeros(i);         

    private static final Map<Integer, Integer> LOG2_LM =
        Map.ofLazy(KEYS, LOG2_FUNCTION);   
        
    public static void main(String[] args) {
        // This is eligible for constant folding; log base 2 of 16 is 4
        System.out.println("Log base 2 of 16 is " + LOG2_LM.get(16));
        System.out.println();
        
        LOG2_LM.entrySet()
            .stream()
            .forEach(e -> System.out.println(
                "Log base 2 of " + e.getKey() + " is " + e.getValue()));
    }
}

The example prints output similar to the following:

Log base 2 of 16 is 4

Log base 2 of 32 is 5
Log base 2 of 8 is 3
Log base 2 of 16 is 4
Log base 2 of 4 is 2
Log base 2 of 2 is 1
Log base 2 of 1 is 0

Constant Folding

Constant folding is a compiler optimization in which constant expressions are evaluated at compile time instead of run time. The JIT compiler sometimes performs constant folding for fields declared final, lazy constants that reside in static fields, records, and hidden classes. Constant folding elides the need to load a value from memory because the value can instead be embedded in the machine code emitted by the JIT compiler. Constant folding is often the first step in a chain of optimizations that together can provide significant performance improvements. For example, the expression LOG2_LM.get(16) in LazyMapExample is eligible for constant folding.

Thread Safety

As demonstrated in the example LazyListExample, lazy constants are thread-safe. The contents of a lazy constant is guaranteed to be set at most once. If competing threads are racing to set a lazy constant, only one update succeeds, while other updates are blocked until the lazy constant becomes set.