같은 작업을 List.contains(O(n²))와 HashSet(O(n))으로 수행해 시간 차이를 확인한다.
import java.util.*;
public class Main {
public static void main(String[] args) {
int n = 30_000;
List<Integer> input = new ArrayList<>();
Random rnd = new Random(42);
for (int i = 0; i < n; i++) input.add(rnd.nextInt(n / 2)); // 중복 많음
long t0 = System.nanoTime();
List<Integer> viaList = new ArrayList<>();
for (Integer x : input) if (!viaList.contains(x)) viaList.add(x); // contains O(n)
long listMs = (System.nanoTime() - t0) / 1_000_000;
t0 = System.nanoTime();
Set<Integer> viaSet = new LinkedHashSet<>(input); // 삽입 순서 유지 + 중복 제거
long setMs = (System.nanoTime() - t0) / 1_000_000;
System.out.println("고유 개수: " + viaList.size() + " / " + viaSet.size());
System.out.println("List.contains: " + listMs + "ms");
System.out.println("LinkedHashSet: " + setMs + "ms");
// 출력 (환경에 따라 다름):
// 고유 개수: 12942 / 12942
// List.contains: 1020ms
// LinkedHashSet: 77ms
}
}주문 목록을 상태별로 그룹핑하고 합계·건수·최대 금액을 구한다. Map.merge, computeIfAbsent, 스트림 groupingBy를 모두 보여준다.
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)
);
// 1) Map.merge로 상태별 합계 (TreeMap이라 키 정렬)
Map<String, Long> sumByStatus = new TreeMap<>();
for (Order o : orders) sumByStatus.merge(o.status(), o.amount(), Long::sum);
System.out.println("합계: " + sumByStatus);
// 출력: 합계: {CANCELLED=50000, PAID=141000, SHIPPED=8000}
// 2) computeIfAbsent로 상태별 주문 목록
Map<String, List<String>> idsByStatus = new LinkedHashMap<>();
for (Order o : orders) idsByStatus.computeIfAbsent(o.status(), k -> new ArrayList<>()).add(o.id());
System.out.println("목록: " + idsByStatus);
// 출력: 목록: {PAID=[O-1, O-2, O-5], CANCELLED=[O-3], SHIPPED=[O-4]}
// 3) 스트림 groupingBy로 건수
Map<String, Long> countByStatus = orders.stream()
.collect(Collectors.groupingBy(Order::status, TreeMap::new, Collectors.counting()));
System.out.println("건수: " + countByStatus);
// 출력: 건수: {CANCELLED=1, PAID=3, SHIPPED=1}
// 4) 최대 금액 주문
Order max = Collections.max(orders, Comparator.comparingLong(Order::amount));
System.out.println("최대: " + max);
// 출력: 최대: Order[id=O-5, status=PAID, amount=99000]
}
}accessOrder = true와 removeEldestEntry만으로 LRU(Least Recently Used, 가장 오래 안 쓴 것부터 제거) 캐시를 만든다.
import java.util.LinkedHashMap;
import java.util.Map;
class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
LruCache(int capacity) {
super(16, 0.75f, true); // accessOrder = true: get/put 시 해당 엔트리를 맨 뒤로
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // 용량 초과 시 가장 오래된(맨 앞) 엔트리 제거
}
}
public class Main {
public static void main(String[] args) {
LruCache<String, String> cache = new LruCache<>(3);
cache.put("A", "상품A");
cache.put("B", "상품B");
cache.put("C", "상품C");
System.out.println(cache.keySet()); // 출력: [A, B, C]
cache.get("A"); // A를 최근 사용으로 → 맨 뒤로 이동
System.out.println(cache.keySet()); // 출력: [B, C, A]
cache.put("D", "상품D"); // 용량 초과 → 가장 오래된 B 제거
System.out.println(cache.keySet()); // 출력: [C, A, D]
System.out.println(cache.containsKey("B")); // 출력: false
}
}LinkedHashMap의 removeEldestEntry는 정확히 이 용도로 만들어진 훅 메서드(템플릿 메서드 패턴)이다. 상속이 정당한 드문 사례다.
순회 중 삭제가 왜 실패하는지 재현하고, Iterator.remove와 removeIf로 고친다.
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> files = new ArrayList<>(List.of("a.log", "tmp1.log", "b.log", "tmp2.log"));
try {
for (String f : files) {
if (f.startsWith("tmp")) files.remove(f); // 구조 변경 → 다음 next()에서 예외
}
} catch (ConcurrentModificationException e) {
System.out.println("CME 발생: 순회 중 remove");
// 출력: CME 발생: 순회 중 remove
}
files = new ArrayList<>(List.of("a.log", "tmp1.log", "b.log", "tmp2.log"));
Iterator<String> it = files.iterator();
while (it.hasNext()) {
if (it.next().startsWith("tmp")) it.remove(); // Iterator를 통한 삭제는 안전
}
System.out.println(files); // 출력: [a.log, b.log]
files = new ArrayList<>(List.of("a.log", "tmp1.log", "b.log", "tmp2.log"));
files.removeIf(f -> f.startsWith("tmp")); // 가장 간결
System.out.println(files); // 출력: [a.log, b.log]
}
}다중 기준 정렬과 TreeMap의 headMap/tailMap/floorKey를 활용한 등급 조회를 보여준다.
import java.util.*;
record Member(String name, String grade, int points) {}
public class Main {
public static void main(String[] args) {
List<Member> members = new ArrayList<>(List.of(
new Member("kim", "GOLD", 1200),
new Member("lee", "SILVER", 800),
new Member("park", "GOLD", 3000),
new Member("choi", "BRONZE", 100)
));
// 등급 오름차순 → 포인트 내림차순 → 이름
members.sort(Comparator.comparing(Member::grade)
.thenComparing(Member::points, Comparator.reverseOrder())
.thenComparing(Member::name));
members.forEach(m -> System.out.println(m.grade() + " " + m.name() + " " + m.points()));
// 출력:
// BRONZE choi 100
// GOLD park 3000
// GOLD kim 1200
// SILVER lee 800
// TreeMap: 포인트 하한 → 등급 (floorKey로 "이 포인트 이하 중 가장 큰 하한" 조회)
TreeMap<Integer, String> gradeByMinPoints = new TreeMap<>(Map.of(0, "BRONZE", 500, "SILVER", 1000, "GOLD"));
for (int p : new int[]{100, 999, 1000, 5000}) {
System.out.println(p + "점 → " + gradeByMinPoints.get(gradeByMinPoints.floorKey(p)));
}
// 출력:
// 100점 → BRONZE
// 999점 → SILVER
// 1000점 → GOLD
// 5000점 → GOLD
System.out.println(gradeByMinPoints.headMap(1000)); // 출력: {0=BRONZE, 500=SILVER}
System.out.println(gradeByMinPoints.tailMap(500, true)); // 출력: {500=SILVER, 1000=GOLD}
}
}