• 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。它们在内存中也是紧挨着的,中间没有间隙,当然就无法快速的介入,而删除后,当中就会留下空隙,自然需要时间去弥补,这就是问题所在。

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

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

      

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

      单链表的插入:

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

      

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

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

    单链表第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> 数据元素的结点就没有了上级。这样的插入操作就是失败的。需要注意

      

     单链表的删除:

      

    删除的过程中,我们要做的,其实就是一步,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;当妈妈去牵儿子的手时,这个爸爸就已经与母子两没有任何关系了。如下图:

    单链表第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结构图吧:

    再看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。

     

     

     

      

      

      

     

  • 相关阅读:
    博客
    参考博客
    KMP
    串匹配
    简单数论
    B
    各种常用函数的模板以及自己的测试数据
    header
    memcached的图形界面监控
    缓存策略
  • 原文地址:https://www.cnblogs.com/chen1-kerr/p/7586900.html
Copyright © 2020-2023  润新知