자바 HashMap 실현 원리 상세 설명

14035 단어 JavaHashMap
HashMap 은 해시 표 의 Map 인 터 페 이 스 를 기반 으로 하여 선택 할 수 있 는 모든 맵 작업 을 제공 하고 null 값 과 null 을 사용 하여 만 들 수 있 으 며 동기 화 되 지 않 으 며 맵 순 서 를 보장 하지 않 습 니 다.HashMap 의 실현 원 리 를 연구 하 겠 습 니 다.
HashMap 내부 저장 소
HashMap 내부 에 서 는 순간 변수 배열 table(또는 통)을 유지 하여 모든 키 쌍 관 계 를 저장 합 니 다.통 은 Entry 대상 배열 입 니 다.통 의 크기 는 필요 에 따라 크기 를 조정 할 수 있 고 길 이 는 2 의 차 멱 이 어야 합 니 다.다음 코드:

/**
  *     entry  ,      
  */
 static final Entry<?,?>[] EMPTY_TABLE = {};
 /**
  *  ,      ,    2   
  */
 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
초기 용량 과 부하 인자
HashMap 은 두 개의 매개 변수 가 성능 에 영향 을 주 고 초기 용량 과 부하 인자 가 있 습 니 다.용량 은 해시 표 의 통 수량 이 고 초기 용량 은 해시 표 가 만 들 때 용량 일 뿐 부하 인 자 는 해시 표 가 용량 이 자동 으로 증가 하기 전에 더 많은 척도 에 이 를 수 있다.해시 표 의 항목 수가 부하 인자 와 현재 용량 의 곱 을 초과 할 때 이 해시 표 에 대해 rehash 작업(즉 내부 데이터 구 조 를 재 구축 하 는 것)을 하고 재 구축 할 때 현재 용량 의 두 배 수량 으로 새로 만들어 야 합 니 다.구조 기 를 통 해 초기 용량 과 부하 인 자 를 설정 할 수 있 습 니 다.기본 초기 용량 은 16 개 항목 이 고 최대 용량 은 2^30 제곱 항목 입 니 다.기본 부하 인 자 는 0.75 입 니 다.
통 은 물 을 저장 하 는 물통 과 같 습 니 다.기본 적 인 초기 저장 용량 은 16 개 단위 의 물 입 니 다.기본 적 으로 16*0.75 에 물 을 주입 할 때 다음 에 데 이 터 를 추가 할 때 먼저 용량 을 늘 려 32 단위 로 확대 합 니 다.0.75 는 부하 인자 로 초기 용량 과 부하 인 자 는 물통 을 만 들 때 설정 할 수 있다.물통 의 최대 용량 은 2 의 30 제곱 단위 의 물이 다.초기 용량 설정 의 수량 이 최대 용량 보다 많 을 때 최대 용량 을 기준 으로 합 니 다.확장 시 최대 용량 보다 크 면 바로 돌아 갑 니 다.
아래 는 HashMap 의 일부 소스 코드 로 기본 초기 용량,부하 인자 및 기타 상수 들 을 정의 합 니 다.

/**
  *        ,   2   The default initial capacity - MUST be a power of two.
  */
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 /**
  *     ,                          ,               *    2        2 30    */
 static final int MAXIMUM_CAPACITY = 1 << 30;
 /**
  *        ,          
  */
 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 /**
  *        ,           
  */
 static final Entry<?,?>[] EMPTY_TABLE = {};
 /**
  *   ,           ,        ,       2    
  */
 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
 /**
  * Map       ,              size  +1  -1  .
  */
 transient int size;
 /**
  *    ,          , :(capacity * load factor).                   
  * @serial
  */
 // If table == EMPTY_TABLE then this is the initial capacity at which the
 // table will be created when inflated.
 int threshold;
 /**
  *     ,           ,          ,
  *
  * @serial
  */
 final float loadFactor;
 /**
  * HashMap      ,       HashMap              (  ,* rehash  ,        ),        * HashMap                     
  */
 transient int modCount;
초기 용량 및 부하 인자 성능 조정
일반적으로 기본 부하 인자(0.75)는 시간 과 공간 원가 에서 절충 을 찾는다.부하 인자 가 너무 높 아 공간 비용 을 줄 였 지만 조회 비용 도 증가 했다.초기 용량 을 설정 할 때 맵 에 필요 한 항목 수 와 부하 요 소 를 고려 하여 rehash 작업 횟수 를 최대한 줄 여야 합 니 다.초기 용량 이 최대 항목 수 를 로드 인자 로 나 누 면 rehash 작업 이 일어나 지 않 습 니 다.
많은 맵 관 계 를 HashMap 인 스 턴 스 에 저장 하려 면 필요 에 따라 자동 rehash 작업 을 실행 하여 표 의 용량 을 늘 리 는 것 보다 충분 한 초기 용량 으로 만 들 면 맵 관 계 를 더욱 효과적으로 저장 할 수 있 습 니 다.
HashMap 데이터 구 조 를 재 구축 하기 위 한 코드:

void resize(int newCapacity) {
  Entry[] oldTable = table;
  int oldCapacity = oldTable.length;
  if (oldCapacity == MAXIMUM_CAPACITY) { //           ,            
   threshold = Integer.MAX_VALUE;
   return;
  }
  //     table    
  Entry[] newTable = new Entry[newCapacity];
  //   table        table  ,            
  transfer(newTable, initHashSeedAsNeeded(newCapacity));
  table = newTable;
  //                
  threshold = (int) Math.min(newCapacity * loadFactor,
    MAXIMUM_CAPACITY + 1);
}
HashMap 구조 방법
 
네 번 째 구조 방법 은 이미 존재 하 는 Map 으로 새로운 HashMap 을 만 드 는 것 입 니 다.잠시 후에 말씀 드 리 겠 습 니 다.앞의 세 번 째 구조 방법 은 모두 세 번 째 로 두 개의 매개 변 수 를 가 진 방법 입 니 다.만약 에 매개 변 수 를 전달 하지 않 으 면 기본 적 인 수 치 를 사용 합 니 다.코드 는 다음 과 같 습 니 다.

/**
   * Constructs an empty <tt>HashMap</tt> with the default initial capacity
   * (16) and the default load factor (0.75).
   */
  public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
  }
  /**
   * Constructs an empty <tt>HashMap</tt> with the specified initial
   * capacity and the default load factor (0.75).
   *
   * @param initialCapacity the initial capacity.
   * @throws IllegalArgumentException if the initial capacity is negative.
   */
  public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
  }
  /**
   * Constructs an empty <tt>HashMap</tt> with the specified initial
   * capacity and load factor.
   *
   * @param initialCapacity the initial capacity
   * @param loadFactor   the load factor
   * @throws IllegalArgumentException if the initial capacity is negative
   *     or the load factor is nonpositive
   */
  public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
      throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
      initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
      throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    init();
  }
위 에서 알 수 있 듯 이 구조 함수 에서 초기 용량 이 최대 용량 보다 크 면 직접 최대 용량 으로 대체 된다.
put 방법
이제 HashMap 에서 중요 한 부분 을 살 펴 보도 록 하 겠 습 니 다.

/**
   *               。                   ,      
   *
   * @param         
   * @param         
   * @return  key     ,  key        ,   null(  null           null key  )
   */
  public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
      inflateTable(threshold);
    }
    if (key == null)
      return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
      Object k;
      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        e.recordAccess(this);
        return oldValue;
      }
    }
    modCount++;
    addEntry(hash, key, value, i);
    return null;
  }
 
1.먼저 put 방법 에서 통 이 기본 적 인 초기 화 되 지 않 은 상태 인지 판단 하고 초기 화 되 지 않 으 면 inflateTable 방법 으로 초기 화 한 다음 에 매개 변수 key 가 null 인지 판단 합 니 다.null 이면 putForNullKey 를 사용 하여 key 를 null 로 하 는 데 이 터 를 사용 합 니 다.putForNullKey 방법 은 아래 의 세 번 째 단계 와 똑 같 습 니 다.다만 키 가 null 인 데이터 의 기본 저장 위 치 는 첫 번 째 입 니 다.즉,아래 표 시 는 기본 값 이 0 입 니 다.
 2. key 가 null 이 아니라면 hash()방법 으로 key 의 hash 값 을 가 져 옵 니 다.hash 값,통 의 길이 에 따라 index For 방법 으로 이 key 를 통 의 위치 에 놓 을 수 있 습 니 다.
 3.Entry 대상 중 하나의 속성 next 가 있 는데 단 방향 링크 를 형성 하여 해시 값 과 같은 요 소 를 저장 할 수 있 습 니 다.따라서 key 의 hash 값 이 중복 되 었 을 때 저장 위치 도 중복 되 며,저장 위치 에 있 는 요소 와 이 요소 의 next 속성 링크 에서 주어진 key 와 key 의 hash 값 이 완전히 일치 하 는 지 판단 하면 됩 니 다.만약 완전히 일치 하 는 것 이 있다 면,대표 가 이미 존재 한다 면,낡은 값 을 교체 하고,낡은 값 을 반환 값 으로 직접 되 돌려 줍 니 다.
 4.구조 수정 횟수 1 증가
 5.addEntry 방법 을 호출 하여 새로운 키 쌍 을 HashMap 에 추가 합 니 다.addEntity 방법 은 먼저 현재 항목 데이터 가 부하 값(통 의 용량*부하 인자)보다 크 고 통 의 지정 위 치 는 null 이 아 닌 지 판단 합 니 다.만약 에 이미 크 고 지 정 된 위치 가 null 이 아니라면 통 의 용량 을 현재 용량 의 2 배로 조정 하고 통 의 용량 을 조정 하 는 것 은 위의 초기 용량 과 부하 인자 성능 조정 디 렉 터 리 를 참조 합 니 다.Hash 값 을 다시 계산 하여 저장 위 치 를 계산 합 니 다.createEntry 방법 을 통 에 저장 합 니 다.

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
      resize(2 * table.length);
      hash = (null != key) ? hash(key) : 0;
      bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
  }
  void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
  }
  /**
  * Entry    ,      Entry.
  */
  Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
  }
 6.createEntry 방법 에서 먼저 지 정 된 위치의 entry 를 가 져 온 다음 에 entry 를 새로 생 성 합 니 다.entry 를 생 성 할 때 기 존의 entry 를 새로 생 성 된 entry 의 next 속성 에 저장 하고(Entry 의 구조 방법 참조)지 정 된 위치의 entry 를 새로 생 성 합 니 다.
항목 을 새로 만 들 때 hash 값 을 계산 해 야 하기 때문에 길이 가 부족 할 때 길 이 를 조정 해 야 합 니 다.계 산 된 저장 위치 에 요소 가 있 을 때 링크 식 저장 이 필요 하기 때문에 HashMap 을 사용 하여 새로 만 드 는 작업 의 효율 이 그리 높 지 않 습 니 다.
get 방법
우선 get 방법의 원본 코드 를 보십시오.

/**
   *           ;        ,            ,   null
   *   null                  ,               null,   containsKey          
   * @see #put(Object, Object)
   */
  public V get(Object key) {
    if (key == null)
      return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
  }
  final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
      return null;
    }
    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K,V> 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;
}
get 방법 은 비교적 간단 하 다.다음은 몇 가지 절차 이다.
  • 키 가 null 인지 여 부 를 먼저 판단 하고 null 이면 getForNullKey 방법 으로 가 져 옵 니 다.null 이 아니라면 getEntry 방법 으로 가 져 옵 니 다.getForNullKey 방법 은 getEntity 와 기본적으로 일치 합 니 다.한 단계 만 빠 지지 않 았 습 니 다.기본 키 가 null 인 저장 위 치 는 첫 번 째,즉 아래 가 0 으로 표시 되 어 위 치 를 계산 하지 않 았 을 뿐 입 니 다.
  • getEntity 방법 은 key 에 따라 해시 값 을 계산 한 다음 에 해시 값,통 의 길이 로 저장 위 치 를 계산한다.
  • getEntity 는 지정 한 위 치 를 가 져 오 는 entry 를 옮 겨 다 니 는 시작 으로 entry 의 next 단일 체인 표를 옮 겨 다 니 며 entry 의 해시 값 이 계 산 된 해시 값 과 일치 하고 entry 의 key 가 지정 한 것 과 같 으 면 entry
  • 로 돌아 갑 니 다.
  • getEntity 가 되 돌아 오 는 값 에 따라 get 방법 은 해당 하 는 값 을 되 돌려 줍 니 다.
  • get 의 소스 코드 를 보면 get 방법 은 key 의 해시 값 과 통 의 길 이 를 통 해 저장 위 치 를 계산 하고 기본적으로 찾 아야 할 요 소 를 찾 을 수 있 습 니 다.해시 값 을 반복 하 는 key 를 몇 개 더 옮 겨 다 녀 도 빠 릅 니 다.해시 값 이 상대 적 으로 유일 하기 때문에 HashMap 은 검색 성능 이 매우 빠 릅 니 다.
    사용자 정의 대상 을 HashMap 키 로 사용 합 니 다.
    
    class User {
      //      
      protected int idNumber;
      public User(int id){
        idNumber = id;
      }
    }
    public class TestUser{
      public static void main(String[] args) {
        Map<User, String> map = new HashMap<User, String>();
        for (int i=0; i<5; i++) {
          map.put(new User(i), "  : " + i);
        }
        System.out.println("User3    :" + map.get(new User(3)));
      }
    }
      :
    User3    :null
    
    위 코드 와 같이 사용자 정의 User 클래스 인 스 턴 스 를 통 해 HashMap 의 대상 으로 인쇄 할 때 User 3 의 이름 을 찾 을 수 없습니다.User 클래스 는 기본 Object 를 자동 으로 계승 하기 때문에 Object 의 hashCode 방법 으로 해시 값 을 자동 으로 생 성 합 니 다.기본 값 은 대상 의 주소 로 해시 값 을 계산 합 니 다.따라서 new User(3)가 생 성 한 첫 번 째 인 스 턴 스 의 해시 값 은 생 성 된 두 번 째 인 스 턴 스 의 해시 값 과 다르다.그러나 hashCode 를 간단하게 덮어 쓰 는 방법 만 있 으 면 정상적으로 작 동 할 수 없습니다.equals 방법 을 동시에 덮어 쓰 지 않 는 한 Object 의 일부분 이기 도 합 니 다.HashMap 은 equals()를 사용 하여 현재 키 가 표 에 존재 하 는 키 와 같 는 지 판단 하고 위의 get 또는 put 방법 을 참고 할 수 있 습 니 다.
    정확 한 equals()방법 은 다음 과 같은 5 가지 조건 을 만족 시 켜 야 한다.-489 페이지 참조
  • 자 반성.임의의 x,x.equals(x)에 대해 서 는 반드시 true
  • 로 돌아 갑 니 다.
  • 대칭 성.임의의 x 와 y 에 대해 y.equals(x)가 true 로 돌아 가면 x.equals(y)도 true
  • 로 돌아 갑 니 다.
  • 전달 성.임의의 x,y,z 에 대해 x.equals(y)가 true 로 돌아 가면 y.equals(z)가 true 로 돌아 가면 x.equals(z)는 반드시 true
  • 로 돌아 갑 니 다.
  • 일치 성,임 의 x 와 y 에 대해 대상 에서 등가 비교 에 사용 되 는 정보 가 변 하지 않 았 다 면 x.equals(y)를 몇 번 호출 하 든 되 돌아 오 는 결 과 는 일치 해 야 합 니 다.일치 하거나 true 이거 나 일치 하 는 것 은 false 입 니 다.
  • null 이 아 닌 x,x.equals(null)는 반드시 false
  • 로 돌아 갑 니 다.
    다시 강조:기본 Object.equals()는 대상 의 주소 만 비교 하기 때문에 하나의 new User(3)는 다른 new User(3)와 같 지 않 습 니 다.따라서 자신의 클래스 를 HashMap 키 로 사용 하려 면 hashCode()와 equals()를 동시에 다시 불 러 와 야 합 니 다.
    다음 코드 는 정상적으로 작 동 할 수 있 습 니 다.
    
    class User {
      //      
      protected int idNumber;
      public User(int id){
        idNumber = id;
      }
      @Override
      public int hashCode() {
        return idNumber;
      }
      @Override
      public boolean equals(Object obj) {
        return obj instanceof User && (idNumber==((User)obj).idNumber);
      }
    }
    public class TestUser{
      public static void main(String[] args) {
        Map<User, String> map = new HashMap<User, String>();
        for (int i=0; i<5; i++) {
          map.put(new User(i), "  : " + i);
        }
        System.out.println("User3    :" + map.get(new User(3)));
      }
    }
      :
    User3    :  : 3
    
    위 에 서 는 hashCode 에서 idNumber 를 유일한 판별 으로 되 돌려 주 었 을 뿐 사용자 도 자신의 업무 에 따라 자신의 방법 을 실현 할 수 있 습 니 다.equals 방법 에서 인 스 턴 스 of 는 대상 이 null 인지 조용히 검사 합 니 다.인 스 턴 스 of 왼쪽 매개 변수 가 null 이면 false 로 돌아 갑 니 다.equals()의 매개 변수 가 null 이 아니 고 유형 이 정확 하 다 면 각 대상 의 실제 idNumber 를 기반 으로 비교 합 니 다.수출 을 통 해 알 수 있 듯 이 현재 의 방식 은 정확 하 다.
    참고:
       《자바 프로 그래 밍 사상》.
        JDK 1.6 중국어 도움말 북
    이상 은 본 고의 모든 내용 입 니 다.본 고의 내용 이 여러분 의 학습 이나 업무 에 어느 정도 도움 이 되 기 를 바 랍 니 다.또한 저 희 를 많이 지지 해 주시 기 바 랍 니 다!

    좋은 웹페이지 즐겨찾기