공공부하자개발 · 영어 학습 노트
자바
중급객체지향과 코어 라이브러리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정리‹ 이전다음 ›

4. 응용 변형 예제

같은 문제를 List·Set·Map으로 각각 풀어 보고, 루프와 스트림의 통계를 나란히 놓고, 상위 N개 조회를 범용 메서드로 빼고, 로그 이벤트에 Map·Deque를 적용한 뒤, 불변 컬렉션과 뷰의 엣지 케이스를 확인한다.

변형 1: 중복 체크 — List, Set, Map으로 각각

이메일 목록의 중복을 세 가지 자료구조로 처리한다. List.contains는 작은 데이터에서 코드가 가장 단순하지만 O(n²)이고, Set.add의 반환값을 쓰면 한 번의 순회로 "처음 본 것"과 "중복"을 나눌 수 있으며, 횟수까지 필요하면 Map.merge다. 무엇을 알아야 하는지(존재 여부·중복 목록·횟수)에 따라 고른다.

java
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> emails = List.of("a@x.com", "b@x.com", "a@x.com", "c@x.com", "b@x.com");

        // 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);                    // 출력: [a@x.com, b@x.com, c@x.com]
        System.out.println(seen + " 중복=" + dups);     // 출력: [a@x.com, b@x.com, c@x.com] 중복=[a@x.com, b@x.com]
        System.out.println(count);                      // 출력: {a@x.com=2, b@x.com=2, c@x.com=1}
    }
}

변형 2: 루프 vs 스트림 — 같은 통계

예제 2의 주문 통계를 루프와 스트림으로 나란히 쓴다. 루프는 변수 세 개를 직접 관리하고, 스트림은 summaryStatistics()가 건수·합계·최대·평균을 한 번에 준다. 조건이 하나이고 집계가 여러 개면 스트림이 짧고, 집계 중간에 복잡한 분기가 끼면 루프가 읽기 쉽다.

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)
        );

        // 루프
        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}
    }
}

변형 3: 상위 N개 조회를 범용 메서드로

"빈도 상위 N개"는 단어·상품·오류코드 어디에나 나온다. Map<String, Integer>와 n을 받아 값 내림차순, 같으면 키 오름차순으로 정렬한 엔트리 목록을 돌려주는 메서드로 빼면 재사용된다. n이 크기보다 크거나 맵이 비어 있어도 Math.min과 subList 덕분에 예외 없이 동작한다.

java
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]   (동점은 키 순)
    }
}

변형 4: 로그 이벤트에 Map과 Deque 적용하기

서비스별 마지막 이벤트 시각은 merge에 "큰 쪽을 남기는" 함수를 넘겨 한 줄로 구하고, 최근 3건만 유지하는 창은 ArrayDeque로 만든다. 서비스별 목록은 computeIfAbsent로 첫 등장 순서를 지킨다. 세 가지 모두 실무 모니터링 코드의 단골 형태다.

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

변형 5: 엣지 케이스 — 불변 컬렉션, null 키, 빈 컬렉션, subList 뷰

List.of/Map.of는 수정과 null을 거부하고, HashMap은 null 키를 허용하지만 TreeMap은 비교할 수 없어 거부한다. 빈 결과는 null 대신 빈 컬렉션을 돌려주면 호출자가 검사 없이 순회할 수 있다. subList는 복사본이 아니라 뷰라서 뷰를 지우면 원본이 지워지고, 원본의 구조가 바뀌면 뷰는 못 쓰게 된다.

java
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 없음
        }
    }
}
응용 변형 예제
  • 변형 1: 중복 체크 — List, Set, Map으로 각각
  • 변형 2: 루프 vs 스트림 — 같은 통계
  • 변형 3: 상위 N개 조회를 범용 메서드로
  • 변형 4: 로그 이벤트에 Map과 Deque 적용하기
  • 변형 5: 엣지 케이스 — 불변 컬렉션, null 키, 빈 컬렉션, subList 뷰
이전 섹션3 코드 예제4 / 7다음 섹션5 자주 하는 실수 (Tip)