Boost Java Loop Performance: Replace Nested Loops with a HashMap
This article demonstrates how to dramatically speed up Java code that matches items between two large lists by eliminating nested loops, using early‑exit with break and, more effectively, pre‑building a HashMap for O(1) lookups, with concrete timing results and full code examples.
When you need to find matching IDs between two collections—such as a
Userlist and a
UserMemolist—many developers write a nested
forloop, which becomes extremely slow as the data size grows.
Example entity classes:
<code>@Data
public class User {
private Long userId;
private String name;
}
</code> <code>@Data
public class UserMemo {
private Long userId;
private String content;
}
</code>Test data generation creates 50,000
Userobjects and 30,000
UserMemoobjects.
<code>public static List<User> getUserTestList() {
List<User> users = new ArrayList<>();
for (int i = 1; i <= 50000; i++) {
User user = new User();
user.setName(UUID.randomUUID().toString());
user.setUserId((long) i);
users.add(user);
}
return users;
}
public static List<UserMemo> getUserMemoTestList() {
List<UserMemo> userMemos = new ArrayList<>();
for (int i = 30000; i >= 1; i--) {
UserMemo userMemo = new UserMemo();
userMemo.setContent(UUID.randomUUID().toString());
userMemo.setUserId((long) i);
userMemos.add(userMemo);
}
return userMemos;
}
</code>Naïve nested loop implementation:
<code>for (User user : userTestList) {
Long userId = user.getUserId();
for (UserMemo userMemo : userMemoTestList) {
if (userId.equals(userMemo.getUserId())) {
String content = userMemo.getContent();
System.out.println("Processing content: " + content);
}
}
}
</code>This approach iterates 5 × 10⁴ × 3 × 10⁴ ≈ 1.5 billion comparisons, taking about 26,857 ms on the test data.
Adding a
breakafter finding the first match reduces the inner loop work, cutting the time to roughly 10,000 ms .
For a scalable solution, pre‑populate a
Map<Long, String>from the
UserMemolist and then perform a single lookup per user:
<code>public static void main(String[] args) {
List<User> userTestList = getUserTestList();
List<UserMemo> userMemoTestList = getUserMemoTestList();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// Build map: userId → content
Map<Long, String> contentMap = userMemoTestList.stream()
.collect(Collectors.toMap(UserMemo::getUserId, UserMemo::getContent));
for (User user : userTestList) {
Long userId = user.getUserId();
String content = contentMap.get(userId);
if (StringUtils.hasLength(content)) {
System.out.println("Processing content: " + content);
}
}
stopWatch.stop();
System.out.println("Final time: " + stopWatch.getTotalTimeMillis());
}
</code>Running this version drops the execution time to just a few hundred milliseconds, because the map provides average‑case O(1) access.
The improvement stems from changing the algorithmic complexity from quadratic (
O(n²)) to linear (
O(n)). A hash‑based map hashes the
userIdto an array index, yielding near‑constant lookup time; collisions are rare with Java 8’s hash function, and even in the worst case the cost degrades only to
O(log n)or
O(n)for a single bucket.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.