HashMap과 LinkedHashMap은 둘 다 Java에서 제공하는 Map 인터페이스의 구현체이지만, 동작 방식과 특징에 몇 가지 중요한 차이점이 있습니다:
🔹 1. 저장 순서 유지 여부
| 특징 | HashMap |
LinkedHashMap |
|---|---|---|
| 순서 유지 | ❌ 저장 순서 보장 안 됨 | ✅ 입력 순서(또는 접근 순서) 유지 |
HashMap은 내부적으로 순서를 신경 쓰지 않기 때문에, 데이터를 삽입한 순서와 꺼낼 때 순서가 다를 수 있음.LinkedHashMap은 이중 연결 리스트를 사용해 데이터를 저장한 순서를 기억함.
🔹 2. 성능
| 성능 | HashMap |
LinkedHashMap |
|---|---|---|
| 일반적으로 더 빠름 | ✅ | ❌ (조금 느림) |
LinkedHashMap은 연결 리스트를 유지해야 하므로 약간의 오버헤드가 있음.
🔹 3. null 허용 여부
| 요소 | HashMap |
LinkedHashMap |
|---|---|---|
null 키 허용 |
1개 가능 | 1개 가능 |
null 값 허용 |
여러 개 가능 | 여러 개 가능 |
둘 다
null키 하나와null값 여러 개 허용함.
🔹 4. 접근 순서 유지 모드 (LinkedHashMap만 해당)
LinkedHashMap은 생성자 옵션을 통해 “접근 순서"로 순서를 유지할 수도 있음:
LinkedHashMap<K, V> map = new LinkedHashMap<>(16, 0.75f, true);
- 위처럼
accessOrder=true로 설정하면,get()등으로 접근한 항목이 가장 뒤로 이동됨 → LRU 캐시 구현에 자주 사용.
🔹 5. 사용 예시
Map<String, String> hashMap = new HashMap<>();
hashMap.put("one", "1");
hashMap.put("two", "2");
hashMap.put("three", "3");
// 출력 순서: 랜덤
Map<String, String> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("one", "1");
linkedHashMap.put("two", "2");
linkedHashMap.put("three", "3");
// 출력 순서: one → two → three
✅ 정리 요약
| 항목 | HashMap |
LinkedHashMap |
|---|---|---|
| 순서 보장 | ❌ 없음 | ✅ 있음 |
| 성능 | 빠름 | 약간 느림 |
| 메모리 사용 | 적음 | 더 많음 |
| 사용 용도 | 일반적인 Map | 순서가 중요한 경우, LRU 캐시 등 |
필요에 따라 선택하면 되는데, “순서가 중요하면 LinkedHashMap”, **“성능과 메모리 절약이 중요하면 HashMap”**을 쓰면 됩니다.
✅ LRU 캐시 예제 (LinkedHashMap 사용)
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
// 생성자: accessOrder = true 설정하면 접근 순서로 정렬됨
public LRUCache(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
// 가장 오래된 항목을 자동으로 제거할지 결정하는 메서드 오버라이딩
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
// 테스트용 main 메서드
public static void main(String[] args) {
LRUCache<Integer, String> cache = new LRUCache<>(3);
cache.put(1, "One");
cache.put(2, "Two");
cache.put(3, "Three");
System.out.println(cache); // {1=One, 2=Two, 3=Three}
cache.get(1); // 1을 가장 최근으로 갱신
cache.put(4, "Four"); // 2가 제거됨 (가장 오래 사용 안 됨)
System.out.println(cache); // {3=Three, 1=One, 4=Four}
}
}
💡 설명
-
super(capacity, 0.75f, true)
→true는 access-order 모드, 즉 가장 최근에 접근한 순서를 유지. -
removeEldestEntry()
→size()가 초과되면 자동으로 가장 오래된 항목 제거.
📌 출력 결과
{1=One, 2=Two, 3=Three}
{3=Three, 1=One, 4=Four}
2는 최근에 접근하지 않았기 때문에, 4가 추가되면서 제거됨. 딱 LRU 방식이죠.
이제 위에 만든 LRU 캐시를 스레드 세이프하게 만들고, 제너릭 타입을 고정한 버전도 보여드릴게요.
✅ 1. 스레드 세이프(Thread-safe) LRU 캐시
LinkedHashMap 자체는 스레드 안전하지 않으므로, synchronized 키워드 또는 Collections.synchronizedMap() 등을 활용해야 합니다.
🔸 방법 A: synchronized 키워드로 메서드 감싸기
import java.util.LinkedHashMap;
import java.util.Map;
public class ThreadSafeLRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public ThreadSafeLRUCache(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected synchronized boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
@Override
public synchronized V get(Object key) {
return super.get(key);
}
@Override
public synchronized V put(K key, V value) {
return super.put(key, value);
}
@Override
public synchronized String toString() {
return super.toString();
}
}
✅ 사용 예시
public static void main(String[] args) {
ThreadSafeLRUCache<Integer, String> cache = new ThreadSafeLRUCache<>(2);
cache.put(1, "A");
cache.put(2, "B");
cache.get(1); // A가 최근
cache.put(3, "C"); // B 제거
System.out.println(cache); // {1=A, 3=C}
}
✅ 2. 제너릭 타입 고정 버전 (예: Integer 키, String 값)
public class IntStringLRUCache extends ThreadSafeLRUCache<Integer, String> {
public IntStringLRUCache(int capacity) {
super(capacity);
}
public static void main(String[] args) {
IntStringLRUCache cache = new IntStringLRUCache(3);
cache.put(10, "Apple");
cache.put(20, "Banana");
cache.get(10);
cache.put(30, "Cherry");
cache.put(40, "Durian");
System.out.println(cache); // {10=Apple, 30=Cherry, 40=Durian}
}
}
🧠 정리
| 버전 | 특징 |
|---|---|
| 기본 버전 | 빠르고 간단하지만 싱글스레드 전용 |
ThreadSafeLRUCache |
멀티스레드 환경에서 안전 |
IntStringLRUCache |
제너릭 타입을 Integer, String으로 고정 |