Map 키와 갱신 연산
배열을 Map으로 변환하고 공통 키·단어 빈도·회원 저장소·게시글 반응 집계를 서로 다른 키 중복 정책으로 해결합니다.
Map 문제는 문법보다 키를 무엇으로 정하는지가 어렵습니다.
이름을 키로 고르면 동명이인을 합칠 수 있고, 상품 객체 전체를 키로 쓰면 가격 변경이 조회를 깨뜨릴 수 있습니다.
문제 문장에서 고유성·갱신·부재의 의미를 먼저 표시한 뒤 put, putIfAbsent, merge, computeIfAbsent 중 맞는 연산을 고릅니다.
containsKey 갱신 누락
분기 안에서 계산만 하고 put을 하지 않으면 두 번째 등장도 1로 남습니다.
컴파일과 실행은 성공하지만 결과가 틀립니다.
import java.util.HashMap;
import java.util.Map;
public final class WordCountUpdateBug {
public static void main(String[] args) {
Map<String, Integer> counts = new HashMap<>();
for (String word : new String[] {"map", "hash", "map"}) {
if (counts.containsKey(word)) {
counts.get(word) + 1;
} else {
counts.put(word, 1);
}
}
System.out.println(counts);
}
}이 코드는 Java에서 단독 덧셈 표현식이 문장으로 허용되지 않아 컴파일 실패합니다.
int next = counts.get(word) + 1로 바꿔도 put을 빠뜨리면 논리 버그가 남습니다.
merge는 읽기·계산·쓰기를 한 호출로 묶어 이 누락을 줄입니다.
배열·Map 변환의 중복 키 정책
회원 ID 배열과 이름 배열을 Map으로 묶을 때는 두 배열의 길이가 같은지 검사해야 합니다.
같은 ID가 두 번 나오면 마지막 값으로 교체할지, 첫 값을 지킬지, 오류로 중단할지 정책을 정해야 합니다.
무조건 put하면 마지막 값이 이기는 정책이 암묵적으로 생깁니다.
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public final class MapProblemWorkbook {
public static void main(String[] args) {
System.out.println(toMap(new int[] {11, 12}, new String[] {"Ada", "Linus"}));
System.out.println(common(Map.of("array", 40, "hash", 50), Map.of("hash", 45, "map", 30)));
System.out.println(countWords("map hash map queue hash map"));
System.out.println(findByValue(Map.of("A", 50, "B", 70, "C", 50), 50));
}
static Map<Integer, String> toMap(int[] ids, String[] names) {
if (ids.length != names.length) throw new IllegalArgumentException("length mismatch");
Map<Integer, String> result = new LinkedHashMap<>();
for (int i = 0; i < ids.length; i++)
if (result.putIfAbsent(ids[i], names[i]) != null)
throw new IllegalArgumentException("duplicate id=" + ids[i]);
return Map.copyOf(result);
}
static Set<String> common(Map<String, Integer> left, Map<String, Integer> right) {
Set<String> keys = new java.util.HashSet<>(left.keySet());
keys.retainAll(right.keySet());
return Set.copyOf(keys);
}
static Map<String, Integer> countWords(String line) {
Map<String, Integer> counts = new TreeMap<>();
for (String word : line.split("\\s+")) {
counts.merge(word, 1, Integer::sum);
}
return Map.copyOf(counts);
}
static Set<String> findByValue(Map<String, Integer> source, int target) {
Set<String> keys = new java.util.TreeSet<>();
for (var entry : source.entrySet())
if (entry.getValue() == target) keys.add(entry.getKey());
return Set.copyOf(keys);
}
}값은 고유하지 않을 수 있으므로 역조회 결과를 키 하나로 반환하지 않습니다.
대상 50에는 A와 C 둘 다 해당합니다.
값 조회가 빈번하다면 매번 전체 entrySet을 걷는 대신 Map<Value, Set<Key>> 보조 인덱스를 유지할 수 있지만 갱신 일관성 비용이 생깁니다.
사전과 회원 저장소의 부재 표현
사전 조회에서 없는 단어는 null, 예외, Optional, 추천 목록 중 하나로 표현할 수 있습니다.
UI 자동 완성이라면 빈 결과가 정상이고, 내부 필수 설정이라면 예외가 결함을 빨리 드러냅니다.
Map.get의 null을 그대로 모든 계층에 흘리지 않습니다.
회원 저장소는 id 중복 저장을 거부하고 findById에서 Optional을 반환할 수 있습니다.
갱신은 존재하지 않는 id를 새로 만들지, 실패할지 별도 메서드로 나눕니다.
저장 하나가 insert와 갱신을 모두 뜻하면 호출자의 실수를 숨깁니다.
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
public final class MemberRepositoryMap {
public static void main(String[] args) {
Repository repository = new Repository();
repository.insert(new Member("M1", "Andongmin"));
repository.updateName("M1", "Dongmin");
System.out.println(repository.find("M1").orElseThrow());
System.out.println("missing=" + repository.find("M2"));
}
private static final class Repository {
private final Map<String, Member> members = new LinkedHashMap<>();
void insert(Member member) {
if (members.putIfAbsent(member.id(), member) != null)
throw new IllegalArgumentException("duplicate");
}
void updateName(String id, String name) {
Member before = members.get(id);
if (before == null) throw new java.util.NoSuchElementException(id);
members.put(id, new Member(id, name));
}
Optional<Member> find(String id) {
return Optional.ofNullable(members.get(id));
}
}
private record Member(String id, String name) {}
}Member를 불변 record로 두고 이름 변경은 같은 id의 새 값을 put합니다.
키로 쓰는 id는 바뀌지 않습니다.
저장소 내부 Map은 외부에 노출하지 않습니다.
게시글 ID와 반응 수 모델
Map<Post, Integer>는 Post의 equals/hashCode가 게시글 식별 의미와 정확히 맞아야 합니다.
더 단순하게 postId를 키로 쓰고 제목은 게시글 저장소에서 조회할 수 있습니다.
집계 시점 제목을 고정해야 한다면 ReactionLine 값에 게시글 정보와 반응 수를 함께 보관합니다.
반응 수 추가는 merge로 표현할 수 있지만 취소와 상한 같은 규칙이 생기면 명시적 메서드가 읽기 쉽습니다.
반응 수가 0인 항목을 Map에 남길지 제거할지도 불변식으로 정합니다.
import java.util.LinkedHashMap;
import java.util.Map;
public final class BoardReactionCounter {
public static void main(String[] args) {
Counter counter = new Counter();
counter.add(new Post("P1", "Java 컬렉션"), 1);
counter.add(new Post("P1", "Java 컬렉션"), 2);
counter.add(new Post("P2", "스레드 기초"), 1);
System.out.println(counter.lines());
System.out.println("total=" + counter.totalReactions());
}
private static final class Counter {
private final Map<String, Line> lines = new LinkedHashMap<>();
void add(Post post, int amount) {
if (amount <= 0) throw new IllegalArgumentException("amount");
lines.compute(
post.id(),
(id, line) ->
line == null
? new Line(post, amount)
: new Line(line.post(), line.count() + amount));
}
int totalReactions() {
return lines.values().stream().mapToInt(Line::count).sum();
}
Map<String, Line> lines() {
return Map.copyOf(lines);
}
}
private record Post(String id, String title) {}
private record Line(Post post, int count) {}
}연습 문제
게시글 제목→범주 Map을 범주→제목 Set Map으로 뒤집으세요.
같은 범주의 모든 제목을 보존하고 결과 제목은 입력 순서를 유지합니다.
정답과 해설
computeIfAbsent는 범주가 처음 나올 때만 LinkedHashSet을 만들고 현재 제목을 추가합니다.
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
public final class ReverseGroupingMapSolution {
public static void main(String[] args) {
Map<String, String> source = new LinkedHashMap<>();
source.put("array", "collection");
source.put("hash", "collection");
source.put("thread", "concurrency");
Map<String, Set<String>> grouped = new LinkedHashMap<>();
for (var e : source.entrySet())
grouped.computeIfAbsent(e.getValue(), k -> new LinkedHashSet<>()).add(e.getKey());
System.out.println(grouped);
}
}컬렉션에는 배열과 해시가 함께 남습니다.
값이 중복될 수 있으므로 역방향 자료형은 Set 하나가 아니라 범주마다 Set을 가진 Map입니다.
Map 문제의 답은 API 선택 전에 키의 고유성, 중복 put 의미, 없는 키 처리, 값의 다중성을 적는 데서 시작합니다.
이 네 질문이 맞으면 merge와 compute 계열도 목적에 맞게 선택할 수 있습니다.