자바 집합 시리즈 의 링크 드 HashMap 소스 분석

이 글 에서 우 리 는 링크 드 하 쉬 맵 의 소스 코드 를 분석 하기 시작 했다.링크 드 하 쉬 맵 은 하 쉬 맵 을 계승 했다.즉,링크 드 하 쉬 맵 은 하 쉬 맵 을 바탕 으로 확 장 된 것 이기 때문에 링크 드 하 쉬 맵 소스 코드 를 보기 전에 독자 들 은 하 쉬 맵 의 소스 코드 를 먼저 알 아야 한다.제 가 지난 글 에서 소개 한 을 볼 수 있 습 니 다.HashMap 의 실현 원 리 를 깊이 이해 하고 고 개 를 돌려 LinkedHashMap,HashSet 과 LinkedHashSet 의 소스 코드 를 보면 매우 간단 하 다.그 렇 기 때문에 독자 들 은 하 쉬 맵 의 소스 코드 를 연구 하 는 데 인내심 을 가 져 라.이것 은 1+3 의 좋 은 장 사 를 사 는 것 이다.앞에서 HashMap 소스 코드 를 분석 할 때 저 는 문 제 를 중심 으로 소스 코드 를 분석 하면 머리 가 없 는 파리 처럼 함부로 분석 하지 않 고 독자 들 도 문 제 를 더욱 깊이 이해 할 수 있 습 니 다.이 편 에서 나 는 여전히 이런 방식 으로 링크 드 하 쉬 맵 을 분석 하기 로 결정 했다.
1.링크 드 하 쉬 맵 내 부 는 어떤 구 조 를 사 용 했 습 니까?

이 를 통 해 알 수 있 듯 이 링크 드 HashMap 은 HashMap 에서 계승 되 었 기 때문에 링크 드 HashMap 내부 도 하나의 해시 표 이다.다만 링크 드 HashMap 은 하나의 Entry 를 다시 썼 고 원래 HashMap 의 Entry 에 두 개의 구성원 변 수 를 추 가 했 는데 그것 이 바로 전 계 결점 인용 과 후 계 결점 인용 이다.이렇게 하면 모든 노드 를 연결 시 켜 하나의 양 방향 링크 를 구성 하고 요 소 를 얻 을 때 이 양 방향 링크 를 직접 옮 겨 다 니 면 됩 니 다.링크 드 하 쉬 맵 이 이 룬 엔 트 리 가 어떤 모습 인지 살 펴 보 자.

private static class Entry<K,V> extends HashMap.Entry<K,V> {
  //                  
  Entry<K,V> before;
  //                  
  Entry<K,V> after;

  Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
    super(hash, key, value, next);
  }

  //           
  private void remove() {
    before.after = after;
    after.before = before;
  }

  //                       
  private void addBefore(Entry<K,V> existingEntry) {
    //                   
    after = existingEntry;
    //                         
    before = existingEntry.before;
    //                         
    before.after = this;
    //                   
    after.before = this;
  }

  //        ,          
  void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    //          
    if (lm.accessOrder) {
      lm.modCount++;
      //            
      remove();
      //           
      addBefore(lm.header);
    }
  }
  
  void recordRemoval(HashMap<K,V> m) {
    remove();
  }
}
2.링크 드 HashMap 은 어떻게 삽입 순서에 따라 정렬 합 니까?

//  put          
void addEntry(int hash, K key, V value, int bucketIndex) {
  //     addEntry  
  super.addEntry(hash, key, value, bucketIndex);
  //       LRU     ,         ,         
  Entry<K,V> eldest = header.after;
  if (removeEldestEntry(eldest)) {
    removeEntryForKey(eldest.key);
  }
}

//   addEntry         
void createEntry(int hash, K key, V value, int bucketIndex) {
  //   HashMap Entry
  HashMap.Entry<K,V> old = table[bucketIndex];
  //   LinkedHashMap   Entry
  Entry<K,V> e = new Entry<>(hash, key, value, old);
  table[bucketIndex] = e;
  //               
  e.addBefore(header);
  size++;
}

LinkedHashMap 은 부모 클래스 HashMap 의 addEntry 와 createEntry 방법 을 다시 썼 습 니 다.키 값 을 삽입 하려 면 먼저 부모 클래스 HashMap 의 put 방법 을 사용 합 니 다.put 방법 에 서 는 해시 표 에 대응 하 는 key 가 존재 하 는 지 확인 합 니 다.존재 하면 value 를 직접 바 꾸 면 됩 니 다.존재 하지 않 으 면 addEntry 방법 으로 Entry 를 새로 만 듭 니 다.이때 링크 드 하 쉬 맵 의 addEntry 방법 으로 호출 되 었 습 니 다.우 리 는 위의 코드 를 보 았 습 니 다.이 addEntry 방법 은 부모 클래스 의 addEntry 방법 을 되 돌 리 는 것 외 에 removeEldestEntry 를 호출 하여 가장 오래된 요 소 를 제거 합 니 다.이 동작 은 주로 LRU 알고리즘 을 실현 하기 위해 서 입 니 다.아래 에 설명 하 겠 습 니 다.링크 드 HashMap 을 보고 createEntry 방법 을 다시 썼 습 니 다.Entry 를 새로 만 들 려 면 이 방법 을 사용 합 니 다.createEntry 방법 은 Entry 를 해시 표 에 넣 을 때마다 addBefore 방법 으로 현재 노드 를 양 방향 링크 의 끝 에 삽입 합 니 다.이렇게 하면 양 방향 링크 는 매번 삽 입 된 노드 의 순 서 를 기록 하고 요 소 를 가 져 올 때 이 양 방향 링크 만 옮 겨 다 니 면 됩 니 다.다음 그림 은 매번 addBefore 를 호출 하 는 동작 을 보 여 줍 니 다.양 방향 링크 이기 때문에 현재 노드 를 끝 점 에 삽입 하기 전에 현재 노드 를 양 방향 링크 의 끝 부분 에 삽입 하 는 것 입 니 다.

3.어떻게 LinkedHashMap 을 이용 하여 LRU 캐 시 를 실현 합 니까?
우 리 는 캐 시 실현 이 컴퓨터 의 메모리 에 의존 한 다 는 것 을 알 고 있 습 니 다.메모리 자원 은 상당히 제한 되 어 있 습 니 다.무제 한 저장 요 소 는 불가능 합 니 다.그래서 우 리 는 용량 이 부족 할 때 일부 요 소 를 적당 하 게 삭제 해 야 합 니 다.그러면 어떤 요 소 를 삭제 하 는 것 이 좋 습 니까?LRU 알고리즘 은 하나의 데이터 가 최근 에 방 문 된 적 이 있다 면 앞으로 방 문 될 확률 도 높다 는 사상 이다.그래서 자주 접근 하지 않 는 데 이 터 를 삭제 할 수 있 습 니 다.다음은 링크 드 하 쉬 맵 내부 에서 LRU 체 제 를 어떻게 실현 하 는 지 살 펴 보 자.

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
  //       
  private transient Entry<K,V> header;
  //         
  private final boolean accessOrder;
  ...
  public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
  }
  //  key  value 
  public V get(Object key) {
    //        key   Entry
    Entry<K,V> e = (Entry<K,V>)getEntry(key);
    if (e == null) {
      return null;
    }
    //            ,                    
    e.recordAccess(this);
    return e.value;
  }
  private static class Entry<K,V> extends HashMap.Entry<K,V> {
    ...
    //                       
    private void addBefore(Entry<K,V> existingEntry) {
      //                   
      after = existingEntry;
      //                         
      before = existingEntry.before;
      //                         
      before.after = this;
      //                   
      after.before = this;
    }
    //        ,          
    void recordAccess(HashMap<K,V> m) {
      LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
      //          
      if (lm.accessOrder) {
        lm.modCount++;
        //            
        remove();
        //           
        addBefore(lm.header);
      }
    }
    ...
  }
  //  put          
  void addEntry(int hash, K key, V value, int bucketIndex) {
    //     addEntry  
    super.addEntry(hash, key, value, bucketIndex);
    //       LRU     ,         ,         
    Entry<K,V> eldest = header.after;
    if (removeEldestEntry(eldest)) {
      removeEntryForKey(eldest.key);
    }
  }
  //         ,             
  protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
  }
}
보다 직관 적 으로 위 에 붙 인 코드 에서 저 는 무관 한 코드 를 생략 했 습 니 다.우 리 는 링크 드 HashMap 에 구성원 변수 accessOrder 가 있 는 것 을 볼 수 있 습 니 다.이 구성원 변 수 는 방문 순서에 따라 정렬 해 야 하 는 지 여 부 를 기록 하고 구조 기 를 제공 하여 accessOrder 의 값 을 스스로 지정 할 수 있 습 니 다.get 방법 을 호출 할 때마다 요소 식 을 가 져 올 때마다 e.recordAccess(this)를 호출 합 니 다.이 방법 은 현재 노드 를 양 방향 링크 의 끝으로 이동 합 니 다.이제 알 겠 습 니 다.이 단계 의 목적 은 가장 자주 사용 하 는 요소 와 자주 사용 하지 않 는 요 소 를 구별 하고 자주 사용 하 는 요 소 는 꼬리 에 두 고 자주 사용 하지 않 는 요 소 는 머리 에 두 는 것 이다.우 리 는 위의 코드 로 돌아 가서 addEntry 방법 을 호출 할 때마다 가장 오래된 요 소 를 삭제 해 야 하 는 지 여 부 를 판단 합 니 다.판단 의 논 리 는 removeEldestEntry 로 이 루어 졌 으 며,이 방법 은 하위 클래스 로 덮어 쓰 고 그 안의 논 리 를 다시 쓰 도록 설계 되 었 다.최근 에 방문 한 결점 이 양 방향 체인 의 꼬리 부분 으로 옮 겨 졌 기 때문에 여 기 는 양 방향 체인 의 머리 에서 가장 오래된 결점 을 꺼 내 삭제 합 니 다.다음 예 는 간단 한 LRU 캐 시 를 구현 합 니 다.

public class LRUMap<K, V> extends LinkedHashMap<K, V> {
  
  private int capacity;
  
  LRUMap(int capacity) {
    //       ,           
    super(capacity, 1f, true);
    this.capacity = capacity;
  }
  
  @Override
  public boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    //              
    return this.size() >= capacity;
  }
  
  public static void main(String[] args) {
    LRUMap<Integer, String> map = new LRUMap<Integer, String>(4);
    map.put(1, "a");
    map.put(2, "b");
    map.put(3, "c");
    System.out.println("    :" + map);
    String s = map.get(2);
    System.out.println("    :" + map);
    map.put(4, "d");
    System.out.println("    :" + map);
  }
  
}

결 과 는 다음 과 같다.

주의:상기 모든 분석 은 JDK 1.7 을 바탕 으로 버 전 간 에 차이 가 있 을 수 있 으 므 로 독자 들 은 주의해 야 합 니 다.
이상 이 바로 본 고의 모든 내용 입 니 다.여러분 의 학습 에 도움 이 되 고 저 희 를 많이 응원 해 주 셨 으 면 좋 겠 습 니 다.

좋은 웹페이지 즐겨찾기