HashMap源码分析

基于JDK1.7中的HashMap分析

字段说明

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  默认容器长度(与直接写16有什么区别?这样效率高?)
static final int MAXIMUM_CAPACITY = 1 << 30;  //容器最大长度
static final float DEFAULT_LOAD_FACTOR = 0.75f;  //默认扩容阀值
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  //瞬态的Entry数组对象,不会被序列化
transient int size;  //数据数量
int threshold;  //数据长度扩容阀值,即达到此数量的数据时扩容
final float loadFactor;  //扩容阀值

Entry,数据单元

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
      //省略其它getKey、getValue、setValue、equals、hashCode、toString等方法
    }
  1. Entry是个内部类,有4个属性,key、value、hash这就是存进来的数据key,value和hash值
  2. 重点是Entry<K,V> next,这个是下一个Entry,一个扣一个,可见Entry是链式结构
  3. 而HashMap的容器table呢,是Entry<K,V>[] table这样的,数组形式的Entry,也就是说 容器是数组+链式结构
  4. 这样的容器结构的好处是什么呢?数组的优点是便于查找,链式的优点是便于修改,这样可以把两者优点结合

创建对象

  1. 构造函数是HashMap(int initialCapacity, float loadFactor),还有三个重载函数,HashMap()、HashMap(int initialCapacity),和 HashMap(Map<? extends K, ? extends V> m)
  2. initialCapacity是指初始化容器长度,默认值为 1 << 4 ,也就是16,指定的长度不能大于MAXIMUM_CAPACITY,也就是 1 << 30,超过就指定为MAXIMUM_CAPACITY
  3. loadFactor为阀值,默认为0.75f,也就是说在容器内数据达到3/4时,进行容器扩容
  4. HashMap在创建对象时没有扩张容器,table还是EMPTY_TABLE

基本方法

put(k,v),添加数据

  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;
    }
    //添加Entry数据单元
    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);
    }
    //创建Entry对象并与数据链连接
    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++;
    }
  1. 在这可以看到,在添加数据时才判断如是容器为空,则扩张容器
  2. 如果key为null,先进行特殊处理
  3. 根据key得到一个hash值,再根据hash值得到一个索引值,接下来先看容器table中对应索引位置的Entry链上有没有重复的key,有的话覆盖原值并返回老值,没有的话继续往后走,进入到addEntry()
  4. 在addEntry中有个判断,如果数据达到阀值,则扩容数据,长度是现在容器长度的2倍,然后重新获取索引值
  5. 到这就只需要创建一个新的Entry对象,并链接对应索引位置的Entry,就相于在一个锁链的前端加了一个扣,就完事了

inflateTable(int toSize),扩张容器

  private void inflateTable(int toSize) {
      // Find a power of 2 >= toSize
      int capacity = roundUpToPowerOf2(toSize);

      threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
      table = new Entry[capacity];
      initHashSeedAsNeeded(capacity);
    }

threshold是创建对象时的initialCapacity,如果没有指定,则默认为16

get(k),获取数据

  public V get(Object key) {
     if (key == null)
          return getForNullKey();
      Entry<K,V> entry = getEntry(key);

      return null == entry ? null : entry.getValue();
    }

    //getEntry才是取数据的实际操作
  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;
    }

取数据时也就跟存数据类似了,取hash值,得到索引位置,然后遍历Entry链,找到对应key的value

resize(n) 扩容

    void resize(int newCapacity) {
      Entry[] oldTable = table;
      int oldCapacity = oldTable.length;
      if (oldCapacity == MAXIMUM_CAPACITY) {
          threshold = Integer.MAX_VALUE;
          return;
      }

      Entry[] newTable = new Entry[newCapacity];
      transfer(newTable, initHashSeedAsNeeded(newCapacity));
      table = newTable;
      threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    //转移数据,将旧容器中的数据遍历,重新获取在新容器中的索引位置,然后存入进去
    void transfer(Entry[] newTable, boolean rehash) {
      int newCapacity = newTable.length;
      for (Entry<K,V> e : table) {
          while(null != e) {
              Entry<K,V> next = e.next;
              if (rehash) {
                  e.hash = null == e.key ? 0 : hash(e.key);
              }
              int i = indexFor(e.hash, newCapacity);
              e.next = newTable[i];
              newTable[i] = e;
              e = next;
          }
      }
    }

补充说下扩容方法,从上面可以看出,扩容时都是扩容成原来的2倍容量,并且需要把原数据都取出来重新计算存入新容器中,所以如果在使用HashMap时提前知道要存放的数据量并定义好,可以减少一些不必要的性能消耗

其它方法

其它的常用方法,remove(k)就是跟put(k,v)反过来,找到对应的Entry,然后从链上去除,别的如containsKey(k),containsValue(v),keySet(),entrySet()等也基本都是包装的getEntry()方法

posted @ 2021-12-30 14:06  lixuelong  阅读(41)  评论(0编辑  收藏  举报