Hashmap \ \ LinkedHashMap 의 실현 원리 분석

인터넷 에 이미 많은 사람들 이 HashMap 소스 코드 분석 에 관 한 글 을 썼 지만 시간 이 지나 자 약간 모호 해 졌 다. 자신 이 진정 으로 다시 한 번 그것 을 밟 고 진정 으로 그것 을 분명하게 말 할 수 있어 야 자신 이 진정 으로 파악 할 수 있다 고 생각 했다.본문 을 읽 기 전에 다음 과 같은 몇 가지 문제 에 대해 훤 히 알 고 있다 면 이 글 은 생략 할 수 있다.1. HashMap 의 데이터 구 조 는 무엇 입 니까?hash 충돌 은 무엇 을 말 합 니까?2. HashMap 은 hash 충돌 을 어떻게 해결 하고 링크 법 은 어떻게 실현 합 니까?3. 왜 HashMap 의 용량 은 반드시 2 의 지수 멱 이 어야 합 니까?4. 링크 드 하 쉬 맵 의 실현 은 하 쉬 맵 과 어떤 차이 가 있 습 니까?링크 드 HashMap 은 어떻게 요소 의 질 서 를 실현 합 니까?5. 왜 equals 방법 을 다시 쓰 려 면 hashcode 방법 을 다시 써 야 합 니까?6. HashMap 의 put 방법 실현 과정 은?
1. 데이터 구조
hashmap 는 실제 적 으로 하나의 배열 + 링크 의 데이터 구조 이 고 hashmap 바 텀 은 하나의 배열 구조 이 며 배열 의 모든 항목 은 하나의 링크 구조 이다.링크 는 hash 충돌 을 해결 하기 위해 서 입 니 다.
     /**
     *   
     */
    static final HashMapEntry,?>[] EMPTY_TABLE = {};

    /**
     *        ,           ,        2    ,        
     */
    transient HashMapEntry[] table = (HashMapEntry[]) EMPTY_TABLE;
    /**
     *              ,  next       ,          map    ,  key、value
     */
    static class HashMapEntry implements Map.Entry {
        final K key;
        V value;
        HashMapEntry next;
        int hash;
    }

2. hashmap 초기 화
hashmap 의 구조 함수 에서 hashmap 용량 을 초기 화 합 니 다. 기본 적 인 초기 용량 은 16 입 니 다. 적재 용량 은 0.75 이기 때 문 입 니 다. 즉, 용량 이 12 에 이 르 면 hashmap 를 확대 하고 한 부 를 확대 하 며 32 로 변 합 니 다. 우 리 는 그 구조 함 수 를 호출 하여 용량 크기 를 스스로 설정 할 수 있 습 니 다.
//initialCapacity    ,  16、loadFactor    ,  0.75
public HashMap(int initialCapacity, float loadFactor) {

 }

3. hasmap 액세스 구현
3.1 put 조작
put 작업 이 이 루어 지 는 방식 은 key 에 따라 hash 값 을 계산 하고 배열 에 대응 하 는 위 치 를 찾 아 이 배열 에 대응 하 는 위치 에 값 이 있 는 지 판단 하고 값 이 있 으 면 같은 key 값 이 있 는 지 판단 하 며 새 값 을 오래된 값 으로 바 꾸 고 오래된 값 을 되 돌려 저장 하 는 것 입 니 다.이 배열 은 대응 하 는 위치 에 값 이 없 거나 값 이 있 지만 같은 key 값 이 없 으 면 Entry 요 소 를 새로 만 들 고 해당 하 는 배열 위치 에 놓 습 니 다.소스 코드 먼저 보기:
public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            //       ,        table[0]  
            return putForNullKey(value);    
        //  1:  hash     key hash 
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //  2:  hash       hashmap     
        int i = indexFor(hash, table.length);
        //  3:  hashmap            ,      for  ,    for                 key             key   
        for (HashMapEntry e = table[i]; e != null; e = e.next) {
            Object k;
            //  hash    key   ,          ,       
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //  4:       hash                   ,           key         key      ,    Entry   hashmap 
        addEntry(hash, key, value, i);
        return null;
    }

상기 네 가지 절 차 는 hash 충돌 처리 방법 을 나타 내지 않 았 습 니 다. hash 충돌 은 key 값 이 다 르 지만 계산 한 hash 값 은 같 습 니 다.우 리 는 이어서 addEntry 방법 을 보 았 다.
void addEntry(int hash, K key, V value, int bucketIndex) {
    //  1:    hashmap             (      16*0.75),   ,       。            hash           
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    //  2:    Entry    hashmap 
    createEntry(hash, key, value, bucketIndex);
}

createEntry 함수 에 들 어가 서 hashmap 가 어떻게 새 요 소 를 배열 에 저장 하 는 지 확인 합 니 다.
void createEntry(int hash, K key, V value, int bucketIndex) {
    //  1:          ,   Entry      
    HashMapEntry e = table[bucketIndex];
    //  2:        Entry ,   e    key,value,hash   
    table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
    size++;
}

여기 서 hashmap 가 충돌 을 처리 하 는 방식 이 나타 나 지 않 았 습 니 다. 우 리 는 이어서 새로운 Entry 요소 방법 을 보 겠 습 니 다.
HashMapEntry(int h, K k, V v, HashMapEntry n) {
    value = v;//  value
    next = n;//   ,        next,               ,    next          
    key = k;
    hash = h;
}

드디어 hashmap 에서 충돌 을 처리 하 는 코드 를 찾 았 습 니 다. hash 충돌 이 발생 하면 새 요 소 를 링크 머리 에 두 고 next 를 원래 링크 의 요 소 를 가리 킵 니 다.예 를 들 어 첫 번 째 키 값 은 A 에 들 어 와 서 key 의 hash 를 계산 하여 얻 은 index = 2 를 기록 합 니 다: Entry [2] = A.잠시 후에 또 하나의 키 값 이 B 에 들 어 왔 습 니 다. 계산 을 통 해 index 도 2 와 같 습 니 다. HashMap 은 B. next = A, Entry [2] = B 를 다시 들 어 오 면 index 도 2 와 같 습 니 다. 그러면 C. next = B, Entry [2] = C;이렇게 해서 우 리 는 index = 2 의 곳 에서 A, B, C 세 개의 키 값 을 액세스 한 것 을 발 견 했 습 니 다. 그들 은 next 라 는 속성 을 통 해 연결 되 어 있 습 니 다.
3.2 해시 값 에 따라 배열 의 위 치 를 찾 습 니 다.
static int indexFor(int h, int length) {
        return h & (length-1);
    }

비트 에 따라 취 합 하면 모드 모드 모드 나 나머지% 작업 에 해당 하지만 바 이 너 리 를 이용 하 는 작업 이 빠 릅 니 다.여기 서 배열 의 길 이 는 반드시 2 의 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수 지수그리고 확장 후 한 개의 차이 만 있 습 니 다. 즉, 가장 왼쪽 에 있 는 1 이 더 많아 졌 습 니 다. 그러면 h & (length - 1) 를 통과 할 때 h 에 대응 하 는 가장 왼쪽 에 있 는 차이 점 이 0 이면 얻 을 수 있 는 새로운 배열 색인 과 오래된 배열 색인 이 일치 하고 이전에 잘 분 열 된 오래된 배열 의 데이터 위 치 를 크게 줄 일 수 있 습 니 다.
3.3 키 를 빈 값 으로 저장 합 니 다.
key 값 이 비어 있 는 모든 요 소 는 배열 의 머리 table [0] 위치 에 놓 습 니 다. 여 기 는 배열 의 머리 table [0] 위치 에 값 이 있 는 지, 값 이 있 는 지, 같은 key 값 이 있 는 지, 있 으 면 교체 되 는 지, 없 으 면 새 Entry 를 판단 합 니 다. 절 차 는 위 와 유사 합 니 다.
private V putForNullKey(V value) {
        for (HashMapEntry e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

4 get 방법
get 작업 의 소스 코드 는 다음 과 같 습 니 다:
public V get(Object key) {
        //  key  null,         
        if (key == null)
            return getForNullKey();
        //     key       Entry
        Entry entry = getEntry(key);
        //  Entry   value 
        return null == entry ? null : entry.getValue();
    }

getEntry () 방법의 실현 을 다시 봅 시다.
final Entry getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        //   :   key   hash 
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //   :  hash            ,       Entry key         key   ,        Entry
        for (HashMapEntry e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

5. hashmap 스 트 리밍 방법
1) Iterator 로 옮 겨 다 니 기
    Iterator iter = map.entrySet().iterator();
  while (iter.hasNext()) {
      Map.Entry entry = (Map.Entry) iter.next();
      Object key = entry.getKey();
      Object val = entry.getValue();
  }

2) 각 사 이 를 위 한
for (Entry entry : map.entrySet()) {
    Object key = entry.getKey();
    Object val = entry.getValue();
}

6. LinkedHashMap 원리
링크 드 하 쉬 맵 은 하 쉬 맵 의 하위 클래스 로 실현 구 조 는 하 쉬 맵 과 유사 하 다. 다만 하 쉬 맵 의 링크 는 단 방향 링크 이 고 링크 드 하 쉬 맵 은 양 방향 링크 이 며 하 쉬 맵 을 바탕 으로 순 서 를 매 겨 실현 했다.
6.1 구조 함수
링크 드 HashMap 의 구조 함 수 는 HashMap 보다 정렬 매개 변수 accessOrder 가 많 습 니 다.
/**
     * @param  initialCapacity     ,16
     * @param  loadFactor          0.75
     * @param  accessOrder           , true              , false          ,         
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

6.2 정렬 방식 실현
링크 드 Hash MapEntry 에 recordAccess 방법 을 다시 썼 습 니 다. Hash Map 의 Hash MapEntry 는 빈 방법 입 니 다.이 방법 은 접근 순서에 따라 정렬 할 지 여 부 를 판단 합 니 다. addBefore () 를 호출 하면 현재 방문 한 요 소 를 링크 머리 로 이동 합 니 다.접근 순서에 따라 정렬 하지 않 으 면 링크 의 요 소 는 변 하지 않 습 니 다.요 소 를 삽입 할 때 새로 삽 입 된 요 소 는 HashMap 에서 링크 머리 에 넣 는 것 이기 때 문 입 니 다.
void recordAccess(HashMap m) {
            LinkedHashMap lm = (LinkedHashMap)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }

/**
 *            
 */
private void addBefore(LinkedHashMapEntry existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

6.3 put 실현
링크 드 HashMap 은 put 방식 을 재 작성 하지 않 았 으 나 addEntry (), createEntry (), 링크 드 HashMapEntry 의 recordAccess () 방법 을 재 작성 했다.HashMap 에서 하나의 요 소 를 넣 을 때 저장 할 요소 의 hash 값 과 key 값 이 현재 링크 에 존재 하면 이전 값 을 교체 한 후 recordAccess () 방법 을 호출 하기 때 문 입 니 다.createEntry () 방법 에서 LinkedHashMap 의 실현 은 다음 과 같 습 니 다. addBefore () 방법 호출 을 추 가 했 습 니 다.현재 새로 삽 입 된 요 소 를 링크 머리 에 놓 습 니 다.
void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry old = table[bucketIndex];
        LinkedHashMapEntry e = new LinkedHashMapEntry<>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);//   ,           ,                 
        size++;
    }

6.3 get 실현
링크 드 HashMap 에서 요 소 를 가 져 올 때 도 recordAccess 방법 을 사용 하여 방문 요 소 를 링크 머리 로 옮 겼 습 니 다.
public V get(Object key) {
        LinkedHashMapEntry e = (LinkedHashMapEntry)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);//   ,          ,               
        return e.value;
    }

7. LruCache 에서 LinkedHashMap 을 호출 하 는 실현
final LinkedHashMap cache = new LinkedHashMap(100, 0.75f, true);

accessOrder 인 자 를 true 로 설정 하면 방문 순서에 따라 정렬 할 수 있 습 니 다.
8. object 류 의 equals () 를 다시 쓰 는 동시에 hashcode () 를 다시 쓰 는 이 유 는 무엇 입 니까?
Object 대상 의 equals (Object) 방법 은 비어 있 지 않 은 인용 값 x 와 y 에 대해 x 와 y 가 같은 대상 을 인용 할 때 만 true 로 돌아 갑 니 다.equals () 방법 이 재 작성 되 었 을 때 hashcode () 방법 을 다시 써 야 합 니 다. hash 협정 성명 이 같은 대상 은 똑 같은 hashcode 를 가 져 야 합 니 다. 다음 과 같 습 니 다.
1) obj 1. equals (obj 2) 가 true 일 때 obj 1. hashCode () = obj 2. hashCode () 는 true 2 여야 합 니 다. obj 1. hashCode () = = = obj 2. hashCode () 가 false 일 때 obj 1. equals (obj 2) 는 false 여야 합 니 다. obj 1. hashCode () = = obj 2. hashCode () 가 true 일 때 obj 1. equals (obj 2) 가 true 가 아 닙 니 다.
equals 를 다시 쓰 지 않 으 면 대상 의 참조 가 같은 메모리 주 소 를 가리 키 는 지 비교 합 니 다. 다시 쓰 면 두 대상 의 value 값 이 같 는 지 비교 하기 위해 서 입 니 다.Hashcode 는 해시 데이터 의 빠 른 액세스 에 사 용 됩 니 다. 예 를 들 어 HashSet \ HashMap \ \ Hashtable 류 를 이용 하여 데 이 터 를 저장 할 때 hashcode 값 이 같 는 지 판단 하고 다 르 면 equals 비 교 를 할 필요 가 없습니다.equals () 를 다시 썼 지만 hashcode () 를 다시 쓰 지 않 았 다 면 new 대상 이 있 을 때 원래 대상. equals (새로운 대상) 가 true 와 같 을 때 이들 의 hashcode 는 같 지 않 아 두 대상 이 서로 다른 상황 을 얻 을 수 있다.저장 할 때 도 두 개의 값 이 같은 대상 이 발생 합 니 다. hashcode 는 다 르 고 두 개 를 저장 하여 헷 갈 립 니 다.그래서 Object 의 equals () 방법 은 다시 쓰 는 동시에 hashcode () 방법 도 다시 써 야 합 니 다.
자신 이 소스 코드 를 한 번 훑 어 본 후에 자신 이 그것 을 다시 써 서 HashMap 의 실현 에 대한 이 해 는 더욱 깊 어 졌 다.

좋은 웹페이지 즐겨찾기