W3docs

Java Performance Tips

Practical performance tips for Java — measure first, avoid premature optimization, common micro-optimizations.

Java is fast enough for almost everything, but slow code still happens — usually from doing too much work, allocating too much memory, or picking the wrong data structure. The most important performance tip is not a trick at all: measure before you change anything. This chapter covers how to measure honestly, the handful of optimizations that pay off most often (string building, data-structure choice, avoiding needless allocation), and the gotchas that make naive "fast" code actually slow.

Measure first, optimize second

Guessing about performance is how you spend hours speeding up code that was never slow. Profile a real workload, find the hot path (the small fraction of code where most of the time is actually spent), and only then optimize it. For quick experiments, System.nanoTime() gives a wall-clock delta; for serious benchmarks use a tool like JMH (Java Microbenchmark Harness), which warms up the JIT and accounts for measurement noise.

long start = System.nanoTime();
doWork();
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("Took " + elapsedMs + " ms");

Two rules go with this. First, avoid premature optimization — clear code that is fast enough beats clever code that is hard to read. Second, the JVM's JIT compiler optimizes hot code at runtime, so a method only reaches full speed after it has run many times; a single timed call tells you little.

Build strings with StringBuilder

Because strings are immutable, s += x inside a loop creates a brand-new string and copies every previous character on each iteration — that is O(n²) work. StringBuilder keeps a growable buffer and appends in place, turning the same job into O(n).

// Slow: a new String allocated every iteration
String csv = "";
for (String field : fields) {
    csv += field + ",";
}

// Fast: one buffer, appended in place
StringBuilder sb = new StringBuilder();
for (String field : fields) {
    sb.append(field).append(',');
}
String csv2 = sb.toString();

A single a + b + c outside a loop is fine — the compiler already turns it into one StringBuilder (see string concatenation for what the compiler does). The problem is concatenation inside a loop, where each pass adds another full copy.

Pick the right data structure

The biggest wins usually come from algorithmic choices, not micro-tweaks. Looking up a value in an ArrayList scans every element (O(n)); a HashMap or HashSet does it in roughly constant time (O(1)). Choose the collection that matches how you actually access the data.

NeedUseLookup cost
Index access, append at endArrayListO(1) by index, O(n) by value
Key/value lookupHashMapO(1) average
Membership test, no duplicatesHashSetO(1) average
Sorted keysTreeMapO(log n)
Frequent insert/remove at endsArrayDequeO(1) at ends

If you know the final size, pass it to the constructor: new ArrayList<>(10_000) or new HashMap<>(capacity). This avoids the repeated reallocation and copying that happens as a collection grows.

Avoid needless object creation

Every object allocated must later be collected, and garbage collection is not free. Reuse immutable values, prefer primitives over their boxed wrappers in tight loops, and don't create objects you immediately throw away.

// Autoboxing: every += boxes a new Integer
Long total = 0L;
for (int i = 0; i < n; i++) total += i;   // slow, allocates boxes

// Primitive: no allocation at all
long sum = 0L;
for (int i = 0; i < n; i++) sum += i;     // fast

Other easy wins: cache compiled Pattern objects instead of calling String.matches() in a loop, reuse a DateTimeFormatter (it's thread-safe and immutable), and favor enhanced for over streams in the hottest inner loops where allocation matters.

java— editable, runs on the server

What to take from the run:

  • Same result? true proves StringBuilder produces exactly the same string as +=, so swapping it in changes only speed, never correctness.
  • The printed "x slower" ratio shows loop concatenation costs many times more than appending to a buffer, because each += copies the whole string so far.
  • Both lists size 100000: true confirms a pre-sized ArrayList ends up identical to a grown one — the constructor hint affects allocation, not contents.
  • Pre-sized faster? true shows that telling ArrayList its capacity up front avoids the repeated resize-and-copy steps.
  • Map lookups found: 50000 in ... ms demonstrates that 50,000 HashMap lookups finish in roughly a millisecond, the payoff of choosing O(1) access over an O(n) list scan.

Common pitfalls

A few mistakes turn "obviously faster" code into the opposite:

  • Trusting a single timed run. The JIT hasn't warmed up yet, and the OS may have scheduled something else mid-measurement. Repeat the work thousands of times, or use JMH, before believing a number.
  • Micro-optimizing cold code. A method that runs once at startup gains nothing from a tighter loop. Spend effort only on the hot path the profiler points to.
  • String building with + in a loop. The single most common avoidable slowdown — reach for StringBuilder whenever you concatenate inside a loop.
  • Hidden autoboxing. A List<Integer>, Map<Integer, Integer>, or a stray Long accumulator boxes every value. In a tight numeric loop, prefer primitives and primitive arrays.
  • Optimizing before it works. Correct first, fast second. Clear code you can profile beats clever code you can't reason about.

Summary

  • Measure before changing anything — profile a real workload and optimize only the hot path.
  • Build strings with StringBuilder, not +=, inside loops.
  • Match the collection to the access pattern: HashMap/HashSet for lookups, ArrayList for indexed access; pre-size when the count is known.
  • Avoid needless allocation: prefer primitives, reuse immutable objects, and remember that every object adds garbage-collection work.

Practice

Practice
Why is using '+=' to build a String inside a loop slow compared to StringBuilder?
Why is using '+=' to build a String inside a loop slow compared to StringBuilder?
Was this page helpful?