JDK学习---深入理解java中的HashMap、HashSet底层实现

本文参考资料:

1、《大话数据结构》

2、http://www.cnblogs.com/dassmeta/p/5338955.html

3、http://www.cnblogs.com/dsj2016/p/5551059.html

4、http://blog.csdn.net/hackbuteer1/article/details/6591486/

5、http://blog.csdn.net/feixiaoxing/article/details/6848077

6、http://www.cppblog.com/cxiaojia/archive/2012/07/31/185760.html

7、http://www.cnblogs.com/dolphin0520/p/3681042.html

  刚刚添加好《JDK学习---深入理解java中的String》一篇的第四节数据结构部分,相信大家对线性表的顺序存储结构有一定的了解了吧。因为HashMap的底层就涉及到了链表,那么接下来我就再介绍一下链表、尤其是单链表的知识。

 一、链表

  线性表的顺序存储结构,在上一篇博客《JDK学习---深入理解java中的String》的第四节买火车的例子中已经说明了,它的最大缺点就是插入或删除的时候,需要大量的移动元素,这显然是耗时间的,在数据结构中能够有更加优化的方案呢?

  要解决这个问题,我们就得思考一下导致这个问题的原因。

  为什么插入和删除时,需要大量移动元素,仔细分析后发现原因在于相邻的元素在存储位置也具有邻居关系,它们的编号分别为1,2,3......,n。它们在内存中也是紧挨着的,中间没有间隙,当然就无法快速的介入,而删除后,当中就会留下空隙,自然需要时间去弥补,这就是问题所在。

  接下来会引入链表,链表就是链式存储的线性表。根据指针域的不同,链表分为单向链表双向链表循环链表等等。

  本文只介绍链表中最简单的一种:单向链表。每个元素包含两个域,值域和指针域,我们把这样的元素称之为节点。每个节点的指针域内有一个指针,指向下一个节点,而最后一个节点则指向一个空值。具体请看下图:

  JDK学习---深入理解java中的HashMap、HashSet底层实现

从上图我们可以看出来,每一个节点,都会存在一个指针域,而这个指针域持有的是下一个节点的地址,这样的话就可以避免每个节点元素在内存中存在邻居关系,也就是说各个节点可以分散存储,每个节点只需要持有下一个节点的内存地址即可。这样也就避免了像线性表的顺序存储结构的缺点。

  单链表的插入:

     假设存储元素e的节点为s,那实现节点p、p->next和s之间的逻辑关系的变化,只需要将节点s插入到节点p和节点p->next之间即可。如下图所示,单链表的插入根本不需要惊动其他节点,只需要让s->next和p->next 指针做一点改变即可。

  JDK学习---深入理解java中的HashMap、HashSet底层实现

s->next = p-> next;
p->next = s;

   解读这两句话,就是让p的后继节点改成s的后继节点,再把节点s变成p的后继节点,如下图:

JDK学习---深入理解java中的HashMap、HashSet底层实现

单链表第i个数据插入节点的算法思路:

    1、声明一个节点p指向链表的第一个节点,初始化j从1开始;

    2、当 j<i 时,就遍历链表,让p的指针向后移动,不断指向下一个节点,j累加1;

    3、若到链表末尾p为空,则说明第i个元素不存在;

    4、否则查找成功,在系统中生成一个空节点s;

    5、将数据元素e 赋值给 s->next;

    6、单链表插入的标准语句:s->next = p-> next; p->next = s;

    7、返回成功。

思考:上面两句节点操作语句是否可以交换顺序?

  如果先 p->next = s; 再 s->next = - ->next;会怎么样?因为第一句会使得将p->next给覆盖成s的地址了。那么s->next = p->next,其实就等于s->next = s;这样真正的拥有的a<i+1> 数据元素的结点就没有了上级。这样的插入操作就是失败的。需要注意

  

单链表的删除:

  JDK学习---深入理解java中的HashMap、HashSet底层实现

删除的过程中,我们要做的,其实就是一步,p->next = p->next->next, 用q来取代 p->next, 即:

q=p->next; p->next = q->next;

   解读这段代码,也就是说让p的后继的后继结点改成p的后继结点。

   举个例子,爸爸的左手牵着妈妈的手,右手牵着宝宝的手在散步。突然迎面来了一个美女,爸爸一下子看呆了,此情此景被妈妈逮了个正着,于是她甩开牵着爸爸的手,绕过他,扯开父子俩,拉起宝宝的手就超前走去。妈妈是P节点,妈妈的后继节点是爸爸p->next,也可以叫做q节点。妈妈的后继的后继节点是儿子:p->next ->next,即q->next;当妈妈去牵儿子的手时,这个爸爸就已经与母子两没有任何关系了。如下图:

JDK学习---深入理解java中的HashMap、HashSet底层实现

单链表第i个数据删除的算法思路:

    1、声明一个节点p指向链表的第一个节点,初始化j从1开始;

    2、当 j<i 时,就遍历链表,让p的指针向后移动,不断指向下一个节点,j累加1;

    3、若到链表末尾p为空,则说明第i个元素不存在;

    4、否则查找成功,将欲删除的节点 p->next 赋值给q;

    5、单链表的删除标准语句为 p->next = q->next;

    6、将q节点中的数据赋值给e,作为返回;

    7、释放q节点,返回成功;

  

 单链表的读取:

    线性表顺序存储结构中,我们要查询任意的存储位置都很容易。但在单链表中,由于第i个元素到底在哪?没有办法一开始就知道,必须从头开始找。说白了就是从头开始找,直到找到第i个元素为止。由于这个算法的时间复杂度取决于i的位置,当 i = 1时,则不需要遍历,第一个就取出数据了;而当 i =n 时则遍历n-1次才可以。这是时间复杂度。

    由于单链表的结构没有定义表长,所以事先不能直到要循环多少次,因此也就不方便for来循环控制,其核心思想就是 “工作指针后移”,很麻烦。如果仅仅只是读取,链表还不如线性表的顺序存储结构效率高呢!

二、HashSet底层解读:

  JDK的API上说此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用 null 元素。

  为什么呢?下面看看源码去一探究竟吧。

HashSet的成员变量:

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L; private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

add方法添加元素:直接将元素存储到map中。

  public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

remove方法:直接将map中对应的元素移除。

  public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}

isEmpty方法:

 public boolean isEmpty() {
return map.isEmpty();
}

remove方法:

   public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}

iterator方法:

 public Iterator<E> iterator() {
return map.keySet().iterator();
}

 现在回忆一下常见的面试题

    1、HashSet集合是否有重复元素? (不可以,因为HashMap的key不可以重复)

  2、HashSet集合是否可以有null?(可以,因为HashMap的key可以有一个null)

  3、HashSet元素是否有序?  (无序,因为HashMap的key无序)

  以上这些就是我们常用的HashSet方法了吧,有没有觉得好简单,读到这些代码,有没有觉得信心爆棚,此刻是不是膨胀了? jdk源码原来so easy,哈哈!

三、HashMap底层实现

  之前是逗你玩呢,还真以为JDK有这么简单啊,要是都这样的级别,那java岂不是人人都可以成为大神了?收拾收拾心情,回来继续学习,现在进入JDK的入门代码继续研究吧!

认识一下HashMap结构图吧:

JDK学习---深入理解java中的HashMap、HashSet底层实现

再看HashMap的内部类Entry<K,V>,这个类是全文的中心点

 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;
} public final K getKey() {
return key;
} public final V getValue() {
return value;
} public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
} public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
} public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
} public final String toString() {
return getKey() + "=" + getValue();
} /**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
} /**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}

     这个类是干什么的呢?简单点说,HashMap里面保存的数据最底层是一个Entry型的数组,这个Entry则保留了一个键值对,还有一个指向下一个Entry的指针。所以HashMap是一种结合了数组和链表的结构。

 

大家调测代码的时候,是不是根据一个功能逐步的去跟踪代码呢?现在我们也按照这个思路去逐步分解HashMap方法吧?

 先看看put方法:

public V put(K key, V value) {
//当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
if (key == null)
return putForNullKey(value);
//计算key的hash值
int hash = hash(key.hashCode());
//计算key hash 值在 table 数组中的位置
int i = indexFor(hash, table.length);
//从i出开始迭代 e,找到 key 保存的位置
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
//1、判断该条链上是否有相同的hash值并且key相同,那么此处直接找到table数组修改对应的e原始value值
//2、如果此处hash值不同,则直接添加数组tablle
//3、如果此处hash值相同,但是key不同。那么找到数组下标就有可能重复,那么此时table数组的同一个下标处就会存储多个元素,它们是以链表形式存储
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value; //旧值 = 新值
e.value = value;
e.recordAccess(this);
return oldValue; //返回旧值
}
}
//修改次数增加1
modCount++;
//将key、value添加至i位置处
addEntry(hash, key, value, i);
return null;
}

  

 代码分解:int hash = hash(key);这个方法是根据key生成一个hash值。两个对象的存储地址不同也有可能得到相同的hashcode值,虽然这种概率极小,但还是有这样的几率存在的;因此,hash值也有可能重复的

    final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
} h ^= k.hashCode(); // This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);

 代码分解:int i = indexFor(hash, table.length);这个就是在table数组中返回一个下标索引,table是一个Entry<K,V>[]数组

    /**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}

进入主程序:

  1、我们在使用put添加元素的时候,HashMap一开始是没有key的,因此也不存在key,table数组也不可能存在数据。put的方法会进入到addEntry(hash, key, value, i)方法中

  2、addEntry一开始,也只是进入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);
}

这个方法中有两点需要注意:

      一是链的产生。这是一个非常优雅的设计。系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。

      二、扩容问题。

随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

3、进入createEntry方法: 

   void createEntry(int hash, K key, V value, int bucketIndex) {
     // 获取指定 bucketIndex 索引处的 Entry
Entry<K,V> e = table[bucketIndex];
      // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry  
table[bucketIndex] = new Entry<>(hash, key, value, e);
     //记录HashMap的长度
size++;
}

  其实,我们根据这一条线可以发现,内部类Entry的构造方法,最后一个参数e,居然对应的是next,这不就是链表的后继节点吗?

 Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}

 

那么,第一次put的过程,我是不是可以这样理解:

   a>、首先判断key是否为null,若为null,则直接调用putForNullKey方法

   b>、然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,则直接保存。

 那么,如果同一个key、value进行第二次put怎么办呢?

仔细看看put方法,我们看到了下面这段代码,包含在put方法中的if语句块:

 //从i出开始迭代 e,找到 key 保存的位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
        
       //判断该条链上是否有hash值相同的(key相同)
//若存在相同,则直接覆盖value,返回旧value,这里并没有处理key,这就解释了HashMap中没有两个相同的key
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}  
}

  

get方法解读:

先认识一下get(key)方法的源码:

public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
     
     //在本文的一开始,我就贴出了Entry<K,V>源码,getValue()方法就是返回map中的value,可以自己去本文开始的地方看
return null == entry ? null : entry.getValue();
}

里面涉及到了getEntry(key)方法:这个方法,其实就是根据key生成的下标,到table数组中获取Entry<K,V> e值并返回

 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;
}

  

size()方法:

这个size在createEntry方法方法中已经说明过了

 public int size() {
return size;
}

  

entryKey()、keySet()、values()方法解读:

篇幅有限,其他方法留给自己去解读吧!

   这边我就重点说一下entryKey这个方法,keySet和values方法的原理与它都是一样的。

   HashMap里面保存的数据最底层是一个Entry型的数组,这个Entry则保留了一个键值对,还有一个指向下一个Entry的指针。所以HashMap是一种结合了数组和链表的结构。通过JDK的api文档我们也知道,entryKey()方法返回的是Set<Map.Entry<K,V>>的类型,因此我们可以通过迭代器遍历出key/value键值对了,那么底层究竟值怎么做的呢?

  源码解读:

//一级方法,未做任何逻辑判断,直接对entrySet0方法进行调用
public Set<Map.Entry<K,V>> entrySet() {
return entrySet0();
}
  
  //此方法知识返回一个Set<Map.Entry<K,V>>类型的es,。从方法中我们知道,entrySet有可能是一个默认值,也有可能是通过(entrySet = new EntrySet())方法生成的
private Set<Map.Entry<K,V>> entrySet0() {
 
//直接点击entry方法进去查看,发现是抽象类HashIterator<E>中的private transient Set<Map.Entry<K,V>> entrySet = null;其中并未中任何的初始化
Set<Map.Entry<K,V>> es = entrySet;
     
//因此,我判断第一次调用entrySet()方法的时候,是通过new方法生成的,下面去查看EntrySet类的源码
return es != null ? es : (entrySet = new EntrySet());
}   //EntrySet类型只有一个默认的构造方法,并且继承了AbstractSet<Map.Entry<K,V>>抽象类,跟进去以后发现:
  //AbstractSet<E> extends AbstractCollection<E> implements Set<E>是一个继承了Set接口,那么最终EntrySet自然也就是一个Set类型的实现类了,并且它重新了Set接口的几个方法,
//源码如下,这样的话我们调用entrySet()方法的时候,其实就是返回了实现Set接口的EntrySet的一个实例,并且这个实例重新了Set接口的方法,尤其是iterator()方法 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();
}
}

我们平时用entrySet()遍历map,一般都是这样做的:

      HashMap map = new HashMap();
map.put("j1", "k1");
map.put("j1", "k2");
map.put("j3", "k3"); Set<Map.Entry<String,String>> set = map.entrySet();
for(Iterator iter = set.iterator(); iter.hasNext();)
{
Map.Entry<String,String> entry = (Entry<String, String>) iter.next();
System.out.println("key :" + entry.getKey() + " , value : " + entry.getValue());
}

而此处使用set的迭代器方法iterator,此时的set是EntrySet的一个实例,因此此处的iterator()方法具体实现为:

   private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public Iterator<Map.Entry<K,V>> iterator() {
return newEntryIterator();
}
    ...........

 跟进去newEntryIterator()方法:

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

 再次跟进EntryIterator类的实例:发现原来是重写了next()方法

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

那么我们继续跟进nextEntry()方法:

      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;
}

至此,我们发现,原来我们在自己的代码中调用iterator()方法,最终在底层返回的是Entry<K,V> e类的实例。返回的接口并没有实例化需要返回的参数,而是在调用返回的set实例的iterator()方法才初始化需要返回的Entry<K,V>类型,而我在本文一开始的地方,就将Entry<K,V>类源码就贴出来了,细心的朋友可以能已经发现,此类已经提供了直接返回key、value的方法了,因此我们可以直接调用。

一个不得不说的方法,remove(Key)方法:

 public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}

这个方法没干什么事情,只是简单的调用了removeEntryForKey(Key)方法了,下面看看这个方法干了什么事情:

final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
} return e;
}

     这个方法我最想提起的是上面标红的部分: Entry<K,V> prev = table[i];Entry<K,V> e = prev;  那下面再判断 if (prev == e){     table[i] = next; }有意思吗?请大家思考,这个地方判断有意思吗?有意义吗?

       这个地方,一开始我也不是很明白,感觉这不就是一个指针么,实例e指向了prev了,那么prev == e这个逻辑应该是恒成立的才对呀?

       其实,因为HashMap的底层实现是链表,而链表的插入和删除的实现思路,在上面的说链表的时候已经提起了,这里需要指出的是Entry<K,V> prev = table[i];这其实是table数组的一个元素而已,但是这个元素本身底层却是一个Entry<Key,Value>类型的链表,也就是说可能有很多元素。那么我们在定义一个节点Entry<K,V> e = prev 指向perv,其实只是指向prev链表的第一个节点;如果prev == e,那就说明此处的链表仅有一个节点,而且这个节点没有后继节点。否则,prev肯定不等于e。

 

 

 

  

  

  

 

上一篇:java中的IO操作总结


下一篇:Java NIO学习系列六:Java中的IO模型