🎯1주차 Unit 6.5 — TreeMap, LinkedHashMap

Psj·2026년 5월 11일

F-lab

목록 보기
45/240

🎯 Unit 6.5 — TreeMap, LinkedHashMap ★★

F-lab Java 1주차 / Phase 6 / Unit 6.5 본격 학습 자료 — Phase 6 마무리!
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.

선수 지식: Phase 4-5, Unit 6.1-6.4 (특히 HashMap, ArrayList/LinkedList)
다음 학습: Phase 7 — I/O & NIO (예정)

이 Unit의 의미: 순서가 있는 Map 의 두 가지 답.
TreeMap = 정렬 순서 (범위 쿼리). LinkedHashMap = 삽입/접근 순서 (LRU 캐시).
면접 ("TreeMap 언제 쓰나?") + 실무 (LRU 캐시 6줄 구현) 두 마리.


🌍 1. 세상 속 비유

TreeMap = "사전" / LinkedHashMap = "방명록"

세 가지 Map 을 일상 비유로 풀어보겠습니다.

HashMap = 우체국 사물함 (복습)

  • 빠른 접근 (O(1))
  • 순서 없음 — 사물함 번호 순서대로 들어있지 않음
  • 사물함 위치만 알면 됨

TreeMap = 영어 사전

[A]
  Apple — 사과
  Apricot — 살구
[B]
  Banana — 바나나
  Berry — 베리
[C]
  Cat — 고양이
  Cherry — 체리
...

특징:

  • 단어들이 항상 알파벳 순
  • "C 로 시작하는 단어" → 책 중간으로 직접 → 범위 쿼리
  • "Banana 다음 단어" → 즉시 → 순서 활용
  • 새 단어 추가 → 정렬된 위치에 끼워넣기 (느림)

핵심: 정렬된 상태 유지 + 범위 검색.


LinkedHashMap = 방명록

2024-12-01 14:30  Alice
2024-12-02 09:15  Bob
2024-12-02 11:45  Charlie
2024-12-03 16:20  David

특징:

  • 방문 순서대로 기록
  • "세 번째로 방문한 사람?" → 순서대로 세서 답
  • 새 방문자 추가 → 마지막에 한 줄 추가
  • 빠른 검색 + 순서 유지

핵심: 삽입 순서 또는 접근 순서 유지.


핵심 한 문장

"HashMap 은 순서 없음 (빠름), TreeMap 은 정렬 순서 (범위), LinkedHashMap 은 삽입/접근 순서 (LRU)."

비유 정리:

비유자바 클래스순서특징
사물함HashMap없음가장 빠름
영어 사전TreeMap정렬범위 쿼리
방명록LinkedHashMap삽입/접근LRU 캐시

또 다른 비유 — "도서관 책 정리"

HashMap = 임의 배치 책꽂이:

  • ISBN 으로 즉시 찾을 수 있음
  • 그러나 "5,000원 이하 책" 같은 범위 검색 X

TreeMap = 분류 정리된 책꽂이:

  • 가격순/제목순으로 정렬
  • "5,000원~10,000원 책" 즉시 찾기 가능
  • "마지막에 산 책" 도 알 수 있음

LinkedHashMap = 도착 순서 책꽂이:

  • 구매 순서대로 배치
  • "최근 구매" 추적 가능
  • LRU (자주 안 본 책 처분) 정책 구현

🔥 2. 탄생 배경

HashMap 만으로 부족한 경우들

HashMap 은 빠르지만 순서 정보가 없음.

문제 1 — "범위 검색이 필요해"

Map<Integer, Customer> customers = new HashMap<>();
customers.put(20, alice);
customers.put(35, bob);
customers.put(40, charlie);
customers.put(55, david);

// 30~50대 고객만 가져오기?
// HashMap 으로는 — 전체 순회 + 필터링 → O(n)

→ 순서가 있다면 O(log n + k) 로 가능.


문제 2 — "삽입 순서가 중요해"

// LRU 캐시 시나리오
// 가장 오래 안 쓴 항목 제거
Map<String, Cache> cache = new HashMap<>();
cache.put("A", a);  // 1번째
cache.put("B", b);  // 2번째
cache.put("C", c);  // 3번째

// "가장 오래된 항목" → 어떻게?
// HashMap 은 순서 없음 → 추적 불가

→ 삽입 순서가 유지된다면 즉시 처리 가능.


문제 3 — "정렬된 출력이 필요해"

Map<String, Integer> wordCount = new HashMap<>();
wordCount.put("apple", 5);
wordCount.put("banana", 3);
wordCount.put("cherry", 8);

// 알파벳 순서로 출력?
// HashMap 으로는 — keys → 정렬 → 매번 O(n log n)

→ 항상 정렬되어 있다면 O(n) 으로 끝.


자바의 두 가지 답

답 1: TreeMap — "정렬 순서"

// Java 1.2 (1998)
public class TreeMap<K, V> implements NavigableMap<K, V> {
    private final Comparator<? super K> comparator;
    private transient Entry<K,V> root;  // Red-Black Tree
    private transient int size;
}

핵심:

  • Red-Black Tree 기반
  • 항상 정렬된 상태 유지
  • 범위 쿼리 메서드 (subMap, headMap, tailMap, floorKey, ceilingKey)

답 2: LinkedHashMap — "삽입/접근 순서"

// Java 1.4 (2002)
public class LinkedHashMap<K, V> extends HashMap<K, V> {
    transient LinkedHashMap.Entry<K,V> head;  // 첫 요소
    transient LinkedHashMap.Entry<K,V> tail;  // 마지막 요소
    final boolean accessOrder;  // false = 삽입 순서, true = 접근 순서
    
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;  // 양방향 연결
    }
}

핵심:

  • HashMap 상속 + 양방향 연결 리스트
  • 두 가지 모드:
    • accessOrder=false (기본): 삽입 순서 유지
    • accessOrder=true: 접근 순서 유지 (LRU 구현 가능)

시간 복잡도 비교

연산HashMapTreeMapLinkedHashMap
getO(1) ⭐O(log n)O(1) ⭐
putO(1) ⭐O(log n)O(1) ⭐
removeO(1) ⭐O(log n)O(1) ⭐
containsKeyO(1) ⭐O(log n)O(1) ⭐
firstKey/lastKeyXO(log n) ⭐O(1) (첫/마지막)
subMap (범위)XO(log n + k) ⭐X
순회 (순서)보장 X정렬 순서 ⭐삽입/접근 순서 ⭐

→ TreeMap 은 정렬 메서드, LinkedHashMap 은 순서 + 같은 성능.


메모리 비교

항목HashMapTreeMapLinkedHashMap
Node 크기32 바이트40 바이트 (parent 추가)48 바이트 (before/after 추가)
메모리/요소1배1.25배1.5배

→ LinkedHashMap 가 메모리 가장 큼, 그러나 순서 유지의 가치 가 있을 때만.


핵심 통찰

"같은 Map 인터페이스, 세 가지 다른 trade-off."

속도만 필요하면 HashMap, 정렬과 범위가 필요하면 TreeMap, 삽입 순서나 LRU 가 필요하면 LinkedHashMap. 상황에 맞는 선택 이 시니어 자바 개발자의 역량.


💣 3. 없으면 생기는 문제

시나리오 1: ILIC 의 가격대별 화물 검색

@Service
public class CargoSearchService {
    
    // ❌ HashMap 으로 가격대 검색
    private Map<Integer, List<Cargo>> cargosByFare = new HashMap<>();
    
    public List<Cargo> findByFareRange(int minFare, int maxFare) {
        List<Cargo> result = new ArrayList<>();
        for (Map.Entry<Integer, List<Cargo>> entry : cargosByFare.entrySet()) {
            // ⚠️ 모든 가격대 순회 → O(n)
            if (entry.getKey() >= minFare && entry.getKey() <= maxFare) {
                result.addAll(entry.getValue());
            }
        }
        return result;
    }
}

문제:

  • 1만 개 가격대 → 1만 번 비교
  • 매번 O(n)

해결: TreeMap

private NavigableMap<Integer, List<Cargo>> cargosByFare = new TreeMap<>();

public List<Cargo> findByFareRange(int minFare, int maxFare) {
    // O(log n + k) — 정렬된 트리에서 범위 검색
    return cargosByFare.subMap(minFare, true, maxFare, true)
        .values().stream()
        .flatMap(List::stream)
        .collect(Collectors.toList());
}

시나리오 2: LRU 캐시 — 직접 구현의 함정

// ❌ HashMap + List 로 LRU 구현 시도
public class BadLRUCache<K, V> {
    private Map<K, V> map = new HashMap<>();
    private List<K> accessOrder = new ArrayList<>();
    private int capacity;
    
    public V get(K key) {
        V value = map.get(key);
        if (value != null) {
            accessOrder.remove(key);  // ⚠️ O(n)
            accessOrder.add(key);
        }
        return value;
    }
    
    public void put(K key, V value) {
        if (map.size() >= capacity && !map.containsKey(key)) {
            K oldest = accessOrder.remove(0);  // ⚠️ O(n)
            map.remove(oldest);
        }
        map.put(key, value);
        accessOrder.remove(key);  // O(n)
        accessOrder.add(key);
    }
}

문제:

  • 매 get/put 마다 O(n)
  • 1만 개 캐시 → 매 작업 1만 번 비교

해결: LinkedHashMap

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;
    
    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);  // accessOrder = true!
        this.capacity = capacity;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}
// 6 줄로 LRU 캐시 완성! 모든 작업 O(1)

시나리오 3: ILIC 의 시간순 활동 로그

// ❌ HashMap — 순서 보존 X
Map<LocalDateTime, Activity> activities = new HashMap<>();
activities.put(time1, a1);
activities.put(time2, a2);
activities.put(time3, a3);

// 시간순 출력?
for (Activity a : activities.values()) {
    System.out.println(a);  // 순서 보장 X!
}

해결 1: TreeMap (시간순 정렬)

NavigableMap<LocalDateTime, Activity> activities = new TreeMap<>();
// 자동으로 시간순
for (Activity a : activities.values()) { ... }  // 시간순 출력

해결 2: LinkedHashMap (삽입 순서)

Map<LocalDateTime, Activity> activities = new LinkedHashMap<>();
// 삽입 순서 유지 (시간순 삽입했다면 시간순)

시나리오 4: 면접 단골 질문

Q: "TreeMap 은 언제 쓰나요?"
A: "음... HashMap 보다 정렬되는 거에요"  ❌ 표면적

시니어 답변:
1. 범위 쿼리 가 필요할 때 (subMap, headMap, tailMap)
2. floorKey/ceilingKey 같은 근접 키 검색
3. 정렬된 출력 이 자주 필요할 때
4. NavigableMap 의 강력한 메서드들
5. trade-off: O(log n) 비용


시나리오 5: ILIC 의 시간대별 카운트

// ❌ 매번 정렬
Map<Integer, Integer> hourlyCount = new HashMap<>();
// ... 데이터 추가 ...

// 시간순 출력 시
List<Integer> sortedHours = new ArrayList<>(hourlyCount.keySet());
Collections.sort(sortedHours);  // 매번 O(n log n)
for (int hour : sortedHours) {
    System.out.println(hour + ": " + hourlyCount.get(hour));
}

해결: TreeMap

Map<Integer, Integer> hourlyCount = new TreeMap<>();
// 자동 정렬
for (Map.Entry<Integer, Integer> e : hourlyCount.entrySet()) {
    System.out.println(e.getKey() + ": " + e.getValue());
}

시나리오 6: API 응답 순서 보존

// JSON 응답에서 키 순서가 중요한 경우
@RestController
public class ReportController {
    
    @GetMapping("/report")
    public Map<String, Object> getReport() {
        Map<String, Object> result = new HashMap<>();
        result.put("totalCount", 1000);
        result.put("avgFare", 50000);
        result.put("maxWeight", 5000);
        
        return result;
        // JSON 순서가 무작위!
        // {"avgFare":50000, "maxWeight":5000, "totalCount":1000}
    }
    
    // ✓ LinkedHashMap — 삽입 순서 유지
    @GetMapping("/report-ordered")
    public Map<String, Object> getReportOrdered() {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("totalCount", 1000);
        result.put("avgFare", 50000);
        result.put("maxWeight", 5000);
        
        return result;
        // JSON: {"totalCount":1000, "avgFare":50000, "maxWeight":5000}
    }
}

영향 정리

시나리오잘못된 선택올바른 선택
가격대 범위 검색HashMap O(n)TreeMap O(log n + k)
LRU 캐시직접 구현 O(n)LinkedHashMap O(1)
시간순 출력HashMap + 정렬 매번TreeMap 또는 LinkedHashMap
면접 답변표면적시니어
API 응답 순서HashMap (무작위)LinkedHashMap

→ 적절한 Map 선택은 시니어의 핵심 역량.


✅ 4. 해결책 — 두 Map 의 정확한 이해

TreeMap — 정렬된 Map

기본 사용

TreeMap<String, Integer> map = new TreeMap<>();
map.put("Charlie", 3);
map.put("Alice", 1);
map.put("Bob", 2);

// 순회 → 정렬된 순서!
for (Map.Entry<String, Integer> e : map.entrySet()) {
    System.out.println(e.getKey() + ": " + e.getValue());
}
// 출력:
// Alice: 1
// Bob: 2
// Charlie: 3

NavigableMap<Integer, String> map = new TreeMap<>();
map.put(10, "A");
map.put(20, "B");
map.put(30, "C");
map.put(40, "D");
map.put(50, "E");

// === 범위 검색 ===
map.subMap(20, 40);              // {20=B, 30=C} (40 미포함)
map.subMap(20, true, 40, true);  // {20=B, 30=C, 40=D}
map.headMap(30);                 // {10=A, 20=B}
map.tailMap(30);                 // {30=C, 40=D, 50=E}

// === 근접 키 검색 ===
map.floorKey(25);    // 25 이하 중 가장 큰 키 → 20
map.ceilingKey(25);  // 25 이상 중 가장 작은 키 → 30
map.lowerKey(20);    // 20 미만 중 가장 큰 키 → 10
map.higherKey(20);   // 20 초과 중 가장 작은 키 → 30

// === 첫 / 마지막 ===
map.firstKey();      // 10
map.lastKey();       // 50
map.firstEntry();    // (10, A)
map.lastEntry();     // (50, E)
map.pollFirstEntry();  // (10, A) 반환 + 제거
map.pollLastEntry();   // (50, E) 반환 + 제거

// === 역순 ===
map.descendingMap();      // 역순 Map
map.descendingKeySet();   // 역순 키

정렬 기준 설정 — Comparator

// 1. 자연 순서 (Comparable)
TreeMap<String, Integer> ascMap = new TreeMap<>();  // A, B, C...

// 2. 역순
TreeMap<String, Integer> descMap = new TreeMap<>(Comparator.reverseOrder());

// 3. 커스텀 Comparator
TreeMap<Customer, Order> byAge = new TreeMap<>(
    Comparator.comparingInt(Customer::getAge)
);

// 4. 다중 정렬
TreeMap<Customer, Order> byGradeThenName = new TreeMap<>(
    Comparator.comparing(Customer::getGrade)
        .thenComparing(Customer::getName)
);

LinkedHashMap — 순서 보존 Map

기본 사용 — 삽입 순서 모드 (기본)

Map<String, Integer> map = new LinkedHashMap<>();
map.put("Charlie", 3);
map.put("Alice", 1);
map.put("Bob", 2);

// 순회 → 삽입 순서!
for (Map.Entry<String, Integer> e : map.entrySet()) {
    System.out.println(e.getKey());
}
// 출력:
// Charlie  (먼저 삽입)
// Alice
// Bob

접근 순서 모드 — LRU 의 핵심

// accessOrder = true
LinkedHashMap<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);

// 현재 순서: A → B → C (삽입 순서)
map.get("A");  // A 접근

// 순서 변경: B → C → A
// (A 가 가장 최근 접근 → 맨 뒤로 이동)

핵심: get() 호출 시 그 항목이 맨 뒤로 이동.


LRU 캐시 구현 — 6 줄 ⭐⭐⭐

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;
    
    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);  // accessOrder = true
        this.capacity = capacity;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;  // capacity 초과 시 가장 오래된 제거
    }
}

// 사용
LRUCache<String, Customer> cache = new LRUCache<>(100);
cache.put("alice", aliceCustomer);
cache.get("alice");  // alice 가 가장 최근
// 100 개 초과 시 자동으로 가장 오래 안 쓴 항목 제거

핵심 메커니즘:

  • accessOrder = true → get 시 맨 뒤로 이동
  • removeEldestEntry 오버라이드 → 자동 제거 정책
  • 모든 작업 O(1)

비교 표 ⭐⭐ (Map 종합)

항목HashMapTreeMapLinkedHashMap
순서없음정렬삽입/접근
시간 복잡도O(1)O(log n)O(1)
null key1개❌1개
메모리/요소1배1.25배1.5배
범위 쿼리X✓ ⭐X
firstKey/lastKeyXO(log n)O(1)
LRU 구현어려움어려움6줄 ⭐
사용 시기기본정렬/범위순서/LRU

선택 가이드 ⭐

Map 이 필요하다
        ↓
순서가 필요한가?
├── 무관 → HashMap (또는 ConcurrentHashMap)
├── 정렬 순서 / 범위 쿼리 → TreeMap ⭐
└── 삽입 / 접근 순서
    ├── 삽입 순서만 → LinkedHashMap (기본)
    └── LRU 캐시 → LinkedHashMap(accessOrder=true) ⭐

흔한 패턴

패턴 1: 자주 사용되는 정렬된 결과

// 단어 빈도 → 알파벳순 출력
TreeMap<String, Integer> wordCount = new TreeMap<>();
words.forEach(w -> wordCount.merge(w, 1, Integer::sum));

wordCount.forEach((word, count) -> 
    System.out.println(word + ": " + count)
);
// 자동으로 알파벳순!

패턴 2: 가격대별 검색

// 가격대별 화물
NavigableMap<Integer, List<Cargo>> byFare = new TreeMap<>();

// 5,000원~10,000원 범위
Map<Integer, List<Cargo>> mid = byFare.subMap(5000, 10001);

패턴 3: LRU 캐시

Map<String, Data> cache = new LinkedHashMap<>(100, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, Data> eldest) {
        return size() > 100;
    }
};

패턴 4: API 응답 순서 보존

Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "success");
response.put("data", ...);
response.put("timestamp", ...);
// JSON 출력 순서 보장

🏗️ 5. 내부 동작 원리

TreeMap — Red-Black Tree 기반

구조

public class TreeMap<K, V> implements NavigableMap<K, V> {
    private final Comparator<? super K> comparator;
    private transient Entry<K,V> root;
    private transient int size;
    
    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;     // 왼쪽 자식
        Entry<K,V> right;    // 오른쪽 자식
        Entry<K,V> parent;   // 부모
        boolean color = BLACK;  // Red 또는 Black
    }
}

Red-Black Tree 규칙:
1. 노드는 Red 또는 Black
2. Root 는 Black
3. Red 노드의 자식은 Black
4. 모든 leaf 까지의 Black 노드 수는 같음
5. → 균형 보장: 최악 깊이 ≤ 2 × log(n+1)


put() 의 동작

public V put(K key, V value) {
    Entry<K,V> t = root;
    
    // 1. 루트가 없으면 root 로
    if (t == null) {
        compare(key, key);  // null/type check
        root = new Entry<>(key, value, null);
        size = 1;
        return null;
    }
    
    // 2. 비교하며 위치 찾기
    Comparator<? super K> cpr = comparator;
    int cmp;
    Entry<K,V> parent;
    
    do {
        parent = t;
        cmp = cpr.compare(key, t.key);
        if (cmp < 0) t = t.left;
        else if (cmp > 0) t = t.right;
        else return t.setValue(value);  // 같은 키 → 값 교체
    } while (t != null);
    
    // 3. 새 노드 추가
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0) parent.left = e;
    else parent.right = e;
    
    // 4. Red-Black 균형 조정
    fixAfterInsertion(e);
    size++;
    return null;
}

O(log n): 트리 깊이만큼 탐색.


subMap() 의 동작

// map.subMap(20, 40)
// → 20 ≤ key < 40 인 모든 entry

알고리즘:
1. 시작 키 (20) 찾기: O(log n)
2. 끝 키 (40) 찾기: O(log n)
3. 사이의 모든 entry 순회: O(k)
4. 총: O(log n + k)

중요: subMap 은 view — 원본 변경 시 반영됨.


Red-Black Tree 의 강점

작업시간
삽입O(log n)
검색O(log n)
삭제O(log n)
최소/최대O(log n)
범위 검색O(log n + k)

자바 8+ HashMap 의 Treeification 도 같은 Red-Black Tree.


LinkedHashMap — HashMap + 양방향 연결 리스트

구조

public class LinkedHashMap<K,V> extends HashMap<K,V> {
    transient Entry<K,V> head;  // 첫 번째 (가장 오래된)
    transient Entry<K,V> tail;  // 마지막 (가장 최근)
    final boolean accessOrder;  // false=삽입순서, true=접근순서
    
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;  // 양방향 연결
    }
}

핵심:

  • HashMap 의 모든 기능 유지 (O(1) get/put)
  • 추가로 양방향 연결 리스트로 순서 추적

메모리 그림

HashMap 부분 (table 배열):
table[0]: ...
table[3]: Node(B) → Node(D)
table[5]: Node(A)
table[7]: Node(C)
...

LinkedHashMap 의 양방향 리스트:
head → A ↔ B ↔ C ↔ D ← tail
       ↑               ↑
       가장 오래된     가장 최근

각 Node 는 next (HashMap) 와 before/after (LinkedHashMap) 모두 보유

put() 의 동작 (삽입 순서)

// LinkedHashMap.newNode()
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<>(hash, key, value, e);
    linkNodeLast(p);  // ★ 양방향 리스트의 끝에 추가
    return p;
}

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

효과:

  • HashMap 의 put 동작 그대로
    • 양방향 리스트 끝에 추가
  • 순회 시 head → after 따라가면 삽입 순서

get() 의 동작 (접근 순서 모드)

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);  // ★ 맨 뒤로 이동
    return e.value;
}

void afterNodeAccess(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        // e 를 현재 위치에서 빼고 tail 로 이동
        LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e;
        LinkedHashMap.Entry<K,V> b = p.before, a = p.after;
        p.after = null;
        if (b == null) head = a;
        else b.after = a;
        if (a != null) a.before = b;
        else last = b;
        if (last == null) head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
    }
}

핵심:

  • 접근된 항목을 양방향 리스트의 끝 으로 이동
  • O(1) — 양방향 리스트의 장점
  • head 는 항상 가장 오래 안 쓴 항목

removeEldestEntry — LRU 의 마법

// LinkedHashMap 의 메서드
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;  // 기본: 제거 안 함
}

// HashMap.afterNodeInsertion 에서 호출
void afterNodeInsertion(boolean evict) {
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

LRU 캐시의 동작:
1. put 호출 → 새 entry 추가
2. afterNodeInsertion 호출
3. removeEldestEntry 오버라이드 가 true 반환 시
4. head (가장 오래된 항목) 제거


accessOrder 의 효과

모드 비교:

// 1. 삽입 순서 (accessOrder = false, 기본)
Map<String, Integer> insertion = new LinkedHashMap<>();
insertion.put("A", 1);
insertion.put("B", 2);
insertion.put("C", 3);
insertion.get("A");  // 순서 변경 X

// 순회: A, B, C (삽입 순서)

// 2. 접근 순서 (accessOrder = true)
Map<String, Integer> access = new LinkedHashMap<>(16, 0.75f, true);
access.put("A", 1);
access.put("B", 2);
access.put("C", 3);
access.get("A");  // A 가 맨 뒤로!

// 순회: B, C, A (접근 순서)

LinkedHashMap vs LinkedHashSet

// LinkedHashSet 도 같은 원리
Set<String> set = new LinkedHashSet<>();
set.add("C");
set.add("A");
set.add("B");

for (String s : set) {
    System.out.println(s);  // C, A, B (삽입 순서)
}

→ HashSet + 양방향 리스트.


💻 6. 실전 코드 예시

예시 1: ILIC 의 가격대별 화물 검색 (TreeMap)

@Service
public class CargoFareService {
    
    private final NavigableMap<Integer, List<Cargo>> cargosByFare = new TreeMap<>();
    
    public void registerCargo(Cargo cargo) {
        cargosByFare
            .computeIfAbsent(cargo.getFare(), k -> new ArrayList<>())
            .add(cargo);
    }
    
    public List<Cargo> findByFareRange(int minFare, int maxFare) {
        // O(log n + k) — 정렬 트리에서 범위 검색
        return cargosByFare.subMap(minFare, true, maxFare, true)
            .values().stream()
            .flatMap(List::stream)
            .collect(Collectors.toList());
    }
    
    public Cargo findCheapestAbove(int minFare) {
        // 가장 저렴한 화물 중 minFare 이상
        Map.Entry<Integer, List<Cargo>> entry = cargosByFare.ceilingEntry(minFare);
        return entry == null ? null : entry.getValue().get(0);
    }
    
    public Cargo findMostExpensive() {
        Map.Entry<Integer, List<Cargo>> entry = cargosByFare.lastEntry();
        return entry == null ? null : entry.getValue().get(0);
    }
}

예시 2: LRU 캐시 — LinkedHashMap

@Service
public class FareCalculationCache {
    
    private final LRUCache<String, BigDecimal> cache = new LRUCache<>(1000);
    
    public BigDecimal getFare(String routeKey) {
        BigDecimal cached = cache.get(routeKey);
        if (cached != null) return cached;
        
        // 비싼 계산
        BigDecimal computed = fareCalculator.calculate(routeKey);
        cache.put(routeKey, computed);
        return computed;
    }
}

// LRU 캐시 — 6 줄!
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;
    
    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

효과:

  • 모든 get/put O(1)
  • 1000 개 초과 시 자동 제거 (LRU)
  • 메모리 사용 일정

예시 3: ILIC 의 일자별 매출 (TreeMap)

@Service
public class DailySalesService {
    
    // 일자 -> 매출
    private final NavigableMap<LocalDate, Long> dailySales = new TreeMap<>();
    
    public void recordSale(LocalDate date, long amount) {
        dailySales.merge(date, amount, Long::sum);
    }
    
    public Map<LocalDate, Long> getRecentDays(int days) {
        LocalDate today = LocalDate.now();
        LocalDate from = today.minusDays(days);
        // O(log n + k)
        return dailySales.subMap(from, true, today, true);
    }
    
    public Long getMaxSale() {
        return dailySales.values().stream()
            .max(Long::compareTo)
            .orElse(0L);
    }
    
    public LocalDate getMaxSaleDate() {
        return dailySales.entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .map(Map.Entry::getKey)
            .orElse(null);
    }
    
    public void printChronological() {
        // 자동으로 날짜순!
        dailySales.forEach((date, amount) ->
            System.out.println(date + ": " + amount + "원"));
    }
}

예시 4: 상위 N 개 조회 (TreeMap.descendingMap)

public class TopNAnalysis {
    
    public List<Map.Entry<String, Integer>> getTopCustomers(
            Map<String, Integer> ordersByCustomer, int n) {
        
        // 주문 수 → 고객 (정렬 트리)
        NavigableMap<Integer, List<String>> byCount = new TreeMap<>();
        
        ordersByCustomer.forEach((customer, count) ->
            byCount.computeIfAbsent(count, k -> new ArrayList<>()).add(customer));
        
        // 상위 N 개
        List<Map.Entry<String, Integer>> top = new ArrayList<>();
        for (Map.Entry<Integer, List<String>> e : byCount.descendingMap().entrySet()) {
            for (String customer : e.getValue()) {
                if (top.size() >= n) return top;
                top.add(Map.entry(customer, e.getKey()));
            }
        }
        return top;
    }
}

예시 5: API 응답 순서 보존 (LinkedHashMap)

@RestController
public class CargoController {
    
    @GetMapping("/api/cargo/{id}")
    public Map<String, Object> getCargoDetails(@PathVariable Long id) {
        Cargo cargo = cargoService.findById(id);
        
        // 순서가 중요한 응답
        Map<String, Object> response = new LinkedHashMap<>();
        response.put("id", cargo.getId());
        response.put("name", cargo.getName());
        response.put("weight", cargo.getWeight());
        response.put("fare", cargo.getFare());
        response.put("originPort", cargo.getOriginPort());
        response.put("destinationPort", cargo.getDestinationPort());
        response.put("createdAt", cargo.getCreatedAt());
        
        return response;
        // JSON 출력 순서 보장!
    }
}

예시 6: 다중 정렬 기준 (Comparator)

public class CustomerSortExample {
    
    public NavigableMap<Customer, Order> sortByGradeAndName(
            Map<Customer, Order> orders) {
        
        // 등급 (높은 등급 우선) → 이름 (가나다순)
        Comparator<Customer> sorter = Comparator
            .comparing(Customer::getGrade, Comparator.reverseOrder())
            .thenComparing(Customer::getName);
        
        TreeMap<Customer, Order> sorted = new TreeMap<>(sorter);
        sorted.putAll(orders);
        return sorted;
    }
    
    public NavigableMap<Customer, Order> sortByFare(
            Map<Customer, Order> orders) {
        
        // 운임 (높은 순)
        Comparator<Customer> byFare = (a, b) -> {
            long fareA = orders.get(a).getFare();
            long fareB = orders.get(b).getFare();
            return Long.compare(fareB, fareA);  // 내림차순
        };
        
        TreeMap<Customer, Order> sorted = new TreeMap<>(byFare);
        sorted.putAll(orders);
        return sorted;
    }
}

예시 7: LinkedHashMap 으로 캐시 + 통계

public class CacheWithStats<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;
    private long hits = 0;
    private long misses = 0;
    
    public CacheWithStats(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }
    
    @Override
    public V get(Object key) {
        V value = super.get(key);
        if (value != null) hits++;
        else misses++;
        return value;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
    
    public double getHitRate() {
        long total = hits + misses;
        return total == 0 ? 0 : (double) hits / total;
    }
    
    public void printStats() {
        System.out.printf("Hits: %d, Misses: %d, HitRate: %.2f%%%n",
            hits, misses, getHitRate() * 100);
    }
}

예시 8: TreeMap 으로 가격 호가창

public class OrderBook {
    
    // 매수 주문 (가격 높은 순으로 정렬)
    private final NavigableMap<BigDecimal, Long> bids = new TreeMap<>(Comparator.reverseOrder());
    
    // 매도 주문 (가격 낮은 순으로 정렬)
    private final NavigableMap<BigDecimal, Long> asks = new TreeMap<>();
    
    public void addBid(BigDecimal price, long quantity) {
        bids.merge(price, quantity, Long::sum);
    }
    
    public void addAsk(BigDecimal price, long quantity) {
        asks.merge(price, quantity, Long::sum);
    }
    
    public BigDecimal getBestBid() {
        return bids.isEmpty() ? null : bids.firstKey();  // 가장 높은 매수 가격
    }
    
    public BigDecimal getBestAsk() {
        return asks.isEmpty() ? null : asks.firstKey();  // 가장 낮은 매도 가격
    }
    
    public BigDecimal getSpread() {
        BigDecimal bestBid = getBestBid();
        BigDecimal bestAsk = getBestAsk();
        if (bestBid == null || bestAsk == null) return null;
        return bestAsk.subtract(bestBid);
    }
    
    public Map<BigDecimal, Long> getTop5Bids() {
        return bids.entrySet().stream()
            .limit(5)
            .collect(Collectors.toMap(
                Map.Entry::getKey, Map.Entry::getValue,
                (a, b) -> a, LinkedHashMap::new));
        // LinkedHashMap → 순서 보존
    }
}

⚠️ 7. 주의사항 & 흔한 실수

실수 1: TreeMap 에 null 키

TreeMap<String, Integer> map = new TreeMap<>();
map.put(null, 1);  // ❌ NullPointerException

이유: 정렬을 위해 키 비교 → null 비교 불가.

해결: HashMap 또는 LinkedHashMap 사용 (null 키 1개 허용).


실수 2: Comparable 미구현 키

class Customer {
    String customerId;
}

TreeMap<Customer, String> map = new TreeMap<>();
map.put(new Customer(), "value");  // ❌ ClassCastException

이유: Customer 가 Comparable 미구현 → 정렬 불가.

해결 1: Comparable 구현

class Customer implements Comparable<Customer> {
    @Override
    public int compareTo(Customer other) {
        return this.customerId.compareTo(other.customerId);
    }
}

해결 2: Comparator 전달

TreeMap<Customer, String> map = new TreeMap<>(
    Comparator.comparing(Customer::getCustomerId)
);

실수 3: subMap 의 view 함정

NavigableMap<Integer, String> map = new TreeMap<>();
map.put(10, "A"); map.put(20, "B"); map.put(30, "C");

Map<Integer, String> sub = map.subMap(15, 25);  // {20=B}

map.put(25, "X");  // 원본 수정
System.out.println(sub);  // {20=B, 25=X} — view 가 갱신됨!

// 수정 시 원본도 변경
sub.put(22, "Y");
System.out.println(map);  // {10=A, 20=B, 22=Y, 25=X, 30=C}

원칙: subMap 은 view. 독립 사본 필요 시 복사.

Map<Integer, String> copy = new TreeMap<>(map.subMap(15, 25));

실수 4: LinkedHashMap 의 accessOrder 망각

// LRU 의도
Map<String, Integer> cache = new LinkedHashMap<>(100);  // ❌ 기본 = 삽입 순서
cache.put("A", 1);
cache.get("A");  // 순서 변경 안 됨!

해결: 생성자 명시

new LinkedHashMap<>(100, 0.75f, true);  // accessOrder = true

실수 5: removeEldestEntry 미오버라이드

// LRU 캐시 시도
Map<String, Data> cache = new LinkedHashMap<>(100, 0.75f, true);
// ❌ 자동 제거 안 됨!
// removeEldestEntry 가 항상 false 반환 (기본값)

해결: 익명 클래스 또는 상속

Map<String, Data> cache = new LinkedHashMap<>(100, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, Data> eldest) {
        return size() > 100;
    }
};

실수 6: TreeMap 의 O(log n) 비용 무시

// ❌ 빠른 lookup 이 필요한데 TreeMap 사용
Map<String, Customer> lookup = new TreeMap<>();
for (int i = 0; i < 1_000_000; i++) {
    lookup.get(keys[i]);  // O(log n) × 100만 ≈ 2천만
}

해결: 정렬/범위 필요 없으면 HashMap.

Map<String, Customer> lookup = new HashMap<>();  // O(1) × 100만 = 100만

실수 7: LinkedHashMap 의 메모리 망각

// 100만 데이터 + 순서 불필요
Map<String, Customer> map = new LinkedHashMap<>(1_000_000);  // ❌ 1.5배 메모리

해결: 순서 필요 없으면 HashMap.

Map<String, Customer> map = new HashMap<>(1_000_000);  // 메모리 효율

실수 8: 정렬 일관성 위반

class Cargo {
    private double weight;
    
    // ❌ Comparable 위반
    public int compareTo(Cargo other) {
        if (this.weight < other.weight) return -1;
        return 1;  // ⚠️ 같을 때도 1 반환 → 일관성 X
    }
}

원칙: compareTo == 0 이면 equals == true 여야 함 (정렬 안정성).

해결:

public int compareTo(Cargo other) {
    return Double.compare(this.weight, other.weight);
}

🔗 8. 연관 개념 맵

Phase 6 (데이터 다루기) 마무리

[Unit 6.1: String + Constant Pool] ✓
        ↓
[Unit 6.2: StringBuilder vs StringBuffer] ✓
        ↓
[Unit 6.3: ArrayList vs LinkedList] ✓
        ↓
[Unit 6.4: HashMap 내부 구조] ✓ ★★★
        ↓
[Unit 6.5: TreeMap, LinkedHashMap] ← 지금 (Phase 6 완료!)
        ↓
[Phase 7: I/O & NIO] (예정)

자료구조 종합

List ─── ArrayList ⭐ (Unit 6.3)
     ├── LinkedList
     └── Vector

Set  ─── HashSet
     ├── LinkedHashSet (LinkedHashMap 의 Set 버전)
     └── TreeSet (TreeMap 의 Set 버전)

Map  ─── HashMap ⭐ (Unit 6.4)
     ├── LinkedHashMap ⭐ (Unit 6.5)
     ├── TreeMap ⭐ (Unit 6.5)
     ├── ConcurrentHashMap
     └── Hashtable (legacy)

Queue ── ArrayDeque ⭐
     ├── LinkedList (Deque 도 구현)
     └── PriorityQueue (TreeMap 과 유사 — 정렬 유지)

Red-Black Tree 의 자바 활용

사용처설명
TreeMapMap 의 기본 구현
TreeSetSet 의 정렬 구현
HashMap (Java 8+)충돌 8개 초과 시 트리화
LinkedHashMap (Java 8+)같은 트리화
ConcurrentHashMap (Java 8+)같은 트리화

→ Red-Black Tree 는 자바 컬렉션의 핵심.


다른 언어 비교

언어정렬 Map순서 Map
JavaTreeMapLinkedHashMap
Python(sortedcontainers)OrderedDict
JavaScript(없음)Map (삽입 순서)
C++std::map(없음)
C#SortedDictionary(없음)
RustBTreeMapIndexMap (외부)

→ 자바는 두 가지 모두 표준 제공 — 강점.


면접 단골 질문 매핑

질문이 Unit 에서의 답
TreeMap 언제?정렬/범위 쿼리
LinkedHashMap 언제?순서 보존/LRU
LRU 캐시 구현?LinkedHashMap + accessOrder + removeEldestEntry
TreeMap 내부?Red-Black Tree
HashMap vs TreeMap?O(1) vs O(log n)

📝 9. 핵심 요약 — 3줄 정리

1️⃣ TreeMap = Red-Black Tree, LinkedHashMap = HashMap + 양방향 리스트.

TreeMap 은 정렬된 상태 유지 — 모든 작업 O(log n), 그러나 범위 쿼리/근접 키 검색 이 강점 (subMap, floorKey, ceilingKey). LinkedHashMap 은 HashMap 의 모든 기능 (O(1)) + 양방향 연결 리스트 로 삽입 순서 또는 접근 순서 유지. 메모리는 HashMap 의 1.5배.

2️⃣ LinkedHashMap 의 accessOrder=true + removeEldestEntry = LRU 캐시 6줄.

accessOrder=true 면 get 시 그 항목이 맨 뒤로 이동 → head 는 항상 가장 오래 안 쓴 항목. removeEldestEntry 오버라이드 로 capacity 초과 시 head 자동 제거. 모든 작업 O(1). 자바 표준 라이브러리의 숨겨진 보석.

3️⃣ 선택 원칙 — 정렬/범위 → TreeMap, 순서/LRU → LinkedHashMap, 그 외 → HashMap.

TreeMap 의 강점: subMap, headMap, tailMap, floorKey, ceilingKey 같은 NavigableMap 메서드. LinkedHashMap 의 강점: 삽입 순서 보존 (API 응답 JSON 순서), LRU 캐시. HashMap 은 순서 무관 시 기본. ILIC 의 가격대별 화물 = TreeMap, 운임 캐시 = LinkedHashMap, 일반 매핑 = HashMap. Phase 6 완료 — 자바 컬렉션 정복!


🎓 학습 자기 점검

기본 이해

  • TreeMap 의 Red-Black Tree 기반 동작을 안다
  • LinkedHashMap 의 양방향 리스트 구조를 안다
  • accessOrder 의 두 모드를 안다 (삽입 순서 vs 접근 순서)
  • NavigableMap 의 강력한 메서드들을 안다

실전 적용

  • LinkedHashMap 으로 LRU 캐시를 구현할 수 있다
  • TreeMap 의 범위 쿼리를 활용할 수 있다
  • 상황에 맞는 Map 을 선택할 수 있다
  • Comparator 로 커스텀 정렬을 구현할 수 있다

면접 대비 (5분 답변)

  • "TreeMap 언제 쓰나?" 답변 가능
  • "LRU 캐시 구현?" 답변 가능 (LinkedHashMap)
  • "Map 구현체 비교?" 답변 가능
  • "Red-Black Tree?" 기본 답변 가능

자기 점검 질문 답변

Q1: LRU 캐시를 LinkedHashMap 으로 어떻게 구현하는가?

한 줄 답: accessOrder=true 로 생성 후 removeEldestEntry 오버라이드. 6줄 구현, 모든 작업 O(1).

상세 설명:

1. 기본 구조

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;
    
    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);  // ★ accessOrder = true
        this.capacity = capacity;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;  // ★ 초과 시 가장 오래된 제거
    }
}

6줄로 LRU 완성!


2. 동작 메커니즘

상태 1: 캐시 capacity = 3, 비어있음

head → null ← tail

상태 2: put("A", 1)

head → A ← tail

상태 3: put("B", 2), put("C", 3)

head → A ↔ B ↔ C ← tail

상태 4: get("A")

A 가 접근됨 → 맨 뒤로 이동
head → B ↔ C ↔ A ← tail

상태 5: put("D", 4) — capacity 초과!

1. afterNodeInsertion(true) 호출
2. removeEldestEntry(head=B) 호출 → size()=4 > 3 → true
3. head (B) 제거

head → C ↔ A ↔ D ← tail

→ 가장 오래 안 쓴 항목 (B) 자동 제거.


3. 시간 복잡도

모든 작업: O(1)

  • get: HashMap 의 O(1) + 양방향 리스트 이동 O(1)
  • put: HashMap 의 O(1) + 리스트 끝 추가 O(1)
  • remove (LRU): head 제거 O(1)

4. 실전 활용

@Service
public class CustomerCache {
    private final Map<String, Customer> cache = 
        Collections.synchronizedMap(new LRUCache<>(10000));
    // 10,000 개까지 캐시, 그 이상은 자동 제거
    
    public Customer getCustomer(String id) {
        return cache.computeIfAbsent(id, this::loadFromDb);
    }
    
    private Customer loadFromDb(String id) {
        return customerRepository.findById(id).orElse(null);
    }
}

5. 멀티스레드 고려

LinkedHashMap 은 Thread-Safe X.

해결 1: synchronizedMap

Map<K, V> cache = Collections.synchronizedMap(new LRUCache<>(100));

해결 2: Caffeine 라이브러리 (권장)

Cache<K, V> cache = Caffeine.newBuilder()
    .maximumSize(100)
    .build();

해결 3: ConcurrentHashMap + 별도 LRU 로직 (복잡)


6. 결론

"LinkedHashMap 의 accessOrder=true + removeEldestEntry 오버라이드 = 완벽한 LRU.
6줄로 모든 작업 O(1), 메모리 일정.
자바 표준 라이브러리의 숨겨진 보석 — 알면 시니어, 모르면 직접 구현해 O(n).
멀티스레드 환경에서는 synchronizedMap 또는 Caffeine 라이브러리 활용."


Q2: TreeMap 의 subMap 과 HashMap 으로 같은 작업 구현 시 시간 차이는?

한 줄 답: TreeMap 의 subMap = O(log n + k), HashMap 으로 같은 작업 = O(n). 데이터 크면 수천 배 차이.

상세 설명:

1. 시나리오 — 가격대 검색

// 100만 개 화물, 5,000~10,000원 범위 검색
NavigableMap<Integer, List<Cargo>> tree = new TreeMap<>();
Map<Integer, List<Cargo>> hash = new HashMap<>();
// 데이터 채우기 ...

// TreeMap
List<Cargo> result1 = tree.subMap(5000, 10001).values().stream()
    .flatMap(List::stream).collect(Collectors.toList());

// HashMap
List<Cargo> result2 = hash.entrySet().stream()
    .filter(e -> e.getKey() >= 5000 && e.getKey() <= 10000)
    .flatMap(e -> e.getValue().stream())
    .collect(Collectors.toList());

2. 시간 분석

TreeMap.subMap:
1. 시작 키 (5000) 위치 찾기: O(log n) ≈ 20번 비교 (100만)
2. 끝 키 (10001) 위치 찾기: O(log n) ≈ 20번 비교
3. 사이의 entry 순회: O(k) (예: 100개)
4. 합계: O(log n + k) ≈ 140번 작업

HashMap 필터링:
1. 모든 entry 순회: O(n) = 100만 번
2. 각각 범위 검사: 100만 번 비교
3. 합계: O(n) = 100만 번

차이: 약 7,000배.


3. 실측

public class RangeQueryBenchmark {
    public static void main(String[] args) {
        int N = 1_000_000;
        NavigableMap<Integer, String> tree = new TreeMap<>();
        Map<Integer, String> hash = new HashMap<>();
        
        for (int i = 0; i < N; i++) {
            tree.put(i, "value-" + i);
            hash.put(i, "value-" + i);
        }
        
        // TreeMap subMap
        long start = System.nanoTime();
        Map<Integer, String> sub = tree.subMap(500000, 500100);
        int size1 = sub.size();
        long treeTime = System.nanoTime() - start;
        
        // HashMap 필터링
        start = System.nanoTime();
        long count = hash.entrySet().stream()
            .filter(e -> e.getKey() >= 500000 && e.getKey() < 500100)
            .count();
        long hashTime = System.nanoTime() - start;
        
        System.out.println("TreeMap subMap: " + treeTime + " ns");
        // 약 10,000 ns = 10 μs
        
        System.out.println("HashMap filter: " + hashTime + " ns");
        // 약 30,000,000 ns = 30 ms (3,000배)
    }
}

4. 더 큰 범위에서

데이터 수TreeMap subMapHashMap filter차이
1,0001 μs50 μs50배
10,0002 μs500 μs250배
100,0005 μs5 ms1,000배
1,000,00010 μs30 ms3,000배
10,000,00015 μs300 ms20,000배

→ 데이터 클수록 차이 폭증.


5. 결론

"범위 쿼리는 자료구조 선택의 결정적 차이.
TreeMap 은 정렬된 구조 덕분에 O(log n + k) — 데이터가 100만 개여도 거의 즉시.
HashMap 은 정렬 X → O(n) 필터링 강제 — 매번 전체 순회.
ILIC 의 가격대별 화물, 시간대별 조회, 등급별 검색 등 범위 쿼리가 빈번하면 TreeMap 정답.
시니어 차별화: subMap 의 view 특성, NavigableMap 의 강력한 메서드 활용."


다음 학습으로

  • Phase 6 완료 🎉 (Unit 6.1-6.5 모두 완료)
  • Phase 7 — I/O & NIO 학습 준비 완료
  • 컬렉션 정점에서 자바 시니어 답변 가능
profile
Software Developer

0개의 댓글