java 1.8 HashMap的实现原理

java 1.8 HashMap的实现原理

1. hash 表

  • 数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)

  • 线性链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)

  • 二叉树: 对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。

  • 哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)

    在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的键值对会被放在同一个位桶里,当桶中元素较多时,通过key值查找的效率较低。

    而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8),时,将链表转换为红黑树,这样大大减少了查找时间。

  • HashMap: 由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好

2. HashMap几个关键成员变量

  • initialCapacity:初始容量。指的是 HashMap 集合初始化的时候自身的容量。可以在构造方法中指定;如果不指定的话,总容量默认值是 16 。需要注意的是初始容量必须是 2 的幂次方。

  • size:当前 HashMap 中已经存储着的键值对数量,即 HashMap.size()

  • loadFactor:加载因子。所谓的加载因子就是 HashMap (当前的容量/总容量) 到达一定值的时候,HashMap 会实施扩容。加载因子也可以通过构造方法中指定,默认的值是 0.75 。举个例子,假设有一个 HashMap 的初始容量为 16 ,那么扩容的阀值就是 0.75 * 16 = 12 。也就是说,在你打算存入第 13 个值的时候,HashMap 会先执行扩容。

  • threshold:扩容阀值。即 扩容阀值 = HashMap 总容量 * 加载因子。当前 HashMap 的容量大于或等于扩容阀值的时候就会去执行扩容。扩容的容量为当前 HashMap 总容量的两倍。比如,当前 HashMap 的总容量为 16 ,那么扩容之后为 32 。

3. JDK1.7中的HashMap实现原理

  • HashMap底层维护一个数组,数组中的每一项都是一个Entry,Entry是HashMap中的一个静态内部类

      static class Entry<K,V> implements Map.Entry<K,V> {
          final K key;
          V value;
          Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
          int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
    
  • HashMap的整体结构如下

  • HashMap构造器: HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值,initialCapacity默认为16,loadFactory默认为0.75。

  • 链表解决hash冲突

    1. 如果 table 数组为空时先创建数组,并且设置扩容阀值;

    2. 如果 key 为空时,调用 putForNullKey 方法特殊处理;

    3. 计算 key 的哈希值;

    4. 根据第三步计算出来的哈希值和当前数组的长度来计算得到该key在数组中的索引,其实索引最后的值就等于 hash%table.length ;如果数组的长度为16, 那么hash值为1、17、33,它们计算后(1%16、17%16、33%16)得到的数组小标都为1,这个时候就会产生hash冲突,引入链表,把1、17、33的用一个链表保存起来,这个时候Entry实体里面就是一个列表。

    5. 遍历该数组索引下的整条链表,如果之前已经有一样的 key ,那么直接覆盖value

    6. 如果该 key 之前没有,那么就进入addEntry 方法。

  • put方法

      public V put(K key, V value) {
              //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
              if (table == EMPTY_TABLE) {
                  inflateTable(threshold);
              }
             //如果key为null,存储位置为table[0]或table[0]的冲突链上
              if (key == null)
                  return putForNullKey(value);
              int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
              int i = indexFor(hash, table.length);//获取在table中的实际位置
              for (Entry<K,V> e = table[i]; e != null; e = e.next) {
              //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
                  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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
              addEntry(hash, key, value, i);//新增一个entry
              return null;
          }
    
    1. 如果 table 数组为空时先创建数组,并且设置扩容阀值;

    2. 如果 key 为空时,调用 putForNullKey 方法特殊处理;

    3. 计算 key 的哈希值;

    4. 根据第三步计算出来的哈希值和当前数组的长度来计算得到该key在数组中的索引,其实索引最后的值就等于 hash%table.length ;

    5. 遍历该数组索引下的整条链表,如果之前已经有一样的 key ,那么直接覆盖 value

    6. 如果该 key 之前没有,那么就进入 addEntry 方法。

       void addEntry(int hash, K key, V value, int bucketIndex) {
           if ((size >= threshold) && (null != table[bucketIndex])) {
               resize(2 * table.length);//当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容
               hash = (null != key) ? hash(key) : 0;
               bucketIndex = indexFor(hash, table.length);
           }
      
           createEntry(hash, key, value, bucketIndex);
       }
      

    当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍

  • get方法

      public V get(Object key) {
           //如果key为null,则直接去table[0]处去检索即可。
              if (key == null)
                  return getForNullKey();
              Entry<K,V> entry = getEntry(key);
              return null == entry ? null : entry.getValue();
       }
    

    get方法通过key值返回对应value,如果key为null,直接去table[0]处检索。我们再看一下getEntry这个方法

      final Entry<K,V> getEntry(Object key) {
              
          if (size == 0) {
              return null;
          }
          //通过key的hashcode值计算hash值
          int hash = (key == null) ? 0 : hash(key);
          //indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录
          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;
      }
    

    从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

4. 重写了equals(),为什么还要重写hashCode()呢?

hashCode是用于查找使用的,用于定位在数组中的下标,而equals是用于比较两个对象的是否相等的,我们重写一个类的equals方法,而它的hashcode不重写,将导致定位数组下标的时候,定位到不同下标,put进入一个对象,取出来的时候为null.

public class demo {
    private static class Student{
        int id;
        String userName;

        public Student(int id, String userName) {
            this.id = id;
            this.userName = userName;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()){
                return false;
            }
            Student student = (Student) o;
            //两个对象是否等值,通过id来确定
            return this.id == student.id;
        }

    }
    public static void main(String []args){
        HashMap<Student,String> map = new HashMap<Student, String>();
        Student student = new Student(1,"小明");
        //put到hashmap中去
        map.put(student,"小明");
        //get取出,从逻辑上讲应该能输出“小明”
        System.out.println("结果:"+map.get(new Student(1,"小明")));
    }
}

结果为:NULL

5. JDK1.8中的HashMap

5.1 概述

如果说成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那就不可避免的花费O(N)的查找时间,这将是多么大的性能损失。这个问题终于在JDK8中得到了解决。再最坏的情况下,链表查找的时间复杂度为O(n),而红黑树一直是O(logn),这样会提高HashMap的效率。

JDK7中HashMap采用的是位桶+链表的方式,即我们常说的散列链表的方式,而JDK8中采用的是位桶+链表/红黑树(有关红黑树请查看红黑树)的方式,也是非线程安全的。当某个位桶的链表的长度达到某个阀值的时候,这个链表就将转换成红黑树。

JDK8中,当同一个hash值的节点数大于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树。这就是JDK7与JDK8中HashMap实现的最大区别。

前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找,但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。

5.2 数组元素Node

JDK中Entry的名字变成了Node,原因是和红黑树的实现TreeNode相关联。

transient Node<k,v>[] table;//存储(位桶)的数组</k,v>  

//Node是单向链表,它实现了Map.Entry接口  
static class Node<k,v> implements Map.Entry<k,v> {  
    final int hash;  
    final K key;  
    V value;  
    Node<k,v> next;  
    //构造函数Hash值 键 值 下一个节点  
    Node(int hash, K key, V value, Node<k,v> next) {  
        this.hash = hash;  
        this.key = key;  
        this.value = value;  
        this.next = next;  
    }  
}

5.3 红黑树

//红黑树  
static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {  
    TreeNode<k,v> parent;  // 父节点  
    TreeNode<k,v> left; //左子树  
    TreeNode<k,v> right;//右子树  
    TreeNode<k,v> prev;    // needed to unlink next upon deletion  
    boolean red;    //颜色属性  
    TreeNode(int hash, K key, V val, Node<k,v> next) {  
        super(hash, key, val, next);  
    }  

    //返回当前节点的根节点  
    final TreeNode<k,v> root() {  
        for (TreeNode<k,v> r = this, p;;) {  
            if ((p = r.parent) == null)  
                return r;  
            r = p;  
        }  
    }  

参考博客:https://www.cnblogs.com/chengxiao/p/6059914.html

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值