了解HashMap源码

前言

   在写项目的时候用到过HashMap,在这里先引一下总会动到的方法

  • HashSet: 采用 Hashmap 的 key 来储存元素,主要特点是无序的,基本操作都是 O(1) 的时间复杂度,很快。
  • LinkedHashSet: 这个是一个 HashSet + LinkedList 的结构,特点就是既拥有了 O(1) 的时间复杂度,又能够保留插入的顺序。
  • TreeSet: 采用红黑树结构,特点是可以有序,可以用自然排序或者自定义比较器来排序;缺点就是查询速度没有 HashSet 快。

  已经会用了,就看底层,琢磨底层,然后就写出了这篇,以我的见解写出的HashMap,在我这边我都是全文背诵。

hash是什么?

  Hash,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。也牵扯到摘要算法。

  在JDK1.8 之前 HashMap 由 数组和链表 组成的,数组是 HashMap 的主体,链表主要为了解决哈希冲突而存在的。

 

基本概念:

  假定哈希函数将元素正确分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。

   HashMap 的实例有两个参数影响其性能:初始容量加载因子;初始容量(16);默认加载因子 (0.75)。

   那么一个桶里边如果出现了相同的哈希函数,会怎样?这种现象称为哈希碰撞,哈希碰撞就是有限的值碰到了无限的输出。

扩容的条件,什么时候扩容?

  当达到阈值并不会立即扩容,还要一个条件是存在Hash碰撞才会扩容。

hash的特点

  1. 任意长度的输入,得到固定的输出
  2. 不可逆,可以把原文计算成密文,但不能从密文推回原文。
  3. 算法不固定,只要满足hash的思想就是hash算法。加密领域的常见摘要算法有md5,sha256等。
  4. 快速检索,体现在:比较一篇文章和其他一万篇文章是否相同,一行一行去看太慢了,做个哈希转换成某些数字去比较会更快。
  5. 防篡改。防篡改,字面意思防止篡改,在网络中加密发送后,发送数据时会把原文加密后把原文和密文一起发给对方,对方收到后,先对原文做个加密,如果密文和收到的一样说明内容没被改过。
  6. 密码保存。做过项目的都知道保存用户的密码都是原文展示在数据库中,这样便于查看,但不安全。这里就体现了密码保护。

hash的构成及特点

  HashMap的数据结构是数组+链表+红黑树,这里提一下基本的数据结构:链表、数组、队列、栈、树、图。

  特点:分为两个版本其中jdk1.7中插入数据采用的是头插法,也就是新来的元素会加在链表的开头,可能会引起空指针,类似于栈,后来居上。因为开发者认为后加的元素可能被用到的几率更大,所以头插法可以快速查询。1.8的实现在1.7的基础上,增加了红黑树,HashMap的底层数据结构为数组+链表+红黑树;

  为什么1.8要在1.7的版本基础上加红黑树嘞?

  因为发现不管扩容机制有多好,依然会出现大量链表导致查询效率低下,所以在插入时,依然按照链表插入,这里不同于1.7,1.8里的插入时插入在尾部。主要是为了提高HashMap的性能;当没有冲突的时候放在数组中,当冲突<8放在链表中,当>8的时候放在红黑树中, 时间复杂读从o(n)降到了o(logn)。

  • 1.7 数组特点:连续存储的空间,查询快,增删慢。
  • 1.7链表特点:不连续的区域,每个节点放值和指向下一个节点的指针查询慢,增删快。
  • 1.8引用红黑树;自平衡的二叉树;自平衡的二叉树就是任意节点的左右两个子树高度差都小于等于1,这样便利起来会更均匀。

在前言中提到过得时间复杂度,

没有发生碰撞时间复制度01,只需要查询一次,当时链表的时候0n,采用红黑树就是0(logn),Haximap的底层存放没有顺序。


 

这里看下源码

 1 // 默认容量
 2 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 3 // 默认加载因子
 4 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 5 // 最大容量
 6 static final int MAXIMUM_CAPACITY = 1 << 30;
 7 // 定义一个空数组
 8 static final Entry<?,?>[] EMPTY_TABLE = {};
 9 // 存储键值对的数组,默认为空数组,根据需要进行扩容,长度必须是2的整数幂
10 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
11 // 容量*加载因子,根据此判断是否需要扩容
12 int threshold;
13 // map中的键值对个数
14 transient int size;
15 // 此 HashMap 已在结构上修改的次数。结构修改是指更改 HashMap 中的映射数量或以其他方式修改其内部结构(例如,重新散列)的那些。该字段用于使 HashMap 的 Collection-views 上的迭代器快速失败。 (请参阅 ConcurrentModificationException)。
16 // 结构上的修改一般来说就是添加和删除
17 transient int modCount;
基本参数

在这里会考验到你的二进制,研发出哈希的前辈,真的很强

 1 //指定容量大小的构造函数
 2 public HashMap(int initialCapacity) {
 3     this(initialCapacity, DEFAULT_LOAD_FACTOR);
 4 }
 5 
 6 /*
 7      指定“容量大小”和“加载因子”的构造函数
 8      initialCapacity: 指定的容量
 9      loadFactor:指定的加载因子
10 */
11 public HashMap(int initialCapacity, float loadFactor) {
12     //判断初始化容量initialCapacity是否小于0
13     if (initialCapacity < 0)
14         //如果小于0,则抛出非法的参数异常IllegalArgumentException
15         throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
16     
17     //判断初始化容量initialCapacity是否大于集合的最大容量MAXIMUM_CAPACITY-》2的30次幂
18     if (initialCapacity > MAXIMUM_CAPACITY)
19         //如果超过MAXIMUM_CAPACITY,会将MAXIMUM_CAPACITY赋值给initialCapacity
20         initialCapacity = MAXIMUM_CAPACITY;
21     
22     //判断负载因子loadFactor是否小于等于0或者是否是一个非数值
23     if (loadFactor <= 0 || Float.isNaN(loadFactor))
24         //如果满足上述其中之一,则抛出非法的参数异常IllegalArgumentException
25         throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
26     
27     //将指定的加载因子赋值给HashMap成员变量的负载因子loadFactor
28     this.loadFactor = loadFactor;
29     /*
30             tableSizeFor(initialCapacity) 判断指定的初始化容量是否是2的n次幂,如果不是那么会变为比指定初始化容量大的最小的2的n次幂。
31             但是注意,在tableSizeFor方法体内部将计算后的数据返回给调用这里了,并且直接赋值给threshold边界值了。有些人会觉得这里是一个bug,应该这样书写:
32             this.threshold = tableSizeFor(initialCapacity) *this.loadFactor;
33             这样才符合threshold的意思(当HashMap的size到达threshold这个阈值时会扩容)。
34             但是,请注意,在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算
35         */
36     this.threshold = tableSizeFor(initialCapacity);
37 }
38 
39 /**
40       Returns a power of two size for the given target capacity.
41       返回比指定初始化容量大的最小的2的n次幂
42 */
43 static final int tableSizeFor(int cap) {
44     int n = cap - 1;
45     n |= n >>> 1;
46     n |= n >>> 2;
47     n |= n >>> 4;
48     n |= n >>> 8;
49     n |= n >>> 16;
50     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
51 }
构造函数

接着是方法

  1     public V put(K key, V value) {
  2         if (table == EMPTY_TABLE) {
  3             // 如果是空数组,根据容量进行初始化,
  4             inflateTable(threshold);
  5         }
  6         if (key == null)
  7             // 有则更新,无则新增,下标为0
  8             return putForNullKey(value);
  9         // 根据key取hash值
 10         int hash = hash(key);
 11         // 根据hash值求取下标
 12         int i = indexFor(hash, table.length);
 13         // 如果存在旧值,就更新并返回旧值
 14         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
 15             Object k;
 16             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
 17                 V oldValue = e.value;
 18                 e.value = value;
 19                 e.recordAccess(this);
 20                 return oldValue;
 21             }
 22         }
 23  
 24         modCount++;
 25         // 新增一个
 26         addEntry(hash, key, value, i);
 27         return null;
 28     }
 29  
 30      private void inflateTable(int toSize) {
 31         // 容量是2的整数幂
 32         int capacity = roundUpToPowerOf2(toSize);
 33  
 34         threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
 35         table = new Entry[capacity];
 36         initHashSeedAsNeeded(capacity);
 37     }
 38  
 39     final boolean initHashSeedAsNeeded(int capacity) {
 40         boolean currentAltHashing = hashSeed != 0;
 41         boolean useAltHashing = sun.misc.VM.isBooted() &&
 42                 (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
 43         boolean switching = currentAltHashing ^ useAltHashing;
 44         if (switching) {
 45             // 最后结果是 hashSeed=0
 46             hashSeed = useAltHashing
 47                 ? sun.misc.Hashing.randomHashSeed(this)
 48                 : 0;
 49         }
 50         return switching;
 51     }
 52  
 53      private V putForNullKey(V value) {
 54         for (Entry<K,V> e = table[0]; e != null; e = e.next) {
 55             if (e.key == null) {
 56                 V oldValue = e.value;
 57                 e.value = value;
 58                 e.recordAccess(this);
 59                 return oldValue;
 60             }
 61         }
 62         modCount++;
 63         addEntry(0, null, value, 0);
 64         return null;
 65     }
 66     // 原理(扰动函数),尽可能使生成的hash值分布均匀,随机,避免冲突
 67     final int hash(Object k) {
 68         int h = hashSeed;
 69         if (0 != h && k instanceof String) {
 70             return sun.misc.Hashing.stringHash32((String) k);
 71         }
 72  
 73         h ^= k.hashCode();
 74  
 75         // This function ensures that hashCodes that differ only by
 76         // constant multiples at each bit position have a bounded
 77         // number of collisions (approximately 8 at default load factor).
 78         h ^= (h >>> 20) ^ (h >>> 12);
 79         return h ^ (h >>> 7) ^ (h >>> 4);
 80     }
 81  
 82     // 当length始终为2的n次方时,h&(length-1)等价于h%length
 83     static int indexFor(int h, int length) {
 84         // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
 85         return h & (length-1);
 86     }
 87  
 88     void addEntry(int hash, K key, V value, int bucketIndex) {
 89         if ((size >= threshold) && (null != table[bucketIndex])) {
 90             resize(2 * table.length); // 当size大于容量*负载因子的时候进行扩容
 91             hash = (null != key) ? hash(key) : 0;
 92             bucketIndex = indexFor(hash, table.length);
 93         }
 94  
 95         createEntry(hash, key, value, bucketIndex);
 96     }
 97     // 新增一个entry
 98     void createEntry(int hash, K key, V value, int bucketIndex) {
 99         Entry<K,V> e = table[bucketIndex];
100         table[bucketIndex] = new Entry<>(hash, key, value, e);
101         size++;
102     }
103  
104     // 扩容
105     void resize(int newCapacity) {
106         Entry[] oldTable = table;
107         int oldCapacity = oldTable.length;
108         if (oldCapacity == MAXIMUM_CAPACITY) {
109             threshold = Integer.MAX_VALUE;
110             return;
111         }
112         // 新建一个数组,进行元素转移
113         Entry[] newTable = new Entry[newCapacity];
114         transfer(newTable, initHashSeedAsNeeded(newCapacity));
115         table = newTable;
116         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
117     }
118     // 将旧数组的元素转移到新数组
119     void transfer(Entry[] newTable, boolean rehash) {
120         int newCapacity = newTable.length;
121         for (Entry<K,V> e : table) {
122             while(null != e) {
123                 Entry<K,V> next = e.next;
124                 if (rehash) {
125                     e.hash = null == e.key ? 0 : hash(e.key);
126                 }
127                 int i = indexFor(e.hash, newCapacity);
128                 // 这里采用了“头插法”,相当于倒序插入
129                 // 假如原来的链表为 a->b->c->d->null
130                 // 转移后的新的链表为 d->c->b->a->null
131                 e.next = newTable[i];
132                 newTable[i] = e;
133                 e = next;
134             }
135         }
136     }
put方法

1.首先获取Node数组table对象和长度,若table为null或长度为0,则调用resize()扩容方法获取table最新对象,并通过此对象获取长度大小

2.判定数组中指定索引下的节点是否为Null,若为Null 则new出一个单向链表赋给table中索引下的这个节点

3.若判定不为Null,我们的判断再做分支
3.1 首先对hash和key进行匹配,若判定成功直接赋予e

3.2 若匹配判定失败,则进行类型匹配是否为TreeNode 若判定成功则在红黑树中查找符合条件的节点并将其回传赋给e

3.3 若以上判定全部失败则进行最后操作,向单向链表中添加数据若单向链表的长度大于等于8,则将其转为红黑树保存,记录下一个节点,对e进行判定若成功则返回旧值

4.最后判定数组大小需不需要扩容

 1 //重新设置table大小/扩容 并返回扩容的Node数组即HashMap的最新数据
 2 final Node<K,V>[] resize() {
 3         Node<K,V>[] oldTab = table; //table赋予oldTab作为扩充前的table数据
 4         int oldCap = (oldTab == null) ? 0 : oldTab.length; 
 5         int oldThr = threshold;
 6         int newCap, newThr = 0;
 7         if (oldCap > 0) {
 8              //判定数组是否已达到极限大小,若判定成功将不再扩容,直接将老表返回
 9             if (oldCap >= MAXIMUM_CAPACITY) {
10                 threshold = Integer.MAX_VALUE;
11                 return oldTab;
12             }
13              //若新表大小(oldCap*2)小于数组极限大小 并且 老表大于等于数组初始化大小
14             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
15                      oldCap >= DEFAULT_INITIAL_CAPACITY)
16                 //旧数组大小oldThr 经二进制运算向左位移1个位置 即 oldThr*2当作新数组的大小
17                 newThr = oldThr << 1; // double threshold
18         }
19          //若老表中下次扩容大小oldThr大于0
20         else if (oldThr > 0)
21             newCap = oldThr;  //将oldThr赋予控制新表大小的newCap
22         else { //若其他情况则将获取初始默认大小
23             newCap = DEFAULT_INITIAL_CAPACITY;
24             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
25         }
26         //若新表的下表下一次扩容大小为0
27         if (newThr == 0) {  
28             float ft = (float)newCap * loadFactor;  //通过新表大小*负载因子获取
29             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
30                       (int)ft : Integer.MAX_VALUE);
31         }
32         threshold = newThr; //下次扩容的大小
33         @SuppressWarnings({"rawtypes","unchecked"})
34             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
35         table = newTab; //将当前表赋予table
36         if (oldTab != null) { //若oldTab中有值需要通过循环将oldTab中的值保存到新表中
37             for (int j = 0; j < oldCap; ++j) {
38                 Node<K,V> e;
39                 if ((e = oldTab[j]) != null) {//获取老表中第j个元素 赋予e
40                     oldTab[j] = null; //并将老表中的元素数据置Null
41                     if (e.next == null) //若此判定成立 则代表e的下面没有节点了
42                         newTab[e.hash & (newCap - 1)] = e; //将e直接存于新表的指定位置
43                     else if (e instanceof TreeNode)  //若e是TreeNode类型
44                         //分割树,将新表和旧表分割成两个树,并判断索引处节点的长度是否需要转换成红黑树放入新表存储
45                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
46                     else { // preserve order
47                         Node<K,V> loHead = null, loTail = null; //存储与旧索引的相同的节点
48                         Node<K,V> hiHead = null, hiTail = null; //存储与新索引相同的节点
49                         Node<K,V> next;
50                         //通过Do循环 获取新旧索引的节点
51                         do {
52                             next = e.next;
53                             if ((e.hash & oldCap) == 0) {
54                                 if (loTail == null)
55                                     loHead = e;
56                                 else
57                                     loTail.next = e;
58                                 loTail = e;
59                             }
60                             else {
61                                 if (hiTail == null)
62                                     hiHead = e;
63                                 else
64                                     hiTail.next = e;
65                                 hiTail = e;
66                             }
67                         } while ((e = next) != null);
68                         //通过判定将旧数据和新数据存储到新表指定的位置
69                         if (loTail != null) {
70                             loTail.next = null;
71                             newTab[j] = loHead;
72                         }
73                         if (hiTail != null) {
74                             hiTail.next = null;
75                             newTab[j + oldCap] = hiHead;
76                         }
77                     }
78                 }
79             }
80         }
81         //返回新表
82         return newTab;
83     }
resize方法

1.判定数组是否已达到极限大小,若判定成功将不再扩容,直接将老表返回

2.若新表大小(oldCap2)小于数组极限大小&老表大于等于数组初始化大小 判定成功则 旧数组大小oldThr 经二进制运算向左位移1个位置 即 oldThr2当作新数组的大小

2.1. 若[2]的判定不成功,则继续判定 oldThr (代表 老表的下一次扩容量)大于0,若判定成功 则将oldThr赋给newCap作为新表的容量

2.2 若 [2] 和[2.1]判定都失败,则走默认赋值 代表 表为初次创建

3.确定下一次表的扩容量, 将新表赋予当前表

4.通过for循环将老表中德值存入扩容后的新表中

4.1 获取旧表中指定索引下的Node对象 赋予e 并将旧表中的索引位置数据置空

4.2 若e的下面没有其他节点则将e直接赋到新表中的索引位置

4.3 若e的类型为TreeNode红黑树类型

​ 4.3.1 分割树,将新表和旧表分割成两个树,并判断索引处节点的长度是否需要转换成红黑树放入新表存储

​ 4.3.2 通过Do循环 不断获取新旧索引的节点

​ 4.3.3 通过判定将旧数据和新数据存储到新表指定的位置

 

 

posted @ 2022-07-22 18:46  凤梨小屋  阅读(52)  评论(0)    收藏  举报