Quick Reference
Map<String, Integer> counts = new HashMap<>();
counts.put("java", counts.getOrDefault("java", 0) + 1);
counts.merge("java", 1, Integer::sum);
Map<Long, List<Order>> ordersByUser = new HashMap<>();
ordersByUser
.computeIfAbsent(userId, id -> new ArrayList<>())
.add(order);getOrDefault는 값을 저장하지 않고 fallback만 돌려주며, computeIfAbsent는 없을 때 만든 값을 저장합니다. 기존 값과 새 값을 합칠 때는 merge를 쓰고, remapping 함수가 null을 반환하면 해당 mapping은 제거됩니다.
선택 기준
어떤 Map 메서드를 먼저 떠올리면 되나
| 상황 | 먼저 볼 메서드 |
|---|---|
| 없을 때 기본값만 읽기 | getOrDefault |
| 없을 때 값을 만들어 저장 | computeIfAbsent |
| 있을 때만 값 갱신 | computeIfPresent |
| 있든 없든 현재 값 기준으로 재계산 | compute |
| 누적, 합산, 충돌 병합 | merge |
getOrDefault: 없을 때 기본값으로 읽기
getOrDefault는 map을 바꾸지 않고 읽기 기본값만 제공합니다. 집계 계산을 할 때는 결과를 다시 put해야 하고, 존재하지 않는 key를 실제로 추가하지 않습니다.
Map<String, Integer> scores = new HashMap<>();
int kimScore = scores.getOrDefault("kim", 0);
// 읽기 기본값만 제공하므로 map에는 아직 "kim" key가 없음
System.out.println(scores.containsKey("kim")); // false
scores.put("kim", kimScore + 10);computeIfAbsent: 없을 때만 생성해서 저장
computeIfAbsent는 캐시, 그룹핑, key별 리스트 누적에 잘 맞습니다. key가 없거나 값이 null이면 mapping function을 실행하고, 반환값이 null이 아니면 map에 저장합니다.
Map<Long, List<Order>> ordersByUser = new HashMap<>();
for (Order order : orders) {
ordersByUser
.computeIfAbsent(order.userId(), id -> new ArrayList<>())
.add(order);
}
Map<String, Pattern> patternCache = new HashMap<>();
Pattern pattern = patternCache.computeIfAbsent(
regex,
key -> Pattern.compile(key)
);merge: 누적과 충돌 정책을 한 줄로
merge(key, value, remappingFunction)은 key가 없으면 새 값을 넣고, key가 있으면 기존 값과 새 값을 합쳐 저장합니다. 빈도 계산, 합계 누적, 중복 key 충돌 정책에 적합합니다.
Map<String, Integer> wordCounts = new HashMap<>();
for (String word : words) {
wordCounts.merge(word, 1, Integer::sum);
}
Map<Long, User> usersById = new HashMap<>();
for (User user : users) {
usersById.merge(
user.id(),
user,
(oldUser, newUser) -> newUser.updatedAt().isAfter(oldUser.updatedAt())
? newUser
: oldUser
);
}compute 계열 mapping function 안에서 같은 map을 다시 구조적으로 변경하면 구현체별 제약이나 예외가 생길 수 있습니다. mapping function은 짧고 부작용 없이 작성하고, 외부 I/O나 긴 작업은 map 밖에서 끝낸 뒤 결과만 넣습니다. ConcurrentHashMap은 null key/value를 허용하지 않으므로 일반 HashMap의 null 정책을 그대로 옮길 수 없습니다.
체크포인트
| 목적 | 패턴 |
|---|---|
| 읽을 때만 기본값 사용 | map.getOrDefault(key, defaultValue) |
| key별 리스트 만들기 | map.computeIfAbsent(key, k -> new ArrayList<>()).add(value) |
| 숫자 누적 | map.merge(key, delta, Integer::sum) |
| null 반환 시 mapping 제거 | compute, computeIfPresent, merge의 remapping 결과 확인 |
| 동시 접근 map | ConcurrentHashMap의 원자성 보장 범위 확인 |
computeIfAbsent가 "한 번만 실행된다"는 가정도 구현체와 경쟁 상황을 확인해야 합니다. expensive loader에는 timeout, failure cache, 요청량 제한을 별도로 두고, map 원자성만으로 외부 resource 제어를 해결하려 하지 않습니다.
주의사항
getOrDefault는 값을 저장하지 않습니다. 기본값을 읽은 뒤 map에 key가 생겼다고 착각하면 이후 containsKey나 반복 처리에서 결과가 달라집니다.
Map<String, Integer> counts = new HashMap<>();
int count = counts.getOrDefault("missing", 0);
System.out.println(count); // 0
System.out.println(counts.containsKey("missing")); // falsemerge와 compute 계열에서 remapping 함수가 null을 반환하면 mapping이 제거될 수 있습니다. 삭제 의도가 아니라면 반환값이 null이 되지 않도록 분기를 명확히 두세요.
Map<String, Integer> counts = new HashMap<>();
counts.put("java", 1);
counts.merge("java", 1, (oldValue, newValue) -> null);
System.out.println(counts.containsKey("java")); // false참고 링크
2 sources