같은 문제를 List·Set·Map으로 각각 풀어 보고, 루프와 스트림의 통계를 나란히 놓고, 상위 N개 조회를 범용 메서드로 빼고, 로그 이벤트에 Map·Deque를 적용한 뒤, 불변 컬렉션과 뷰의 엣지 케이스를 확인한다.
이메일 목록의 중복을 세 가지 자료구조로 처리한다. List.contains는 작은 데이터에서 코드가 가장 단순하지만 O(n²)이고, Set.add의 반환값을 쓰면 한 번의 순회로 "처음 본 것"과 "중복"을 나눌 수 있으며, 횟수까지 필요하면 Map.merge다. 무엇을 알아야 하는지(존재 여부·중복 목록·횟수)에 따라 고른다.
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> emails = List.of("[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]");
// 1) List.contains — 작을 때만
List<String> viaList = new ArrayList<>();
for (String e : emails) if (!viaList.contains(e)) viaList.add(e);
// 2) Set.add 반환값 — 중복이면 false
Set<String> seen = new LinkedHashSet<>();
List<String> dups = new ArrayList<>();
for (String e : emails) if (!seen.add(e)) dups.add(e);
// 3) Map.merge — 횟수까지
Map<String, Integer> count = new LinkedHashMap<>();
for (String e : emails) count.merge(e, 1, Integer::sum);
System.out.println(viaList); // 출력: [[email protected], [email protected], [email protected]]
System.out.println(seen + " 중복=" + dups); // 출력: [[email protected], [email protected], [email protected]] 중복=[[email protected], [email protected]]
System.out.println(count); // 출력: {[email protected]=2, [email protected]=2, [email protected]=1}
}
}예제 2의 주문 통계를 루프와 스트림으로 나란히 쓴다. 루프는 변수 세 개를 직접 관리하고, 스트림은 summaryStatistics()가 건수·합계·최대·평균을 한 번에 준다. 조건이 하나이고 집계가 여러 개면 스트림이 짧고, 집계 중간에 복잡한 분기가 끼면 루프가 읽기 쉽다.
import java.util.*;
import java.util.stream.Collectors;
record Order(String id, String status, long amount) {}
public class Main {
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("O-1", "PAID", 30_000),
new Order("O-2", "PAID", 12_000),
new Order("O-3", "CANCELLED", 50_000),
new Order("O-4", "SHIPPED", 8_000),
new Order("O-5", "PAID", 99_000)
);
// 루프
long sum = 0, max = Long.MIN_VALUE;
int n = 0;
for (Order o : orders) {
if (!o.status().equals("PAID")) continue;
sum += o.amount();
max = Math.max(max, o.amount());
n++;
}
System.out.printf("루프 : %d건 합계 %,d 최대 %,d%n", n, sum, max);
// 스트림
LongSummaryStatistics st = orders.stream()
.filter(o -> o.status().equals("PAID"))
.mapToLong(Order::amount)
.summaryStatistics();
System.out.printf("스트림: %d건 합계 %,d 최대 %,d 평균 %.0f%n", st.getCount(), st.getSum(), st.getMax(), st.getAverage());
// 상태별 합계도 한 줄
Map<String, Long> byStatus = orders.stream()
.collect(Collectors.groupingBy(Order::status, TreeMap::new, Collectors.summingLong(Order::amount)));
System.out.println(byStatus);
// 출력:
// 루프 : 3건 합계 141,000 최대 99,000
// 스트림: 3건 합계 141,000 최대 99,000 평균 47000
// {CANCELLED=50000, PAID=141000, SHIPPED=8000}
}
}"빈도 상위 N개"는 단어·상품·오류코드 어디에나 나온다. Map<String, Integer>와 n을 받아 값 내림차순, 같으면 키 오름차순으로 정렬한 엔트리 목록을 돌려주는 메서드로 빼면 재사용된다. n이 크기보다 크거나 맵이 비어 있어도 Math.min과 subList 덕분에 예외 없이 동작한다.
import java.util.*;
public class Main {
static List<Map.Entry<String, Integer>> topN(Map<String, Integer> counts, int n) {
List<Map.Entry<String, Integer>> list = new ArrayList<>(counts.entrySet());
list.sort(Map.Entry.<String, Integer>comparingByValue(Comparator.reverseOrder())
.thenComparing(Map.Entry.comparingByKey()));
return list.subList(0, Math.min(n, list.size()));
}
public static void main(String[] args) {
Map<String, Integer> wordCount = new HashMap<>();
for (String w : "the cat and the dog and the bird".split(" ")) {
wordCount.merge(w, 1, Integer::sum);
}
System.out.println(topN(wordCount, 2)); // 출력: [the=3, and=2]
System.out.println(topN(wordCount, 10)); // 출력: [the=3, and=2, bird=1, cat=1, dog=1]
System.out.println(topN(Map.of(), 3)); // 출력: []
Map<String, Integer> errorCodes = Map.of("E500", 7, "E404", 12, "E403", 7);
System.out.println(topN(errorCodes, 2)); // 출력: [E404=12, E403=7] (동점은 키 순)
}
}서비스별 마지막 이벤트 시각은 merge에 "큰 쪽을 남기는" 함수를 넘겨 한 줄로 구하고, 최근 3건만 유지하는 창은 ArrayDeque로 만든다. 서비스별 목록은 computeIfAbsent로 첫 등장 순서를 지킨다. 세 가지 모두 실무 모니터링 코드의 단골 형태다.
import java.util.*;
public class Main {
public static void main(String[] args) {
String[] events = {"order 10:01", "payment 10:02", "order 10:05", "member 10:03", "order 10:04"};
// 서비스별 가장 늦은 시각
Map<String, String> latest = new TreeMap<>();
for (String ev : events) {
String[] p = ev.split(" ");
latest.merge(p[0], p[1], (a, b) -> a.compareTo(b) >= 0 ? a : b);
}
System.out.println(latest); // 출력: {member=10:03, order=10:05, payment=10:02}
// 최근 3건만 유지
Deque<String> recent = new ArrayDeque<>();
for (String ev : events) {
recent.addLast(ev);
if (recent.size() > 3) recent.removeFirst();
}
System.out.println(recent); // 출력: [order 10:05, member 10:03, order 10:04]
// 서비스별 시각 목록 (첫 등장 순서 유지)
Map<String, List<String>> byService = new LinkedHashMap<>();
for (String ev : events) {
String[] p = ev.split(" ");
byService.computeIfAbsent(p[0], k -> new ArrayList<>()).add(p[1]);
}
byService.forEach((k, v) -> System.out.println(k + " " + v.size() + "건 " + v));
// 출력:
// order 3건 [10:01, 10:05, 10:04]
// payment 1건 [10:02]
// member 1건 [10:03]
}
}List.of/Map.of는 수정과 null을 거부하고, HashMap은 null 키를 허용하지만 TreeMap은 비교할 수 없어 거부한다. 빈 결과는 null 대신 빈 컬렉션을 돌려주면 호출자가 검사 없이 순회할 수 있다. subList는 복사본이 아니라 뷰라서 뷰를 지우면 원본이 지워지고, 원본의 구조가 바뀌면 뷰는 못 쓰게 된다.
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> fixed = List.of("a", "b");
try {
fixed.add("c");
} catch (UnsupportedOperationException e) {
System.out.println("List.of는 추가 불가"); // 출력: List.of는 추가 불가
}
try {
Map.of("k", null);
} catch (NullPointerException e) {
System.out.println("Map.of는 null 값 불가"); // 출력: Map.of는 null 값 불가
}
Map<String, Integer> hm = new HashMap<>();
hm.put(null, 1);
System.out.println(hm.get(null)); // 출력: 1 (HashMap은 null 키 허용)
try {
new TreeMap<String, Integer>().put(null, 1);
} catch (NullPointerException e) {
System.out.println("TreeMap은 null 키 불가"); // 출력: TreeMap은 null 키 불가
}
List<String> empty = Collections.emptyList(); // null 대신 빈 컬렉션
for (String s : empty) System.out.println("실행 안 됨");
System.out.println(empty.isEmpty() + " " + empty.size()); // 출력: true 0
List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> head = nums.subList(0, 2);
head.clear(); // 뷰를 비우면 원본에서 삭제
System.out.println(nums); // 출력: [3, 4, 5]
nums.add(6); // 원본 구조 변경
try {
head.size();
} catch (ConcurrentModificationException e) {
System.out.println("원본 변경 후 subList 사용 불가"); // 출력: 원본 변경 후 subList 사용 불가
}
try {
Collections.max(new ArrayList<Integer>());
} catch (NoSuchElementException e) {
System.out.println("빈 컬렉션의 max 없음"); // 출력: 빈 컬렉션의 max 없음
}
}
}