공공부하자개발 · 영어 학습 노트
자바
중급객체지향과 코어 라이브러리0/9 완료
  • 01상속, 다형성, 오버라이딩
  • 02추상 클래스 vs 인터페이스
  • 03예외 처리
  • 04java.lang 심화
  • 05컬렉션 프레임워크 딥다이브
  • 06메서드 활용 패턴 (중급)
  • 07Object 메서드와 비교
  • 08java.time 실무 날짜 계산
  • 09HTTP 와 JSON 기초
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 중급 › 05 / 9

컬렉션 프레임워크 딥다이브

섹션 7진행 0 / 9
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

3. 코드 예제

예제 1: 중복 제거 — List.contains vs HashSet

같은 작업을 List.contains(O(n²))와 HashSet(O(n))으로 수행해 시간 차이를 확인한다.

java
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
    }
}

예제 2: 주문 통계 — 그룹핑 집계

주문 목록을 상태별로 그룹핑하고 합계·건수·최대 금액을 구한다. Map.merge, computeIfAbsent, 스트림 groupingBy를 모두 보여준다.

java
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]
    }
}

예제 3: LinkedHashMap으로 LRU 캐시

accessOrder = true와 removeEldestEntry만으로 LRU(Least Recently Used, 가장 오래 안 쓴 것부터 제거) 캐시를 만든다.

java
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는 정확히 이 용도로 만들어진 훅 메서드(템플릿 메서드 패턴)이다. 상속이 정당한 드문 사례다.

예제 4: ConcurrentModificationException과 해결

순회 중 삭제가 왜 실패하는지 재현하고, Iterator.remove와 removeIf로 고친다.

java
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]
    }
}

예제 5: Comparator 조합과 TreeMap 범위 검색

다중 기준 정렬과 TreeMap의 headMap/tailMap/floorKey를 활용한 등급 조회를 보여준다.

java
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}
    }
}

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다.

cd java-src\intermediate\05_collections
javac -encoding UTF-8 *.java && java Main
코드 예제
  • 예제 1: 중복 제거 — List.contains vs HashSet
  • 예제 2: 주문 통계 — 그룹핑 집계
  • 예제 3: LinkedHashMap으로 LRU 캐시
  • 예제 4: ConcurrentModificationException과 해결
  • 예제 5: Comparator 조합과 TreeMap 범위 검색
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제