Performance Basics in Java

Naveen AnamaneniUpdated · 8 min read

If you want to reach school faster, buying expensive running shoes helps a little. Not taking the long way round helps a lot more. Code is similar. Most programs don't need clever tricks to be fast; they need to avoid a handful of well-known slow paths that turn a millisecond job into a second-long one. In this lesson, you will learn those everyday performance rules (building strings, boxing, picking and sizing collections) with real timings measured on Java 25, and the most important rule of all: measure before you optimize.

What Are Performance Basics?

Performance basics in Java are a small set of habits that are used to avoid unnecessary work in ordinary code, without making it harder to read. They are not about tuning the JVM or rewriting algorithms. They are the kind of choices a reviewer points out in a pull request: "use a StringBuilder here", "this should be a Set".

The timings in this lesson were measured on an Apple Silicon laptop with Java 25, after warming up the code. Your numbers will differ, but the differences between the choices are what matter.

ChoiceSlow wayFast wayMeasured difference
Building a 20,000-character string in a loops += xStringBuilder.append8.8 ms vs 0.07 ms (about 130 times)
Adding 10 million numbersLong sumlong sum21 ms vs 2.3 ms (about 9 times)
50,000 lookups in 50,000 itemsList.containsSet.contains500 ms vs 0.2 ms (thousands of times)
Adding 1 million items to a listno initial sizenew ArrayList<>(1_000_000)6.4 ms vs 5.6 ms (about 10 percent)

Notice the last row: the famous "always set the capacity" tip gave the smallest gain. The big wins come from avoiding the wrong tool, not from fine-tuning the right one.

Rule 1: Use StringBuilder to Build Strings in Loops

A String is immutable. s += x inside a loop creates a new string every time and copies all the old characters into it, so the work grows with the square of the length. A StringBuilder grows one buffer in place.

RollCall.java▶ Try it
public class RollCall {
    public static void main(String[] args) {
        String[] names = {"Navika", "Srihitha", "Advitha"};
 
        StringBuilder line = new StringBuilder();
        for (int i = 0; i < names.length; i++) {
            if (i > 0) {
                line.append(", ");
            }
            line.append(i + 1).append(". ").append(names[i]);
        }
        System.out.println(line);
 
        System.out.println(String.join(" | ", names));
    }
}
Output
1. Navika, 2. Srihitha, 3. Advitha
Navika | Srihitha | Advitha

Explanation: the loop appends to one builder and converts it to a string once, when printing. For simple joining, String.join (or Collectors.joining with streams) does the same job in one line.

Note that a single expression like "Total: " + total + " rupees" is fine. The compiler already optimizes concatenation within one statement. The problem is only += across loop iterations. See "String Concatenation in Java" for the details.

Rule 2: Avoid Boxing in Hot Loops

Long, Integer and Double are objects. When you do arithmetic on them, Java unboxes, computes, and boxes the result into a new object each time. In a loop that runs millions of times, that means millions of short-lived objects.

Totals.java▶ Try it
public class Totals {
    public static void main(String[] args) {
        long fast = 0;                       // primitive: no objects
        for (int i = 1; i <= 1_000_000; i++) {
            fast += i;
        }
 
        Long slow = 0L;                      // boxed: a new Long most steps
        for (int i = 1; i <= 1_000_000; i++) {
            slow += i;
        }
 
        System.out.println(fast);
        System.out.println(slow);
    }
}
Output
500000500000
500000500000

Explanation: both loops print the same sum, but the second creates a new Long object on almost every step (only values from -128 to 127 are cached). With 10 million steps, the boxed version was about 9 times slower. A one-letter difference, Long versus long, is easy to miss in review.

The same applies to collections: a List<Integer> with a million items holds a million objects. If you only need numbers, an int[] or an IntStream is much lighter.

Rule 3: Pick the Right Collection

The biggest single performance mistake in everyday Java is using a List where you need fast lookups.

You need toUseLookup cost
Keep order and access by positionArrayListget(i) is instant; contains checks every item
Check "is this in the group?"HashSetcontains is nearly instant at any size
Look up a value by keyHashMapget is nearly instant at any size
Keep items sortedTreeSet, TreeMapFast, grows slowly with size
Add and remove at both endsArrayDequeInstant at both ends

list.contains(x) looks at every element until it finds a match. Doing that inside a loop over another large list multiplies the work: in the measured test, 50,000 lookups took about half a second with a list and a fifth of a millisecond with a set.

Rule 4: Size Collections When You Know the Size

An ArrayList starts small and copies itself into a bigger array whenever it fills up. A HashMap rebuilds its table when it gets 75 percent full. If you know roughly how many items are coming, say so up front:

CollectionSized for about n items
ArrayListnew ArrayList<>(n)
HashMap (Java 19+)HashMap.newHashMap(n)
HashSet (Java 19+)HashSet.newHashSet(n)
StringBuildernew StringBuilder(expectedLength)

Note that new HashMap<>(n) sets the table size, not the number of entries it can hold before growing. Because of the 75 percent rule, it still resizes before it reaches n entries. HashMap.newHashMap(n) does the arithmetic for you.

As the table at the top shows, sizing is a small gain: about 10 percent for a million-item list, and too small to measure for the map in that test. Do it when the size is known and the code is hot; don't contort code for it.

Rule 5: Measure Before You Optimize

Donald Knuth's famous warning is that premature optimization is the root of all evil: time spent speeding up code that isn't slow is wasted, and it usually makes the code harder to read.

  1. Write clear code first.
  2. Measure with a profiler (IntelliJ IDEA's profiler, JDK Flight Recorder, VisualVM) to find where time actually goes.
  3. Fix the hot spot, usually a wrong collection, a loop inside a loop, or repeated work such as reading the same file or querying the database in a loop.
  4. Measure again to confirm the change helped.

For small, precise comparisons, use JMH, the benchmarking tool built by the OpenJDK team. A loop around System.nanoTime() is easily fooled by JIT warm-up and dead-code removal.

In Practice

Seen in reviewSuggest
result += ... in a loopStringBuilder or String.join
Long total = 0L; used for arithmeticlong total = 0;
list.contains(x) inside a loopBuild a HashSet once, then look up
Database query or file read inside a loopRead once before the loop, or batch the work
Hand-tuned bit tricks in ordinary business codeClear code; let the JIT do its job

Common Mistakes

Optimizing without measuring. You may speed up code that runs once a day and miss the loop that runs a million times.

Trusting a quick nanoTime test. The first runs include class loading and interpretation; the JIT may also remove code whose result isn't used.

Making code unreadable for a tiny gain. A 2 percent improvement isn't worth confusing every future reader.

Ignoring algorithm choice. No amount of tuning makes a nested loop over two large lists fast. Fix the data structure first.

Practice

  1. Easy: In Totals.java, change both loops to run 100 million times. Time the program with each loop commented out in turn, and compare.
  2. Harder: Write a program with a list of 100,000 student IDs and a second list of 10,000 IDs to check. Count how many of the second list appear in the first, once with List.contains and once after copying the first list into a HashSet. Print both counts (they must match) and both times.

Quick check: Performance basics

4 questions
1Why is s += x slow inside a loop?
2Which is better for adding up millions of numbers?
3You need to check many times whether a student ID is in a group. Which collection?
4What should you do before optimizing?

Key Takeaways

Was this tutorial helpful?

Lesson 540 of 578