Given java.time creates a new object on every plus/minus/with call, when (if ever) is this allocation a real concern, and how would you reason about it?
answer
- Each plus/minus/with allocates a tiny new object
- Young-gen GC reclaims short-lived garbage almost free
- Escape analysis can elide intermediates
- Optimize only on measured GC/allocation hotspots
- Lever = primitive epoch math + hoist invariants, never mutability
basics
~20 sEach call makes a small new object, but these are tiny and short-lived, so the JVM handles them cheaply. It only matters in very hot, high-volume loops; usually you measure first and prefer correctness and clarity over avoiding allocation.
solid answer
~50 sImmutability means each plus/minus/with returns a freshly allocated instance, so a tight loop can create many short-lived objects. In practice this is rarely a problem: java.time values are small, the allocations are in young-generation space where the JVM collects them extremely cheaply, and escape analysis can sometimes elide them entirely. The right reasoning is: default to the clear immutable code, and only optimize if profiling shows GC pressure or allocation as a real hotspot — premature micro-optimization here usually buys nothing. If a genuine hotspot exists (e.g. millions of date computations per second), options include computing with primitive longs/epoch values and converting once, reusing a precomputed Duration/Period, hoisting invariant sub-results out of the loop, or restructuring to do bulk arithmetic. You never restore mutability; you reduce the number of conversions. Correctness, thread-safety, and readability from immutability almost always outweigh the allocation cost.
go deeper
Understands that each call makes a new object but trusts that it's normally fine and not something to worry about.
Knows allocations are small and young-gen GC handles them; would profile before optimizing rather than abandoning immutability.
Reasons about generational GC, escape analysis, and concrete levers (primitive epoch math, hoisting, fewer conversions) for measured hotspots.
Frames a measure-first decision policy, weighs immutability's correctness/thread-safety wins against allocation, distinguishes java.time from structural-sharing collections, and sets team guidance against premature mutability micro-optimizations.
## The mechanic that raises the question Because `java.time` types are immutable, **every** transformation allocates: `d.plusDays(1)` constructs a new `LocalDate`; a loop adding a day 10,000 times constructs ~10,000 objects. A naive reaction is 'isn't that wasteful versus mutating in place?' The principal-level answer is to reason quantitatively rather than reflexively avoid allocation. ## Why it's usually cheap 1. **The objects are tiny.** A `LocalDate` is essentially a few ints (year, month, day). An `Instant` is a long + int. Allocation cost scales with size and these are small. 2. **Generational GC favors short-lived garbage.** The JVM's young generation (eden) uses bump-pointer allocation (incrementing a pointer) and a copying collector that only walks *live* objects. Objects that die young — exactly these throwaway intermediates — are reclaimed almost for free; dead objects are never even visited. So 'lots of short-lived small objects' is the GC's best case, not its worst. 3. **Escape analysis / scalar replacement.** The JIT (C2/Graal) can prove that an intermediate never escapes a method and replace the object with its scalar fields on the stack or in registers, sometimes eliminating the allocation entirely. 4. **No locking, no copying elsewhere.** Immutability removes defensive copies and synchronization you'd otherwise pay for, often a net win. ## When it can actually matter - **Very hot, high-frequency arithmetic**: e.g. a market-data or simulation loop doing millions of date/time computations per second, where allocation rate drives GC frequency and pause time. - **Allocation in the steady-state path** of a latency-sensitive service where even minor GC adds tail latency. - **Object-heavy intermediate types** (e.g. building many `ZonedDateTime`/`DateTimeFormatter`-derived objects) rather than the cheapest primitives. Even then, the discipline is **measure first**: profile allocation (async-profiler, JFR allocation events) and GC logs to confirm `java.time` is the actual hotspot before changing anything. ## How to reduce cost *without* abandoning immutability 1. **Work in primitives, convert once.** Do arithmetic on `long` epoch values (epoch-day, epoch-milli/nano) inside the loop and build a single `java.time` object at the boundary. `LocalDate.toEpochDay()`/`ofEpochDay(long)` and `Instant.toEpochMilli()`/`ofEpochMilli` are designed for this. 2. **Hoist invariants.** Precompute a `Duration`/`Period`/formatter once outside the loop and reuse the immutable instance (sharing is safe). 3. **Avoid redundant conversions.** Don't repeatedly convert between `Instant`, `ZonedDateTime`, and `LocalDateTime`; pick one representation for the hot path. 4. **Batch.** Compute a range with arithmetic (`LongStream` over epoch days) and materialize objects lazily/only when needed. Note what you do **not** do: you never make the types mutable or share a 'reset and reuse' buffer — that would forfeit thread-safety and the discarded-result clarity for a usually-illusory gain. ## The structural-sharing nuance Unlike persistent collections (which share structure between versions), `java.time` values do **not** structurally share — each is an independent small struct. That is fine precisely because they are tiny; structural sharing is a technique for large structures (trees, lists) where copying is expensive, which doesn't apply to a 3-int date. ## The decision framework (what 'principal' looks like here) - Default: immutable, fluent, clear code. - Trigger to optimize: a *measured* allocation/GC hotspot, not a hunch. - Lever: fewer conversions / primitive epoch math / hoisting — never mutability. - Re-measure to confirm the change actually helped and didn't regress correctness (DST, clamping, leap handling).
- Why are millions of short-lived LocalDate objects not a typical GC disaster?They die in the young generation, where allocation is a pointer bump and the copying collector only processes live objects; dead short-lived ones are reclaimed essentially for free.
- If profiling DOES show a hotspot, what's the first lever?Do the arithmetic on primitive epoch values (epoch-day/epoch-milli) in the loop and build a single java.time object at the boundary — reduce conversions, don't add mutability.
- Why doesn't java.time use structural sharing like persistent collections?Its values are tiny fixed-size structs (a few ints/longs), so full copies are cheap; structural sharing only pays off for large structures where copying is expensive.
saying these in an interview costs you the question
- Avoiding java.time fluency 'because allocation is slow' without measuring
- Reintroducing mutable reusable buffers for speed
- Assuming many small short-lived objects are a GC worst case
- Confusing java.time with structural-sharing persistent collections