Java中关于HashMap源码的研究

1.基础知识

1.数组

数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难

2.链表

链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易

3.哈希表

那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表。哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。

2.具体实现

由于HashMap使用的是数组+链表的方式来存储数据的。那么我们先研究下每一个元素存放数据的数据结构--HashMap的内部类。

1.基本元素

Entry<K,V>是HashMap的基本元素单位其本身就是一个链表存储方式。

//定义为静态内部类,使用时不需要外部类的对象
static class Entry<K, V> implements Map.Entry<K, V> {
  //Key为HashMap定义的key,为保证key的稳定性定义为不可修改的final类型
  final K key;
  //value为HashMap的value
  V value;
  //存储的是如果哈希值相同下一个元素的引用。这是一个典型的链表结构
  Entry<K, V> next;
  //hash为hash(key)%length(hashMap长度默认为16)运算后的结果
  int hash;

  //默认构造方法
  Entry(int h, K k, V v, Entry<K, V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
  }
  //后面是一些重写toString、equals、 hashCode等操作就省略了。
}

上面的代码多一句嘴,在JDK1.8中以上的版本中我们会看到Node和TreeNode的基础元素类型是因为JDK1.8版本的HashMap采用数组+链表+红黑树来实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

2.基础结构

下面我们看看HashMap的基础结构Entry数组。

static final Entry<?,?>[] EMPTY_TABLE = {};

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

Entry数组的长度必须是2的幂。至于为什么是2的幂这个问题不是本文的重点。
可以参考
HashMap实现原理及源码分析|
HashMap剖析

3.存取实现。

1.put

既然HashMap的基础是数组那么为什么能够随机存取。而不是数组那样一个一个add存储呢。
为了解释清楚这个概念。需要了解下HashMap内部的一些属性(成员变量)

1.size 这个属性表示了HashMap中所有KV对的数量,包含挂在链表中的KV对。

2.capacity 这个属性表示HashMap的哈希表的长度,也就是table的长度。

3.loadFactor
这个属性表示装载因子(用来形容是否装满,默认为0.75f),用来当HashMap的哈希表是否需要扩容的最大比例。当前的实装的因子为size/capacity。

4.threshold
这个属性表示HashMap的哈希表是否需要扩容的阈值。一般的来说当size大于这个值时会出发resize()操作(哈希散列表扩容的操作)。一般计算方法为capacity*loadFactor

5.modCount
这个属性表示HashMap表修改次数。给迭代器使用以保证Map迭代的完整性。

在项目第一次put是如果发现table的值为空那么就会启动一个初始化table的方法inflateTable(),这个名称很形象叫充气或者叫可以填充的。

if (table == EMPTY_TABLE) {
    inflateTable(threshold);
}
//table初始化方法
private void inflateTable(int toSize) {
    // 将其扩大值2的幂
    int capacity = roundUpToPowerOf2(toSize);

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //创建了一个大小为最大长度的entry数字
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

数组建立完成后数组是有下标的。
我们只需要将key的哈希值与数组的最大长度取余。得出的结果作为存储的下标位置存入该数组。
具体实现如下:

//存值过程  
int index = key.hashCode() % table.length;
Entry[index] = value;

具体是怎么返回索引的呢,h是key的哈希码 length 是table.length由于是全部填充故table的长度大约等于capacity.

& 与运算:参加运算的两个数据,按二进制位进行“与”运算。 运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;

这里用了一个巧妙的算法。应为之前的约定length必定为2的幂。那么如果将length-1的到的结果一定是全1的二进制数字例如15(1111)、 7(111)、 3(11)、1(1)等。
那么将哈希值与这样的值做与运算得出的结果为h对length取余数。下面我举个栗子说明这一点。


我们用长度为16的length举例 16-1=15用二进制表示为(1111)
依据运算规则 0&1 = 0 1&1=1那么我们只要保证,二进制的数值最后4位为0那么他的余数一定是零。只要后面四位有任意一位是1数值都会被过滤出来。成为余数。
11110000(240)、10000(16)、100000(32)、110000(48)等一定是16的倍数。也就是说无论高于4位的数值是什么对余数结果都没有干扰。

对于2的幂作为模数取模,可以用&(2n-1)来替代%2n,位运算比取模效率高很多,因为对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。

static int indexFor(int h, int length) {
   return h & (length-1);
}

这里会出现一个问题如果2个key的哈希值冲突那么会出现什么结果呢。
这时HashMap的链表就登场了。当时我们在研究哈希表存储结构的时候有一个next属性。作用是指向下一个Entry,那么这两个Entry就以链表的形式存储在了一个哈希值下。

public V put(K key, V value) {
    //如果为空即第一次存储执行初始化数组table方法
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    //如果key为null这时就调用putForNullKey来存储value
    //这就是hashMap支持null key的原因。
    if (key == null)
        return putForNullKey(value);
    //上文讲到的计算index
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    //遍历链表
    //这时一个非常漂亮的递归遍历方式
    for (Entry<K,V> 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++;
    //如果key不相同则执行添加(也包含了第一次添加的逻辑)
    addEntry(hash, key, value, i);
    return null;
}
//如果key是null
private V putForNullKey(V value) {
    //如果发现table[0]发现有key等于null的值则覆盖
    for (Entry<K,V> 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++;
    //在table[0]添加一个新的KV
    addEntry(0, null, value, 0);
    return null;
}


//添加新元素
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];
    //创建一个新对象存储当前元素,将当原本元素存储到next中
    //如果两个元素碰撞那么后来者居上。
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    //将元素长度增加
    size++;
}


说下这个扩容的逻辑,就是这个方法resize 需要传入一个容量大小。每次扩容都是前一次容量的两倍。

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.get

取值逻辑

//存值过程  
int index = key.hashCode() % table.length;
return Entry[index]

获取的逻辑就没有存储这么复杂了。

public V get(Object key) {
  //key为null时单独调用获取null的逻辑
   if (key == null)
       return getForNullKey();
  //获取value值
   Entry<K,V> entry = getEntry(key);
   return null == entry ? null : entry.getValue();
}
//key为null值得取值方法
private V getForNullKey() {
     if (size == 0) {
         return null;
     }
     //村吃时候是存在固定位置取时直接从table[0]位置读取
     for (Entry<K,V> e = table[0]; e != null; e = e.next) {
         if (e.key == null)
             return e.value;
     }
     return null;
 }
 //取值逻辑 此方法不能重写。
 final Entry<K,V> getEntry(Object key) {
       if (size == 0) {
           return null;
       }

       int hash = (key == null) ? 0 : hash(key);
       //循环遍历链表查找到值后返回 如果没有返回null
       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;
   }

3.remove、clear、containsValue、containsKey

//依据key移除元素
public V remove(Object key) {
  //依据key的哈希遍历链表然后移除元素
   Entry<K,V> e = removeEntryForKey(key);
   return (e == null ? null : e.value);
}

//clear调用了arrays全填充操作
public void clear() {
    modCount++;
    Arrays.fill(table, null);
    size = 0;
}
//简单粗暴的遍历全部元素判断是否有该value.效率极其低下
public boolean containsValue(Object value) {
    if (value == null)
        return containsNullValue();

    Entry[] tab = table;
    for (int i = 0; i < tab.length ; i++)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            if (value.equals(e.value))
                return true;
    return false;
}
//判断Key是否存在.很高效
public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

4.Iterator

首先我们先看下一个抽象哈希迭代器

private abstract class HashIterator<E> implements Iterator<E> {

    Entry<K,V> next;        // 下一个迭代的元素
    int expectedModCount;   // 开始迭代修改书
    int index;              // 当前的标记
    Entry<K,V> current;     // 当前的实例
    //初始化迭代器给next赋值
    HashIterator() {
      expectedModCount = modCount;
      if (size > 0) { // advance to first entry
          Entry[] t = table;
          while (index < t.length && (next = t[index++]) == null);
      }
    }

    public final boolean hasNext() {
      return next != null;
    }
    //读取下一个元素
    final Entry<K,V> nextEntry() {
      if (modCount != expectedModCount)
          throw new ConcurrentModificationException();
      Entry<K,V> e = next;
      if (e == null)
          throw new NoSuchElementException();

      if ((next = e.next) == null) {
          Entry[] t = table;
          while (index < t.length && (next = t[index++]) == null)
              ;
      }
      //将当前元素赋值给当前元素属性
      current = e;
      return e;
    }

    public void remove() {
      if (current == null)
          throw new IllegalStateException();
      if (modCount != expectedModCount)
          throw new ConcurrentModificationException();
      Object k = current.key;
      current = null;
      HashMap.this.removeEntryForKey(k);
      expectedModCount = modCount;
    }
}

HashMap提供了3中迭代器遍历方式

1.值遍历(values)

//对外提供的方法
//这里的values是Values这个内部类的实例
public Collection<V> values() {
    Collection<V> vs = values;
    return (vs != null ? vs : (values = new Values()));
}
//这是一个内部类实现了一个迭代器Collection<V>能接收valus这个实例这是向上造型
//这个实例返回的实际上是一个Map元素的映射因为基于map所以数值是动态变化的
private final class Values extends AbstractCollection<V> {
    public Iterator<V> iterator() {
        return newValueIterator();
    }
    public int size() {
        return size;
    }
    public boolean contains(Object o) {
        return containsValue(o);
    }
    public void clear() {
        HashMap.this.clear();
    }
}
//返回一个迭代器对象
Iterator<V> newValueIterator()   {
   return new ValueIterator();
}
//迭代器内部类
//当不断调用next()该方法时 元素就一个接一个呗读取出来了
private final class ValueIterator extends HashIterator<V> {
   public V next() {
       return nextEntry().value;
   }
}

具体在代码中的用法

//第一种
Collection<String> vs = m.values();
System.out.println(vs);
//第二种
Iterator<String> vs2 = m.values().iterator();
while(vs2.hasNext()){
	System.out.println(vs2.next());
}

2.键遍历(keySet)

迭代方式和值遍历略有不同本质上还是使用HashIterator来迭代。只不过由取value变成了取key


public Set<K> keySet() {
   Set<K> ks = keySet;
   return (ks != null ? ks : (keySet = new KeySet()));
}

private final class KeySet extends AbstractSet<K> {
   public Iterator<K> iterator() {
       return newKeyIterator();
   }
   public int size() {
       return size;
   }
   public boolean contains(Object o) {
       return containsKey(o);
   }
   public boolean remove(Object o) {
       return HashMap.this.removeEntryForKey(o) != null;
   }
   public void clear() {
       HashMap.this.clear();
   }
}

Iterator<K> newKeyIterator()   {
   return new KeyIterator();
}
private final class KeyIterator extends HashIterator<K> {
    public K next() {
        return nextEntry().getKey();
    }
}

实际迭代用法,雷同与值遍历

Set<String> keys = m.keySet();
System.out.println(keys);
Iterator<String> keys2 = m.keySet().iterator();
while (keys2.hasNext()) {
	System.out.println(keys2.next());
}

3.键值对遍历(entrySet)

迭代方式相同此处就不在赘述。

public Set<Map.Entry<K,V>> entrySet() {
   return entrySet0();
}

private Set<Map.Entry<K,V>> entrySet0() {
    Set<Map.Entry<K,V>> es = entrySet;
    return es != null ? es : (entrySet = new EntrySet());
}

private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public Iterator<Map.Entry<K,V>> iterator() {
        return newEntryIterator();
    }
    public boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<K,V> e = (Map.Entry<K,V>) o;
        Entry<K,V> candidate = getEntry(e.getKey());
        return candidate != null && candidate.equals(e);
    }
    public boolean remove(Object o) {
        return removeMapping(o) != null;
    }
    public int size() {
        return size;
    }
    public void clear() {
        HashMap.this.clear();
    }
}

Iterator<Map.Entry<K,V>> newEntryIterator()   {
    return new EntryIterator();
}

private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() {
        return nextEntry();
    }
}

用法和上面两个并无差别

Set<Entry<String, String>> es = m.entrySet();
System.out.println(es);
Iterator<Map.Entry<String, String>> it = m.entrySet().iterator();
while (it.hasNext()) {
	System.out.println(it.next());
}

3.HashMap的问题

HashMap的线程安全问题一直为人所诟病,幸好我们有了Hashtable、ConcurrentHashMap等安全的hashmap。

4.总结

  1. 允许以Key为null的形式存储<null,Value>键值对。

  2. HashMap的查找效率非常高,因为它使用Hash表对进行查找,可直接定位到Key值所在的链表中;

  3. 使用HashMap时,要注意HashMap容量和加载因子的关系,这将直接影响到HashMap的性能问题。加载因子过小,会提高HashMap的查找效率,但同时也消耗了大量的内存空间,加载因子过大,节省了空间,但是会导致HashMap的查找效率降低。需要使用接从中权衡利弊。

posted @ 2017-12-20 14:49  枫飘雪落  阅读(311)  评论(0编辑  收藏  举报