Lazy Initialization and Lazy Loading in Java: Using Supplier, Streams, and Virtual Proxy Patterns
This article explains the concepts of eager versus lazy evaluation in Java, presents common scenarios for lazy initialization, demonstrates how to implement lazy loading with Supplier, virtual proxy, and delegate patterns, and shows practical Stream examples including infinite prime number generation.
Many operations in code are eager, evaluating parameters immediately during method calls; while eager evaluation simplifies coding, lazy evaluation often yields better performance.
Lazy Initialization is useful in scenarios such as resource‑heavy objects to save resources and speed up creation, and when certain data cannot be obtained at startup and must be fetched later.
Java 8 introduced lambda expressions, making lazy operations convenient through Supplier and Stream. The get() method of a Supplier defers object creation until it is actually needed, similar to Spring's @Lazy annotation.
public class Holder {
// Default first call to heavy.get() triggers synchronized method
private Supplier<Heavy> heavy = () -> createAndCacheHeavy();
public Holder() {
System.out.println("Holder created");
}
public Heavy getHeavy() {
// After first call, heavy points to a new instance, so no more synchronization
return heavy.get();
}
private synchronized Heavy createAndCacheHeavy() {
// Define class inside method, different from nested class loading
class HeavyFactory implements Supplier<Heavy> {
private final Heavy heavyInstance = new Heavy();
public Heavy get() {
// Always return the same instance
return heavyInstance;
}
}
// First call redirects heavy to a new Supplier instance
if (!HeavyFactory.class.isInstance(heavy)) {
heavy = new HeavyFactory();
}
return heavy.get();
}
}The above code implements a lightweight virtual proxy pattern, ensuring correct lazy loading across different environments.
An alternative delegate‑based implementation is shown below:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;
public class MemoizeSupplier<T> implements Supplier<T> {
final Supplier<T> delegate;
ConcurrentMap<Class<?>, T> map = new ConcurrentHashMap<>(1);
public MemoizeSupplier(Supplier<T> delegate) {
this.delegate = delegate;
}
@Override
public T get() {
// computeIfAbsent guarantees the supplier is called only once per key
return this.map.computeIfAbsent(MemoizeSupplier.class,
k -> this.delegate.get());
}
public static <T> Supplier<T> of(Supplier<T> provider) {
return new MemoizeSupplier<>(provider);
}
}Another more complex CloseableSupplier example demonstrates thread‑safe lazy initialization with double‑checked locking, reset after close, and additional utility methods.
public static class CloseableSupplier<T> implements Supplier<T>, Serializable {
private static final long serialVersionUID = 0L;
private final Supplier<T> delegate;
private final boolean resetAfterClose;
private volatile transient boolean initialized;
private transient T value;
private CloseableSupplier(Supplier<T> delegate, boolean resetAfterClose) {
this.delegate = delegate;
this.resetAfterClose = resetAfterClose;
}
public T get() {
if (!this.initialized) {
synchronized (this) {
if (!this.initialized) {
T t = this.delegate.get();
this.value = t;
this.initialized = true;
return t;
}
}
}
return this.value;
}
// Additional methods omitted for brevity
}Stream operations are lazy: intermediate methods like limit(), filter(), map() build a pipeline, and only a terminal operation such as collect() or findFirst() triggers execution. Elements are processed one‑by‑one through the pipeline.
For infinite collections, Stream.iterate() can generate an unbounded stream. Example: generating a list of prime numbers using an infinite stream and a terminating limit() operation.
public class Primes {
public static boolean isPrime(final int number) {
return number > 1 &&
IntStream.rangeClosed(2, (int) Math.sqrt(number))
.noneMatch(divisor -> number % divisor == 0);
}
private static int primeAfter(final int number) {
if (isPrime(number + 1))
return number + 1;
else
return primeAfter(number + 1);
}
public static List<Integer> primes(final int fromNumber, final int count) {
return Stream.iterate(primeAfter(fromNumber - 1), Primes::primeAfter)
.limit(count)
.collect(Collectors.<Integer>toList());
}
}This approach avoids the need to know the numeric bounds in advance and handles large ranges elegantly, showcasing the power of lazy evaluation in Java streams.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
