skip to content

Optimization Techniques

Code-level techniques that actually pay off — avoiding quadratic string building, pre-sizing collections, dodging autoboxing — alongside the JIT behaviors that make other optimizations unnecessary. Interviewers want to see you distinguish the two.

part ofJavaoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

What are autoboxing and unboxing in Java, and why can they hurt performance in hot code?

level: juniorimportance: must knowfreq 72%

answer

  1. Primitive = raw value; wrapper = heap object with header
  2. valueOf on box, intValue on unbox
  3. Generics/streams can't hold primitives → silent boxing
  4. Cost = allocation + GC pressure + pointer indirection
  5. Hot path only: optimize inner loops, not config maps

basics

~20 s

Autoboxing converts a primitive (like int) into its wrapper object (Integer) automatically; unboxing does the reverse. Each box can allocate a new object, which costs memory and creates garbage. In tight loops that adds up and slows things down.

solid answer

~40 s

Java primitives (int, long, double, etc.) hold raw values with no object header, while wrapper types (Integer, Long, Double) are full objects on the heap. Autoboxing is the compiler automatically calling Integer.valueOf(x) when a primitive is used where an object is expected; unboxing calls intValue() for the reverse. The cost is twofold: every box may allocate a heap object (memory + initialization), and those short-lived objects raise garbage-collection pressure. In a hot loop doing millions of operations, that allocation and GC churn dominates. It also adds pointer indirection on reads. The fix is to keep primitives primitive on hot paths: use int instead of Integer, primitive arrays instead of List<Integer>, and primitive streams (IntStream) instead of Stream<Integer>.

go deeper

for a junior

Define autoboxing/unboxing and name int vs Integer as primitive vs object. Knows boxing can create garbage.

for a middle

Explains the allocation + GC-pressure cost, where boxing sneaks in (generics, streams, accumulators), and the primitive-stream / primitive-array fixes.

for a senior

Reasons about cache locality and pointer indirection, restricts the optimization to genuine hot paths, and reaches for primitive-specialized collections when needed.

for a principal

Frames it as a data-layout and allocation-rate concern across a system, weighs Valhalla's trajectory, and sets team conventions/profiling gates rather than micro-optimizing blindly.

## The two worlds: primitives vs wrappers Java has two kinds of integer-ish values: - **Primitives** — `int`, `long`, `short`, `byte`, `char`, `boolean`, `float`, `double`. These are *raw values*. An `int` is just 32 bits sitting in a register or stack slot. No object header, no heap allocation, no pointer. - **Wrapper objects** — `Integer`, `Long`, `Short`, `Byte`, `Character`, `Boolean`, `Float`, `Double`. Each is a real Java *object* living on the heap. On the HotSpot JVM an object has a header (mark word + class pointer, typically 12–16 bytes) plus the value field, so an `Integer` wrapping a 4-byte int can occupy ~16 bytes and is reached through a pointer (a *reference*). ## What is autoboxing / unboxing? **Autoboxing** is the compiler *automatically* converting a primitive to its wrapper when the context wants an object. Writing `Integer i = 5;` compiles to `Integer i = Integer.valueOf(5);`. **Unboxing** is the reverse: `int x = i;` compiles to `int x = i.intValue();`. These conversions are invisible in source code, which is exactly why they sneak in. ## Why it can be slow 1. **Allocation.** Boxing a value the cache doesn't cover allocates a fresh heap object. Allocation itself is cheap on modern JVMs (a pointer bump), but it isn't free. 2. **GC pressure.** Boxed objects are usually short-lived garbage. Create millions of them in a loop and the young-generation garbage collector runs more often, costing CPU and pauses. 3. **Indirection / cache misses.** Reading a boxed value means following a pointer to the heap, which can miss the CPU cache. A primitive array `int[]` is contiguous and cache-friendly; an `Integer[]` (or `List<Integer>`) is an array of pointers to scattered objects. 4. **Extra method calls.** Each box/unbox is a `valueOf`/`intValue` call (usually inlined by the JIT, but still conceptual overhead). ## Where it sneaks in - **Generics & collections.** Generics can't hold primitives, so `List<Integer>`, `Map<Integer, ...>`, `HashSet<Long>` box every element. `map.get(5)` boxes the `5`. - **Streams.** `Stream<Integer>` boxes; `IntStream` does not. `list.stream().mapToInt(...)` unboxes once then stays primitive. - **Ternaries and mixed expressions.** `flag ? 1 : someInteger` can box the `1`. - **Varargs of Object**, `Object`-typed fields, reflection, and logging like `log.info("{}", count)`. ## Hot path vs cold path The rule is about *hot paths* — code that runs millions of times (inner loops, per-request handlers, per-element transforms). Boxing one `Integer` to put in a config map is irrelevant. Boxing inside a loop summing a million values is not. Optimize where it matters; readable boxed code elsewhere is fine. ## The fixes - Use primitives directly (`int sum`, not `Integer sum`). - Use **primitive arrays** (`int[]`, `long[]`) instead of `List<Integer>`. - Use **primitive streams** (`IntStream`, `LongStream`, `DoubleStream`). - Use **primitive-specialized collections** (Eclipse Collections `IntList`, fastutil `IntArrayList`, Trove) when you need list/map/set semantics over primitives. - Beware accumulators: `Integer total = 0; for (...) total += x;` boxes on every iteration — make it `int total`.

  • Why can't a Java generic type parameter be a primitive like int?
    Generics are implemented with type erasure over Object references, and primitives are not subtypes of Object. So the type argument must be a reference type — hence List<Integer>, not List<int>. (Project Valhalla aims to relax this with value/primitive classes, but it isn't standard yet.)
  • Does autoboxing allocate every single time?
    No. Integer.valueOf caches small values (-128..127 by default), so boxing those returns a shared cached instance with no allocation. Values outside the cache allocate a new object each time.

saying these in an interview costs you the question

  • Claiming autoboxing is always free because the JIT optimizes it away — allocation and GC pressure are real in hot loops
  • Thinking int and Integer are interchangeable with no cost difference
  • Saying List<int> is legal Java (generics require reference types)
  • Optimizing boxing everywhere including cold paths, hurting readability for no gain

context

open as a page

What is lazy initialization in Java, and when would you use it?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Lazy 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.

open as a page

What does "premature optimization is the root of all evil" mean, and what is the correct discipline it prescribes?

level: juniorimportance: must knowfreq 70%

basics

~20 s

It means don't make code faster before you know it's slow. First write clear, correct code, then measure where the real slowness is, and only optimize that part. Guessing usually wastes effort and hurts readability.

open as a page

Why is building a String by repeatedly using += inside a loop slow, and how slow does it get as the number of iterations grows?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Strings in Java can't be changed, so each += makes a brand-new String by copying all the characters so far plus the new ones. Doing that every loop turn means you copy more and more text, so the total work grows much faster than the number of loops.

open as a page

Explain the Integer cache (-128..127) and why `==` on boxed Integers is dangerous.

level: middleimportance: must knowfreq 80%

basics

~20 s

Integer.valueOf caches small Integer objects from -128 to 127 and reuses them. So two boxed 100s are the same object (== is true), but two boxed 1000s are different objects (== is false). Use .equals() to compare values, not ==, which compares object identity.

open as a page

How do you correctly size a HashMap to hold a known number of entries without triggering a resize, and why isn't passing the entry count directly enough?

level: middleimportance: must knowfreq 60%

basics

~20 s

Pass capacity = expectedSize / 0.75 + 1, not the raw count. A HashMap resizes when it's about 75% full (the load factor), so if you pass the exact count it will still grow once you near it. Java 19+ has HashMap.newHashMap(n) that does this for you.

open as a page

Explain the initialization-on-demand holder idiom and why it is thread-safe without explicit synchronization.

level: middleimportance: must knowfreq 68%

basics

~20 s

You put the value in a private static nested class. The JVM only loads that class the first time it is used, and class loading is automatically thread-safe, so the value is created lazily and safely without you writing any locks.

open as a page

Why should you profile before optimizing Java code instead of reasoning about which code is slow?

level: middleimportance: must knowfreq 68%

basics

~20 s

Because guesses about what's slow are usually wrong. A profiler runs your program and shows where time and memory actually go, so you fix the real hotspot instead of wasting effort on code that doesn't matter.

open as a page

How do you efficiently build a large String inside a loop, and what role does pre-sizing the StringBuilder's capacity play?

level: middleimportance: must knowfreq 60%

basics

~20 s

Use a StringBuilder: create one, call append in the loop, then toString at the end. If you know roughly how big the result will be, give the StringBuilder that size up front so it doesn't have to keep growing and copying its internal buffer.

open as a page

What does it mean to 'pre-size' a Java collection like ArrayList or HashMap, and why might you do it when you already know roughly how many elements you'll add?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Pre-sizing means telling a collection up front how many items you expect, via a constructor argument (e.g. new ArrayList<>(1000)). It avoids the collection having to grow and copy its internal array repeatedly as you add items, which saves time and memory churn.

open as a page

What is object pooling, and when does it make sense to use it in Java versus just letting the garbage collector handle short-lived objects?

level: juniorimportance: should knowfreq 55%

basics

~20 s

Object pooling means keeping a set of reused objects instead of creating new ones each time. It is worth it only for expensive or scarce things like threads, database connections, or big buffers. For ordinary small objects, just let the garbage collector handle them.

open as a page

When is using the plain + operator for String concatenation perfectly acceptable, and how do you decide between +, StringBuilder, and String.format on a given line of code?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Plain + is fine whenever you join a fixed, small number of strings in one expression and not inside a loop — like building a log message or a greeting. Use StringBuilder when you build up a string across loop iterations. String.format is for readable templated text when speed isn't critical.

open as a page

How can autoboxing introduce a NullPointerException, and where does this commonly bite?

level: middleimportance: should knowfreq 63%

basics

~20 s

Unboxing a null wrapper throws a NullPointerException. If an Integer is null and you assign it to an int, compare with ==, or do arithmetic on it, Java calls null.intValue() and crashes. Common with map lookups, nullable DB columns, and ternary expressions.

open as a page

How do primitive streams (IntStream/LongStream) and primitive collections avoid boxing compared to Stream<Integer> and List<Integer>?

level: middleimportance: should knowfreq 58%

basics

~20 s

Stream<Integer> and List<Integer> store and process boxed Integer objects, allocating one per element. IntStream and primitive collections (like fastutil's IntArrayList) hold raw ints, so no wrapper objects are created and there's no per-element GC pressure.

open as a page

What is cache locality, and why does iterating a Java array sequentially tend to be much faster than jumping around it randomly or chasing pointers through linked structures?

level: middleimportance: should knowfreq 42%

basics

~20 s

Memory is slow, so the CPU keeps recently-used data in small fast caches, and it loads memory in chunks (cache lines). Reading an array in order uses each loaded chunk fully and lets the CPU prefetch the next, so it's fast. Random jumps or pointer-chasing keep missing the cache and waiting on slow memory.

open as a page

What is CPU branch prediction, and why can a Java loop over sorted data run faster than the same loop over unsorted data?

level: middleimportance: should knowfreq 45%

basics

~20 s

Modern CPUs guess which way an if/branch will go before they know, to keep working ahead. With sorted data the pattern is regular so guesses are right; with random data guesses miss often, and each wrong guess wastes work, so the same loop runs slower.

open as a page

Walk through what happens when an ArrayList outgrows its backing array, and quantify the cost of building a large list from the default capacity versus pre-sizing.

level: middleimportance: should knowfreq 40%

basics

~20 s

When an ArrayList's backing array is full and you add one more element, it creates a new array about 1.5x larger, copies all existing elements over, and discards the old array. Building a big list from the default capacity (10) triggers many such grow-and-copy cycles; pre-sizing does the allocation once.

open as a page

What is escape analysis in the JVM, and what optimizations does it enable?

level: middleimportance: should knowfreq 45%

basics

~20 s

Escape analysis is a JIT compiler technique that figures out whether an object created in a method stays inside that method. If it never "escapes," the compiler can avoid allocating it on the heap, which removes work and garbage collection pressure.

open as a page

What concrete downsides and bugs can object pooling introduce, beyond just 'added complexity'?

level: middleimportance: should knowfreq 45%

basics

~20 s

Pooled objects can carry over old data if you forget to reset them, leak if you never return them, or get corrupted if two callers use the same one. The pool also needs locking, which can be slower than just creating objects, and it keeps objects alive so the GC moves them to the slower old generation.

open as a page

Why are thread pools and connection pools considered good uses of pooling, when pooling ordinary objects is discouraged?

level: middleimportance: should knowfreq 50%

basics

~20 s

Threads and connections are very expensive to create and limited in number, so reusing them saves real time and protects a scarce resource. Ordinary objects are cheap to create and the GC cleans them up almost for free, so reusing them adds cost without saving much.

open as a page

Why should you prioritize algorithmic complexity over micro-optimizations, and how does the 80/20 rule guide where to spend effort?

level: middleimportance: should knowfreq 60%

basics

~20 s

A better algorithm (e.g. O(n log n) instead of O(n²)) scales far better than tiny tweaks as data grows. The 80/20 rule says most time is spent in a small part of the code, so fix that part first.

open as a page

What loop optimizations does the JVM's JIT compiler perform automatically — loop unrolling, range-check (bounds-check) elimination — and why does that mean you usually shouldn't hand-unroll loops in Java?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Java runs an interpreter first, then a JIT compiler turns hot loops into optimized machine code. It automatically unrolls loops and removes most array bounds checks. Because it does this for you using runtime profiling, hand-unrolling usually just makes code uglier without helping, or even blocks the JIT's own optimizations.

open as a page

When is pre-sizing collections a premature or counterproductive optimization, and how would you decide where it's actually worth applying?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Pre-sizing only helps when the final size is known and the code is hot. If sizes are small, the path is cold, or the size is unknown, the constructor argument adds clutter without measurable benefit — and a bad over-estimate wastes memory. Measure first; apply it on proven hot, known-size build paths.

open as a page

Explain the three escape states (NoEscape, ArgEscape, GlobalEscape) and give an example of code that produces each.

level: seniorimportance: should knowfreq 35%

basics

~20 s

NoEscape means the object stays inside the method. ArgEscape means it is only handed to another method it calls. GlobalEscape means it leaks out — for example by being returned or stored in a field — so the JVM can no longer optimize away its allocation.

open as a page

What is scalar replacement, how does it differ from stack allocation, and why does it make many small short-lived objects effectively free?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Scalar replacement means the JIT takes a non-escaping object and replaces it with its individual fields, held in CPU registers or on the stack, so the object is never actually created. There is no heap memory and nothing for the garbage collector to clean up, so the object costs almost nothing.

open as a page

What is double-checked locking, why was it broken before Java 5, and how does volatile fix it?

level: seniorimportance: should knowfreq 60%

basics

~20 s

Double-checked locking checks a field for null without a lock, and only locks and builds the object if it's still null. Before Java 5 it could hand back a half-built object because writes could be reordered. Marking the field volatile prevents that reordering and makes the fix work.

open as a page

How can you implement thread-safe lazy initialization using a Supplier or java.util.concurrent helpers, and what are the trade-offs versus the holder idiom?

level: seniorimportance: should knowfreq 48%

basics

~20 s

You wrap the expensive computation in a Supplier and cache its result the first time it's called, so later calls return the cached value. Helpers like ConcurrentHashMap.computeIfAbsent or a memoizing Supplier make this thread-safe and let you parameterize or reset it, which the holder idiom can't.

open as a page

How do TLABs and escape analysis make object allocation cheap on the JVM, and what do they imply about pooling plain objects?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Each thread gets its own slice of memory (a TLAB) so creating an object is just moving a pointer, with no locking. The JIT can also notice an object never leaves a method (escape analysis) and skip the heap entirely. Both make allocation so cheap that pooling plain objects rarely helps.

open as a page

What work does the JVM (JIT and GC) already do for you, and how does trusting it shape when to hand-optimize Java?

level: seniorimportance: should knowfreq 55%

basics

~20 s

The JVM's JIT compiler turns hot code into fast native code (inlining, removing dead code, optimizing loops), and the garbage collector cheaply reclaims short-lived objects. So many manual tweaks are unnecessary — measure first; only hand-optimize what the JVM provably doesn't handle.

open as a page

Since Java 9, how does the compiler handle the + operator on Strings, and why can it optimize a single concatenation expression but not concatenation spread across loop iterations?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The compiler turns one concatenation expression (like a + b + c) into a single efficient operation, so plain + outside loops is fast. But in a loop, each += is its own separate step that depends on the previous turn's result, so the compiler can't merge them into one builder for you.

open as a page

showing 1–30 of 39