What is lazy initialization in Java, and when would you use it?
answer
- Defer expensive work until first access
- null field + build-in-getter shape
- Moves cost from startup to first-access latency
- Skip it if cheap or always-needed
- Naive version is not thread-safe
basics
~20 sLazy initialization means you delay creating an object until the first time it is actually needed, instead of creating it up front. You use it when the object is expensive to build and might never be used.
solid answer
~40 sLazy initialization is the pattern of deferring the creation or computation of a value until the first time it is actually accessed, rather than doing the work eagerly at construction or class-load time. The classic shape is a field that starts null and is populated inside its getter on first call. You reach for it when the value is expensive to build (large object, file/network/DB access, heavy computation) and may never be needed in a given run, so eager creation would waste time and memory. The main trade-off is that you move cost from startup to first access, adding a small latency spike on that first call and some bookkeeping. In multi-threaded code naive lazy init is unsafe, so you need a thread-safe idiom such as the initialization-on-demand holder or double-checked locking with volatile.
go deeper
Can define lazy init as 'build it on first use, not up front', show the null-check getter, and name expensive/rarely-used objects as the use case.
Articulates the startup-vs-first-access trade-off, knows when NOT to use it, and is aware the naive getter is not thread-safe.
Frames it as moving cost in time, weighs first-access latency vs startup, and can immediately reach for a thread-safe idiom in concurrent contexts.
Reasons about it as a system-wide policy (warm-up, predictable tail latency, capacity), and decides where lazy vs eager belongs across a service's lifecycle and SLAs.
## What "initialization" means In Java, **initialization** is the work of giving a variable or field its first real value — for an object field that usually means calling `new SomeType(...)`, which allocates memory and runs the constructor. **Eager** initialization does this as early as possible: in a field initializer or constructor, so the value exists before anyone asks for it. **Lazy** initialization does the opposite: it leaves the field empty (typically `null`) and only does the work the **first time the value is actually requested**. ## The basic shape ```java private Config config; // starts as null public Config getConfig() { if (config == null) { // not built yet? config = loadConfig(); // expensive work, done once } return config; } ``` The first call sees `null`, does the expensive `loadConfig()`, stores the result, and returns it. Every later call sees a non-null field and skips the work. The stored value is called a **cache** or **memoized** result (memoization = remembering the result of an expensive call so you don't repeat it). ## Why do it Three motivations: 1. **Startup time.** If an app eagerly builds everything at boot, startup is slow. Deferring rarely-used objects makes the program start faster. 2. **Memory.** An object you never touch costs nothing if you never build it. 3. **Avoiding work that may never happen.** If a feature is used in only 5% of runs, building its support objects eagerly wastes effort 95% of the time. ## The cost / trade-off Lazy init doesn't make work disappear — it **moves it**. The expensive cost now lands on whoever makes the **first access** ("first-access latency"), not at startup. So you trade *predictable startup cost* for an *unpredictable latency spike* on first use. If the first user is latency-sensitive (e.g. the first web request), that can be worse than eager init. You also add a tiny per-access check (`if (field == null)`). ## When NOT to use it - If the object is cheap to build, lazy init just adds complexity and a branch for no benefit. - If the object is **always** needed, you gain nothing — eager is simpler. - In concurrent code, the naive single-threaded version above is **broken** (two threads can both see `null` and build it twice, or see a half-constructed object). You then need a thread-safe idiom. ## Thread-safety preview The simple `if (field == null)` getter is only safe when called from a single thread. With multiple threads you need one of: the **initialization-on-demand holder idiom** (let the classloader guarantee safety), **double-checked locking** with a `volatile` field, or helpers like `Supplier`-based memoization. Those are deeper topics, but the key takeaway at this level is: *lazy init defers expensive work to first use, and concurrency makes it tricky.*
- What downside does lazy initialization introduce compared to eager initialization?It shifts the expensive work to the first access, causing a latency spike for the first caller and an unpredictable first-use cost, instead of paying a known cost at startup. It also adds a per-access null check and, in concurrent code, the need for a thread-safe idiom.
- Give a concrete example where lazy init is a bad idea.A field that is cheap to construct and used on essentially every request — e.g. a small immutable config record. Lazy init buys nothing and adds a branch and concurrency risk; eager initialization is simpler and clearer.
saying these in an interview costs you the question
- Claiming lazy init makes the total work smaller (it only moves WHEN the work happens)
- Using lazy init for cheap or always-used objects, adding complexity for no gain
- Forgetting that the naive null-check getter breaks under concurrency
- Confusing lazy init with caching results forever even when inputs change