zoukankan      html  css  js  c++  java
  • HashMap

    HashMap是Map接口的实现类,以key-value存储形式存储数据。HashMap的操作不是同步的,所以线程不安全。

    特点:

    无序性 : 存入取出元素顺序不一致

    唯一性 : key唯一

    可存null : 键和值都可以为null,键只能有一个为null

    数据结构 : 数据结构控制的是key而非值value

    HashMap类的继承关系

    说明:
    Cloneable 空接口,表示可以克隆。 创建并返回HashMap对象的一个副本。

    Serializable 序列化接口。属于标记性接口。HashMap对象可以被序列化和反序列化。

    AbstractMap 父类提供了Map实现接口。以最大限度地减少实现此接口所需的工作。

    补充:HashMap已经继承了AbstractMap而 AbstractMap类实现了Map接口,为什么HashMap还要在实现Map接口呢?同样在ArrayList中 LinkedList中都是这种结构。

     据java集合框架的创始人Josh Bloch描述,这样的写法是一个失误。最开始他认为这样写在某些地方可能是有价值的。在java集合框架中,类似这样的写法很多。

    JDK的维护者不认为这个小小的失误值得去修改,所以就这样存在下来了。

    HashMap原理分析 

    什么是哈希表?
    哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。

    它通过把关键码值映射到表中一个位置来访问记录,以加快查找速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

    哈希表本质上是一个数组,这个数组中存储的是哈希函数算出的值。
    目的 : 为了加快数据查找的速度。

     HashMap存储数据过程 

    加载因子 : 默认值是0.75 ,决定了扩容的条件

    // 加载因子 
    final float loadFactor;

    扩容的临界值 : 计算方式为(容量 乘以 加载因子) 

    // 临界值 当实际大小超过临界值时,会进行扩容 
    int threshold;

    容量capacity : 初始化为16

    扩容resize : 达到临界值就扩容。扩容后的 HashMap 容量是之前容量的两倍 。
    集合元素个数size : 表示HashMap中键值对实时数量,不等于数组长度。

    jdk8存储过程

    存储过程源码

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
        //1.判断是否哈希表为空
        if ((tab = table) == null || (n = tab.length) == 0)        
             //2.如果为空初始化容量,16
            n = (tab = resize()).length;
         //3.如果不为空 , 则判断当前key的hash值对应的索引位置是否有元素。
         if ((p = tab[i = (n - 1) & hash]) == null)
            //4.如果没有,往当前索引位置放入一个新的节点
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k; 
           //5.如果有元素,判断当前索引位的节点hash值和equals与新key是否相等
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                //如果相等,则覆盖value
                e = p;
            //6.如果不相等,则判断是否是红黑树
            else if (p instanceof TreeNode)
                //如果是红黑树节点,则将元素存入红黑树节点
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //7.如果不相等,也不是红黑树节点,则遍历所有链表节点
                for (int binCount = 0; ; ++binCount) {
                    //如果到了后一个节点还没找到相等的节点
                    if ((e = p.next) == null) {
                        //在尾部新增一个节点
                        p.next = newNode(hash, key, value, null);
                        //8.判断链表的长度是否大于8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //如果大于8直接将链表转换为红黑树
                            treeifyBin(tab, hash);
                                            break;
                    }
                    //如果遍历的节点的hash值和equals值与新key相同,则跳出循环
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果key存在,则直接覆盖value值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //判断HashMap中节点数是否大于临界值,如果大于则扩容,是之前的两倍
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
     }

     HashMap底层数据结构 

    jdk1.8之前数据结构是:链表 + 数组

    jdk1.8之后数据结构是:链表 + 数组  + 红黑树 。单链表阈值(边界值) > 8 且数组长度大于64,才将链表转换为红黑树。 目的 : 高效查询数据

    扩展知识: 红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数 据结构,典型的用途是实现关联数组。红黑树是在1972年由Rudolf Bayer发明的,当时被称为平 衡二叉B树(symmetric binary B-trees) 

    数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。

    什么是哈希冲突?两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同。
    JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。

    JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其 实就是根据对象的hashCode和equals方法来决定的。

    如果我们往集合中存放自定义的对象,那么保证 其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。 当位于一个链表中的元素较多,即hash值相等但是内容不相等的元素较多时,通过key值依次查找的效 率较低。

    而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度(阀值)超过 8 时且当前数组 的长度 > 64时,将链表转换为红黑树,这样大大减少了查找时间。jdk8在哈希表中引入红黑树的原因只 是为了查找效率更高。
    简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。如下图所示。

     

    HashMap中哈希表的数组的大小?

    创建HashMap集合对象时

    JDK8前,构造方法创建一个长度是16的数组Entry[] table 来存储键值对的对象。   

    JDK8后,不是在构造方法中创建对象数组,而是在第一调用put方法时创建长度是16的Node[] table数组,存储Node对象

    如果节点长度即链表长度大于阈值8,并且数组长度大于64则进行将链表变为红黑树。

    数据结构的源码
    table用来初始化(必须是二的n次幂)(重点)

    //存储元素的数组
     transient Node<K,V>[] table;

    用来存缓存

    //存放具体元素的集合
     transient Set<Map.Entry<K,V>> entrySet;

    HashMap中存放元素的个数(重点)

    //存放元素的个数,注意这个不等于数组的长度。
     transient int size;

    HashMap源码分析

    初始化容量16

    //默认的初始容量是16 -- 1<<4相当于1*2的4次幂---1*16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    初始化容量必须是2的n次幂,为什么?

    向HashMap中添加元素时,要根据key的hash值去确定其在数组中的具体位置。

    HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同。

    怎么让元素均匀分配呢?

    这里用到的算法是hash&(length-1)。hash值与数组长度减一的位运算。算法本质作用是类似于取模, hash%length。

    但是计算机中直接求余效率远不如位运算。 hash%length取模效果操作等于hash&(length-1)的前提是length是2的n次幂

    如果不考虑效率问题,求余即可。就不需要长度必须是2的n次幂了。如果采用位运算,必须 是2的n次幂!

    为什么这样能均匀分布减少碰撞呢?

    2的n次幂实际就是1后面n个02的n次幂-1 实际就是n个1

    举例:位运算规则说明:按&位运算(相同位的两个数字都为1,则为1;若有一个不为1,则为0)。

    例如 : 数组长度8时候,均匀分布在数组中,哈希碰撞的几率比较小;
    求位运算结果:
    314924944 & (8-1) = 0
    00010010110001010101111110010000
    00000000000000000000000000000111
    --------------------------------------------------
    00000000000000000000000000000000 --> 结果为0
    程序员计算器求解 :
    314924944 & (8-1) = 0
    314924945 & (8-1) = 1
    314924946 & (8-1) = 2
    314924947 & (8-1) = 3
    314924948 & (8-1) = 4
    314924949 & (8-1) = 6
    314924950 & (8-1) = 7
    314924951 & (8-1) = 8
    314924952 & (8-1) = 0
    结论是:数组索引存储的数据均匀分布了,减少哈希碰撞的几率
    例如 : 数组长度10时候,没有均匀分布,碰撞几率比较大;
    程序员计算器求解 :
    314924944 & (10-1) = 0
    314924945 & (10-1) = 1
    314924946 & (10-1) = 0
    314924947 & (10-1) = 1
    314924948 & (10-1) = 0
    314924949 & (10-1) = 1
    314924950 & (10-1) = 0
    314924951 & (10-1) = 1
    314924952 & (10-1) = 0
    结论是:数据全部分布在第一个和第二个索引位置上,大大增加了哈希碰撞的几率。效率低下

    HashMap构造方法还可以手动设置初始化容量大小:

    //构造一个带指定初始容量和默认加载因子 (0.75) 的空HashMap
    HashMap(int initialCapacity) 

    如果创建 HashMap对象时,手动设置的数组长度不是2的n次幂,HashMap通过位移运算和或运算得到 离那个数最近的数字2的幂次数。

    //创建HashMap集合的对象,指定数组长度是10,不是2的幂
    HashMap hashMap = new HashMap(10);
    public HashMap(int initialCapacity) {//initialCapacity=10
      this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap(int initialCapacity, float loadFactor) {//initialCapacity=10
      if (initialCapacity < 0)
      throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
      initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
      throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
      this.loadFactor = loadFactor;
      this.threshold = tableSizeFor(initialCapacity);//initialCapacity=10
    }
    /**
    * Returns a power of two size for the given target capacity.
    */
    static final int tableSizeFor(int cap) {//int cap = 10
      int n = cap - 1;
      n |= n >>> 1;
      n |= n >>> 2;
      n |= n >>> 4;
      n |= n >>> 8;
      n |= n >>> 16;
      return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

    假如初始化容量设为10,最终容量会变为最近的16!

     小结:

    1. 根据key的hash确定存储位置时,数组长度是2的n次幂,可以保证数据的均匀插入。如果不是,会浪费数组的空间,降低集合性能!

    2. 一般情况下,我们通过求余%来均匀分散数据。只不过其性能不如位运算【&】。

    3. length的值为2的n次幂,hash & (length - 1) 作用完全等同于hash % length。

    4. HashMap中初始化容量为2次幂原因是为了数组数据均匀分布。尽可能减少哈希冲突,提升集合性能。

    5. 即便可以手动设置HashMap的初始化容量,但是最终还是会被重设为2的n次幂。

  • 相关阅读:
    Android——Room数据库版本管理(保留现有数据不丢失)
    javaweb分页查询实现
    《人月神话》读后感(一)
    Android Studio代理-build过慢以及gradle下载失败解决方案
    Room----Android数据库(SQLite)
    Android Jetpack -- Lifecycles篇
    Android学习进度四
    Android Jetpack -- Navigation 篇
    Android Jetpack -- ViewModel篇(二)
    Android Jetpack -- ViewModel篇(一)
  • 原文地址:https://www.cnblogs.com/Alwaysbecoding/p/14611694.html
Copyright © 2011-2022 走看看