• 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的查找效率降低。需要使用接从中权衡利弊。

  • 相关阅读:
    Erlang 督程 启动和结束子进程
    cocos2d-x 3.0 内存管理机制
    c语言基本数据类型
    4星|《剑桥中国经济史:古代到19世纪》:经济学视角看中国古代史
    孟晚舟三种结局;共享单车大败局;失业潮不会来:4星|《财经》2018年第30期
    2018左其盛差评榜,罕见的差书榜
    2018左其盛好书榜,没见过更好的榜单
    罗振宇时间的朋友2018跨年演讲中最重要的35句话
    中国土地制度与房价走势相关9本书
    2星|水木然《世界在变软》:肤浅的朋友圈鸡汤文
  • 原文地址:https://www.cnblogs.com/yanlong300/p/8073698.html
Copyright © 2020-2023  润新知