[Java] LinkedHashMap
정의
java.util.LinkedHashMap<K,V> 는 HashMap 을 상속해 삽입 순서 (또는 접근 순서) 를 유지 하는 Map. 모든 HashMap 의 평균 O(1) 성능을 유지하면서 추가로 순회 순서가 일관된다.
핵심: 이중 연결 리스트 (doubly-linked list) 가 모든 entry 를 삽입 순서대로 엮어 둔다.
시각화
내부 구조
HashMap.Node 를 확장한 Entry.
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // ← 추가된 두 포인터
}
// LinkedHashMap 의 추가 필드
transient LinkedHashMap.Entry<K,V> head; // 가장 오래된 entry
transient LinkedHashMap.Entry<K,V> tail; // 가장 최근 entry
final boolean accessOrder; // false = 삽입 순서, true = 접근 순서
각 entry 가 해시 버킷의 chain 과는 별도로 linked list 로도 엮인다. 두 구조를 동시에 관리.
두 가지 순회 순서
삽입 순서 (기본)
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("a", 1); map.put("b", 2); map.put("c", 3);
map.put("a", 99); // 기존 key 갱신, 순서 유지
map.forEach((k, v) -> System.out.println(k + "=" + v));
// a=99, b=2, c=3
접근 순서 (LRU 캐시용)
Map<String, Integer> lru = new LinkedHashMap<>(16, 0.75f, true);
// ^^^^ accessOrder
lru.put("a", 1); lru.put("b", 2); lru.put("c", 3);
lru.get("a"); // "a" 가 가장 최근 접근
lru.forEach(...); // b, c, a 순으로 출력
accessOrder = true 면 get, put 마다 entry 가 linked list 의 끝으로 이동.
LRU 캐시 구현
removeEldestEntry 를 override 하면 손쉽게 LRU 캐시를 만들 수 있다.
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; // 초과 시 가장 오래된 항목 제거
}
}
// 사용
Map<String, String> cache = new LRUCache<>(3);
cache.put("a", "1"); cache.put("b", "2"); cache.put("c", "3");
cache.get("a");
cache.put("d", "4");
// cache = {b=2, c=3, a=1, d=4}? 아니, "b" 가 제거되어 {c=3, a=1, d=4}
IMPORTANT
단일 스레드 LRU 라면 LinkedHashMap 이 매우 깔끔하다. 동시성이 필요하면 [Spring] ConcurrentLruCache 또는 Caffeine 같은 외부 라이브러리.
복잡도
HashMap 과 동일.
| 작업 | 평균 | 최악 |
|---|---|---|
get, put, remove | O(1) | O(log n) |
| 순회 | O(n) (linked list 따라가므로 빠름) | 같음 |
순회는 HashMap 보다 빠르다. HashMap 은 빈 버킷도 모두 거쳐야 하지만, LinkedHashMap 은 linked list 만 따라간다.
메모리
각 entry 가 before/after 두 포인터를 추가로 가지므로 HashMap 대비 per-entry 메모리가 ~16-32 바이트 더 든다. 큰 map 에서는 무시 못 한다.
함정
1. accessOrder 가 동시성 문제 증폭
accessOrder = true 일 때 get() 도 구조 변경 (linked list 재정렬). 이는 modCount 를 증가시키지는 않지만, 동시 환경에서 iterator 와 충돌 할 수 있다. 동기화 필요.
2. removeEldestEntry 가 매 put 후 호출
대용량 데이터를 부어 넣는 경우 매 put 마다 호출되는 비용이 누적된다.
3. thread-safe 가 아니다
HashMap 처럼 단일 스레드 전용.
참고
이 글의 용어 (7개)
- [Java] Collectionjava
- 정의 는 그룹으로 묶인 객체들을 표현하는 최상위 인터페이스. JCF (Java Collections Framework) 의 입구이자, / / / 모두 이를 확장한다. 자체는 직접…
- [Java] ConcurrentHashMapjava
- 정의 는 고동시성 환경에서 사용 가능한 구현. JSR-166 (Java 5) 도입, Java 8 에서 내부 구조가 크게 재작성됐다. 과 같은 인터페이스를 제공하면서 thread-…
- [Java] HashMapjava
- 정의 는 해시 테이블 로 구현된 . 평균 O(1) 의 put/get/remove 를 보장하고 단일 스레드 환경의 사실상 표준. JDK 1.2 도입, Java 8 에서 충돌 시 l…
- [Java] Iterablejava
- 정의 는 루프로 순회 가능한 모든 타입의 최상위 인터페이스. 단 하나의 추상 메서드, 를 정의한다. 인터페이스가 을 extends 하므로 , , , 등 모든 컬렉션이 자동으로 대…
- [Java] Objectjava
- 정의 는 Java 의 모든 클래스의 최상위 부모 (root) 클래스. 가 명시되지 않은 클래스는 컴파일러가 자동으로 를 붙인다. 인터페이스는 클래스가 아니라 를 직접 상속하지는 …
- [Java] TreeMapjava
- 정의 는 Red-Black Tree 기반의 정렬된 . (확장 인터페이스) 을 구현해 범위 쿼리, floor/ceiling, sub-map 같은 정렬 기반 연산을 제공한다. key…
- 집합과 맵 (Set and Map)algorithm
- 정의 집합 (Set) 은 고유한 원소들의 모임을 나타내는 추상 자료구조로, 멤버십 검사, 삽입, 삭제를 지원한다. 맵 (Map, Dictionary) 은 키-값 쌍을 저장하며, …
💬 댓글