内含扩容源码的面试题,目标是手写HashMap!

基础知识

java-collection-hierarchy

说说List、Set、Map三者的区别

  • List(对付顺序的好帮手):List接口存储一组不唯一(可以用多个元素引用相同的对象),有序的对象。
  • Set(注重第一无二的性质):不允许重复的集合,不会有多个元素引用相同对象。
  • Map(用key来搜索的专家):使用键值对存储,Map会维护与Key有关联的值,两个Key可以引用相同的对象,但是Key不能重复,典型的Key是String;类型,但是也可以是任何对象。
元素有序允许重复元素
List
AbstractSet
HashSet
TreeSet是(用二叉树排序)
AbstractMaoKey值必须唯一,value可重复
HashMapKey值必须唯一,value可重复
TreeMap是(用二叉树排序)Key值必须唯一,value可重复

集合框架底层数据结构总结

Collection接口下的集合

List
  • ArraylistObject[]数组。
  • VectorObject[]数组。
  • LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)。
Set
  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素。
  • LinkedHashSetLinkedHashSetHashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。
  • treeSet(有序,唯一): 红黑树(自平衡的排序二叉树)。

Map

  • HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
  • LinkedHashMapLinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
  • TreeMap: 红黑树(自平衡的排序二叉树)。

HashMap 和 Hashtable 有什么区别

  • 存储:HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException

  • 线程是否安全:HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);

  • 效率:因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它。

  • 容量:

    1. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。
    2. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小。
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

    推荐在单线程环境下使用HashMap替代,如果需要多线程使用则用ConcurrentHashMap

如何决定使用 HashMap 还是 TreeMap?

   对于在Map中插入、删除、定位一个元素这一类的操作,HashMap是最好的选择,因为相对而言HashMap的插入会更快,但是如果要对一个key集合进行有序的遍历,那么用TreeMap。

HashMap 的实现原理

​ HashMao基于Hash算法实现的,我们通过put(key,value)存储,get(key)来获取。

​ 当传入key时,HashMap会根据key,hashCode()计算出Hash值,根据Hash值将value保存在bucket里。当计算出Hash值相同时,我们称之为Hash冲突,HahsMap的做法时用链表和红黑树存储相同的Hash值的value,当Hash冲突的个数比较少的时候,使用链表,否则使用红黑树。

HashMap简介

​ jdk1.8之前HashMap是由数组+链表组成的,数组是HashMap的主要部分,链表是为了解决Hash冲突(两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用)而设立的(“拉链法”解决冲突)。

image-20201011140112004

​ jdk1.8之后改变了,当链表数组大于一定的值(红黑树的边界值,默认为8)且当前数组长度大于64时,此时索引位置上的数据改为利用红黑树进行存储。这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要逬行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对要快些。所以结上所述为了提高性能和减少搜索时间,底层阈值是链表大于8并且数组长度大于64时,链表才转换为红黑树

​ 总结,HashMap的特点:

  1. 存储无序的。
  2. 键和值位置都可以是 null,但是键位置只能存在一个 null。
  3. 键位置是唯一的,是底层的数据结构控制的。
  4. jdk1.8 前数据结构是链表+数组,jdk1.8 之后是链表+数组+红黑树。
  5. 链表阈值(边界值)> 8 并且数组长度大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。

存储数据的过程

HashMap<String, Integer> map = new HashMap<>();
	map.put("张三", 18);
	map.put("王五", 40);
	map.put("张三", 18);
	map.put("赵六", 20);
  1. 当创建 HashMap 集合对象的时候,在 jdk1.8 之前,构造方法中创建一个长度是16的 **Entry[] table ** 用来存储键值对数据的。在 jdk1.8 以后不是在 HashMap 的构造方法底层创建数组了,是在第一次调用 put 方法时创建的数组 Node[] table 用来存储键值对数据。

  2. 假设向哈希表中存储 <张三,18> 数据,根据张三调用 String 类中重写之后的 hashCode() 方法计算出哈希值,然后结合数组长度采用某种算法计算出向 Node 数组中存储数据的空间的索引值。如果计算出的索引空间没有数据,则直接将<张三,18>存储到数组中,假设我们计算出的索引值为2。

  3. 向哈希表中存储数据 <王五,40>,假设算出的 hashCode() 方法结合数祖长度计算出的索引值也是2,那么此时数组空间不是 null,此时底层会比较张三和王五的 hash 值是否一致,如果不一致,则在空间上划出一个结点来存储键值对数据对 <王五,40>,这种方式称为拉链法。

  4. 假设向哈希表中存储数据 <张三,18>,那么首先根据张三调用 hashCode() 方法结合数组长度计算出索引肯定是 2,此时比较后存储的数据张三和已经存在的数据的 hash 值是否相等,如果 hash 值相等,此时发生哈希碰撞。那么底层会调用张三所属类 String 中的 equals() 方法比较两个内容是否相等。

    ​ (1)相等:将后添加的数据的 value 覆盖之前的 value。

    ​ (2)不相等:继续向下和其他的数据的 key 进行比较,如果都不相等,则划出一个结点存储数据,如果结点长度即链表长度大于阈值 8 并且数组长度大于 64 则将链表变为红黑树。

  5. 在不断的添加数据的过程中,会涉及到扩容问题,当超出阈值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的 2 倍,并将原有的数据复制过来。

  6. 综上描述,当位于一个表中的元素较多,即 hash 值相等但是内容不相等的元素较多时,通过 key 值依次查找的效率较低。而 jdk1.8 中,哈希表存储采用数组+链表+红黑树实现,当链表长度(阈值)超过8且当前数组的长度大于64时,将链表转换为红黑树,这样大大减少了查找时间

20200628085432352

引入红黑树的原因

​ jdk1.8以前 HashMap 的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当HashMap中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有n个元素,遍历的时间复杂度就是O(n),完全失去了它的优势。

​ 针对这种情况,jdk1.8中引入了红黑树(查找时间复杂度为O(logn))来优化这个问题。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树

image-20201011142351515

HashMap的关系图

image-20201011142656991

HashMap 中的底层原理实现

JDK 1.8 之前

    JDK1.8 之前 HashMap 底层是数组 + 链表,HashMap 会使用 hashCode 以及扰动函数处理 key ,然后获取一个hash 值,然后通过 (length- 1) & hash 判断当前元素应该存放的位置,如果这个位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

JDK 1.8 之后

      JDK 8 做了一些较大的调整,当数组中每个格子里的链表,长度大于阈值(默认为8)时,将链表转化为红黑树,就可以大大的减少搜索时间,不过在转为红黑树前会判断,如果数组长度小于 64,还是会优先进行数组扩容。

HashSet 的实现原理

​ HashSet时基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet的实现比较简单,相关的HashSet的操作,基本上都是调用底层HashMap的相关方法来完成的,HashSet不允许重复的值

ArrayList 和 LinkedList 的区别

  1. 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全。

  2. 底层数据结构: Arraylist 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表数据结构。

  3. 插入和删除是否受元素位置的影响:

综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

如何实现数组和 List 之间的转换

  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法

ArrayList 和 Vector 的区别

  • 线程安全:Vector使用了Synchronized来实现线程同步,是线程安全的,而ArrayList是非线程安全的
  • 性能:ArrayList在性能要优于Vector
  • 扩容:ArrayList和Vector都会根据实际的需要动态调整容量,只不过在Vector扩容每次都会增加1倍,而ArrayList只会增加0.5倍

Array 和 ArrayList 有何区别

  • Array可以存储基本的数据类型和对象,ArrayList只能存储对象
  • Array是指定固定大小的,而ArrayList的大小是自动扩展的
  • Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有ArrayList 有

Queue 中 poll()和 remove()有什么区别

  • 相同点:都是返回第一个元素,并在队列中删除返回的对象。

  • 不同点 : 如果没有元 素poll()会返回null ,而remove()会直接抛出NoSuchElementException异常。

哪些集合类是线程安全的

​ Vector、Hashtable、Stack 都是线程安全的,而像 HashMap 则是非线程安全的,不过在 JDK 1.5 之后随着 Java. util. concurrent 并发包的出现,它们也有了自己对应的线程安全类,比如 HashMap 对应的线程安全类就是ConcurrentHashMap

迭代器 Iterator 是什么

​ Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的Enumeration,迭代器允许调用者在迭代过程中移除元素。

Iterator 怎么使用?有什么特点?

List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();while(it. hasNext()){
	String obj = it. next();
	System. out. println(obj);
}

​ Iterator的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出ConcurrentModificationException 异常

Iterator 和 ListIterator 有什么区别

  • Iterator可以遍历Set和List集合,而ListInerator只能遍历List
  • Iterator只能使用 next() 顺序的向后遍历,ListIterator则向前 previous()和向后 next() 遍历都可以。
  • ListInerator从Iterator接口继承,Iterator只能 remove() 元素,而ListIterator可以 add()set()remove()

怎么确保一个集合不能被修改

​ 可以使用 Collections. unmodifiableCollection(Collection c)方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang.UnsupportedOperationException 异常

Collection.sort排序内部原理

​ 在Java6中Arrays.sort()和Collection.sort()使用的是MergeSort(归并),而在Java7中,内部实现换成了TimSort,其对对象间比较的实现要求更加严格

请解释一下HashMap的参数loadFactor(负载因子),它的作用是什么?

​ loadFactor表示HashMap的拥挤程度,影响hash操作到同一个数组位置的概率。默认loadFactor等于0.75,当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,在HashMap的构造器中可以定制loadFactor。

comparable 和 Comparator 的区别

  • comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序。
  • comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序。

比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

  1. HashSetSet 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值。
  2. LinkedHashSetHashSet 的子类,能够按照添加的顺序遍历。
  3. TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。
HashMapHashSet
实现了 Map 接口实现 Set 接口
存储键值对仅存储对象
调用 put()向 map 中添加元素调用 add()方法向 Set 中添加元素
HashMap 使用键(Key)计算 hashcodeHashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性

HashMap 和 HashSet 区别

    HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()writeObject()readObject()HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。

HashMap 和 TreeMap 区别

TreeMap继承结构

    TreeMapHashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口。

  1. 实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。
  2. 实现SortMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序

    相比于HashMap来说 TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。

HashSet 如何检查重复

    当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相同的 hashcodeHashSet 会假设对象没有重复出现。

    但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。

HashMap 的长度为什么是 2 的幂次方

    HashMap底层使用的是哈希表(链表加数组)存储时可以通过运算后得出自己在数组中所存储的位置。int类型的数据的范围是-2147483648~2147483647,很明显这些散列值是不可以直接使用的,因为内存没办法存下一个40多亿长度的数组,所以需要对数组长度进行取模运算,再得出一个数组下标。
    在JDK中有一个方法:indexFor( ),这个方法可以获取到一个地址。

bucketIndex = indexFor(hash, table.length);
static int indexFor(int h, int length) {
    return h & (length - 1);
}

    这个计算的本质是(length-1) & hash
    &为二进制中的与运算,运算规则(两位同时为1,结果才为1,否则为0):

  1. 0&0=0
  2. 0&1=0
  3. 1&0=0
  4. 1&1=1

    为什么取模运算时我们用 & 而不用 % 呢,因为位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快,这样就导致位运算 & 效率要比取模运算 % 高很多。

    我们经过测试可以发现length % hash == (length - 1) & hash,这个公式想要成立的前提,就必须满足 length 是 2 的 n 次方

假设现在数组的长度:2 ^ 14 = 16384,且String key = "zZ1!." 的 hash 值为 115398910。
hash & (length - 1) = 115398910 & 16383 = 6398
6398 的二进制是 ‭0001100011111110‬
hash % length = 115398910 % 16384 = 6398
结果是一样的,但是&运算更快

总结:为什么HashMap的长度是2的整数次幂?

  1. 加快哈希计算:我们都知道为了找到 KEY 的位置在哈希表的哪个槽里面,需要计算 hash(KEY) % 数组长度,但是**% 计算比 & 慢很多**,所以用 & 代替 %,为了保证 & 的计算结果等于 % 的结果需要把 length 减 1(hash(KEY) & (length - 1))。

  2. 因为扩容为 2 的倍数,根据 hash 桶的计算方法,元素哈希值不变而通过 % 计算的方式会因为 length 的变化导致计算出来的 hash 桶的位置不断变化。数据一致在漂移,影响性能

  3. 减少冲突:

    • length 为偶数时:length-1 为奇数,奇数的二进制最后一位是 1,这样便保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(这取决于 hash 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性

      length = 4,length - 1 = 3, 3 的 二进制是 11
      1. 若此时的 hash 是 2,也就是 10,那么 10 & 11 = 10(偶数位置)
      2. 若此时的hash = 3,即 11 & 11 = 11 (奇数位置)
      
    • length 为奇数的时:很明显 length-1 为偶数,它的最后一位是 0,这样 hash & (length-1) 的最后一位肯定为 0,即只能为偶数,这样任何 hash 值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间

      length = 3, 3 - 1 = 2,他的二进制是 10
      10 无论与什么树进行 & 运算,结果都是偶数
      

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

  1. 底层数据结构:JDK1.7的ConcurrentHashMap 底层采用分段的数组+链表实现,JDK1.8采用殴打数据结构与HashMap1.8的结构一样,都是数组+链表+红黑树。Hashtable和JDK1.8之前的HashMap的底层数据结构是类似的,都是采用数组+链表的形式。数组是HashMap的主体,链表则是为了解决哈希冲突而存在的。

  2. 实现线程安全的方式:

    1. ConcurrentHashMap :

      • 在JDK1.7的时候,ConcurrentHashMap使用的是分段锁对整个桶数组进行分段分割(Segment)每一把锁只锁容器其中的一部分数据,多线程访问容器里不同数据段中的数据,就不会存在锁竞争,提高并发访问效率。

      image

      • JDK1.8的1时候,Java摒弃了Segment的概念,而是直接采用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。

JDK1.8的ConcurrentHashMap

  1. Hashtable:使用的是表锁,直接将整张表锁住,使用synchronized来保证线程安全,效率非常低下,当一个线程访问同步方法时,其他线程也访问同步方法,那么就会进入阻塞状态或者轮询状态。比如使用put添加元素时,另一个线程则不能使用put、get等其他方法,竞争激烈,效率低下。

hashtable

ConcurrentHashMap底层

JDK 1.7

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程访问其中一个段的数据时,其他1段的数据也能被其他线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}

一个ConcurrentHashMap 包含了一个Segment数组。Segment的结构和HashMap类似,是一种数组加链表结构,一个Segment包含了一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改的时候,必须首先获得1对应的Segment的锁。

JDK 1.8

ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构和HashMap 1.8的结构类似,都是数组+链表1+红黑树组成。JDK 11.8在链表1长度1超过了一定的阈值(8)时,将链表转化为红黑树。

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率大幅度提高。

ArrayList源码、扩容分析

源码分析

ArrayList的类声明

    ArrayList的类声明是继承一个抽象类和实现四个接口。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{ 
    // 源码具体内容... 
}
  • RandomAccess 是一个标志接口(Marker)只要 List 集合实现这个接口,就能支持快速随机访问(通过元素序号快速获取元素对象 get(int index)
  • Cloneable :实现它就可以进行克隆(clone()
  • java.io.Serializable :实现它意味着支持序列化,满足了序列化传输的条件

HashMap.java类的成员变量

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列号
    private static final long serialVersionUID = 362498820763181265L;    
    // 默认的初始容量是16,1左移四位,相当于1*2的4次方
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30; 
    // 默认的加载因子,判断数组扩容的阈值(影响扩容的一个条件)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶(bucket)上的结点数大于8时会转成红黑树;+对应的table的最小大小为64,即MIN_TREEIFY_CAPACITY ;这两个条件都满足,会链表会转红黑树,否则只是扩容。到了8以后树化的概率非常低,能不用红黑树就不用。
    static final int TREEIFY_THRESHOLD = 8; 
    // 当桶(bucket)上的结点数小于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table; 
    // 存放具体元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器
    transient int modCount;   
    // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
    int threshold;
    // 填充因子
    final float loadFactor;
}

构造方法

public HashMap() {
        //默认构造函数,赋值加载因子为默认的0.75f
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

//同时指定初始化容量 以及 加载因子, 用的很少,一般不会修改loadFactor
    public HashMap(int initialCapacity, float loadFactor) {
        //边界处理
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //初始容量最大不能超过2的30次方
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //显然加载因子不能为负数
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        //设置阈值为初始化容量的 2的n次方的值
        this.threshold = tableSizeFor(initialCapacity);
    }

继续点击tableSizeFor()方法。

// 这个方法返回一个离我们传入容量最近的且比它大的一个2的n次方的数字,0除外。
    static final int tableSizeFor(int cap) {
    //经过下面的 或 和位移 运算, n最终各位都是1。
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        //判断n是否越界,返回 2的n次方作为 table(哈希桶)的阈值
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

put方法

public V put(K key, V value) {
        //先根据key,取得hash值。 再调用上一节的方法插入节点
        return putVal(hash(key), key, value, false, true);
    }
hash方法
static final int hash(Object key) {
        int h;
    // 因为有(key == null) ? 0所以证明了key可以是1null,hash这个算法的原因是让hash散列更均匀,防止形成过多链表,甚至是红黑树.
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
putVal方法
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab存放 当前的哈希桶, p用作临时链表节点  
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果当前哈希表是空的,代表是初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            //那么直接去扩容哈希表,并且将扩容后的哈希桶长度赋值给n
            n = (tab = resize()).length;
        //如果当前index的节点是空的,表示没有发生哈希碰撞。 直接构建一个新节点Node,挂载在index处即可。
        //这里再啰嗦一下,index 是利用 哈希值 & 哈希桶的长度-1,替代模运算,用&运算的原因是因为&运输快
        if ((p = tab[i = (n - 1) & hash]) == null) 
            tab[i] = newNode(hash, key, value, null);
        else {//否则 发生了哈希冲突。
            //e
            Node<K,V> e; K k;
            //如果哈希值相等,key也相等,则是覆盖value操作
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) // 如果这个槽位上没有东西就赋值进去
                e = p;//将当前节点引用赋值给e
            else if (p instanceof TreeNode) //如果在红黑树上
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //按照红黑树的方式插入到红黑树之上
            else {//不是覆盖操作,则插入一个普通链表节点
                //遍历链表
                for (int binCount = 0; ; ++binCount) {
                    // 尾插法源码
                    if ((e = p.next) == null) {//遍历到尾部,追加新节点到尾部
                        p.next = newNode(hash, key, value, null);
                        //如果追加节点后,链表数量 >=8,则转化为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果找到了要覆盖的节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果e不是null,说明有需要覆盖的节点,
            if (e != null) { // existing mapping for key
                //则覆盖节点值,并返回原oldValue
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //这是一个空实现的函数,用作LinkedHashMap重写使用。
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //如果执行到了这里,说明插入了一个新的节点,所以会修改modCount,以及返回null。

        //修改modCount
        ++modCount;
        //更新size,并判断是否需要扩容。
        if (++size > threshold)
            resize();
        //这是一个空实现的函数,用作LinkedHashMap重写使用。
        afterNodeInsertion(evict);
        return null;
    }
treeifyBin

树化的时机:

  1. 容量>=64
  2. 链表的长度>=8

树化过程:

  1. 先把Node转化为treeNode。
  2. 调用treeify进行树化。

这两步时及其耗资源的。

/**
 * tab:元素数组,
 * hash:hash值(要增加的键值对的key的hash值)
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
 
    int n, index; Node<K,V> e;
    /*
     * 如果元素数组为空 或者 数组长度小于 树结构化的最小限制
     * MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换
     * 当一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后结果相同。(并不是因为这些key的hash值相同)
     * 因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上。
     */
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 小于64就扩容,不树化
        resize(); // 扩容
 
    // 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了
    // 根据hash值和数组长度进行取模运算后,得到链表的首节点
    else if ((e = tab[index = (n - 1) & hash]) != null) { 
        TreeNode<K,V> hd = null, tl = null; // 定义首、尾节点
        do { 
            TreeNode<K,V> p = replacementTreeNode(e, null); // 将该普通节点转换为树节点
            if (tl == null) // 如果尾节点为空,说明还没有根节点
                hd = p; // 首节点(根节点)指向 当前节点
            else { // 尾节点不为空,以下两行是一个双向链表结构
                p.prev = tl; // 当前树节点的 前一个节点指向 尾节点
                tl.next = p; // 尾节点的 后一个节点指向 当前节点
            }
            tl = p; // 把当前节点设为尾节点
        } while ((e = e.next) != null); // 继续遍历链表
 
        // 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表
 
        // 把转换后的双向链表,替换原来位置上的单向链表
        if ((tab[index] = hd) != null)
            hd.treeify(tab);//树化源代码
    }
}

扩容:resize方法

扩容的时机:

  1. size>=容量*加载因子。
final Node<K,V>[] resize() {
        //oldTab 为当前表的哈希桶
        Node<K,V>[] oldTab = table;
        //如果旧的容量为null,那么赋值为0,否则为当前哈希桶的容量 length,
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //当前的阈值 16
        int oldThr = threshold;
        //初始化新的容量和阈值为0
        int newCap, newThr = 0;
        //如果旧容量大于0就扩容
        if (oldCap > 0) {
            //如果当前容量已经到达上限
            if (oldCap >= MAXIMUM_CAPACITY) {
                //则设置阈值是2的31次方-1
                threshold = Integer.MAX_VALUE;
                //同时返回当前	的哈希桶,不再扩容
                return oldTab;
            }//否则新的容量为旧的容量的两倍。 
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)//如果旧的容量大于等于默认初始容量16
                //那么新的阈值也等于旧的阈值的两倍
                newThr = oldThr << 1; // double threshold
        }//如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;//那么新表的容量就等于旧的阈值
        else {}//如果当前表是空的,而且也没有阈值。代表是初始化时没有任何容量/阈值参数的情况               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;//此时新表的容量为默认的容量 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的阈值为默认容量16 * 默认加载因子0.75f = 12
        }
        if (newThr == 0) {//如果新的阈值是0,对应的是  当前表是空的,但是有阈值的情况
            float ft = (float)newCap * loadFactor;//根据新表容量 和 加载因子 求出新的阈值
            //进行越界修复
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //更新阈值 
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //根据新的容量 构建新的哈希桶
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //更新哈希桶引用
        table = newTab;
        //如果以前的哈希桶中有元素
        //下面开始将当前哈希桶中的所有节点转移到新的哈希桶中
        if (oldTab != null) {
            //遍历老的哈希桶
            for (int j = 0; j < oldCap; ++j) {
                //取出当前的节点 e
                Node<K,V> e;
                //如果当前桶中有元素,则将链表赋值给e
                if ((e = oldTab[j]) != null) {
                    //将原哈希桶置空以便GC
                    oldTab[j] = null;
                    //如果当前链表中就一个元素,(没有发生哈希碰撞)
                    if (e.next == null)
                        //直接将这个元素放置在新的哈希桶里。
                        //注意这里取下标 是用 哈希值 与 桶的长度-1 。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高
                        newTab[e.hash & (newCap - 1)] = e;
                        //如果发生过哈希碰撞 ,而且是节点数超过8个,转化成了红黑树(暂且不谈 避免过于复杂, 后续专门研究一下红黑树)
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
                    else { // preserve order
                        //因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位=  low位+原哈希桶容量
                        //低位链表的头结点、尾节点
                        Node<K,V> loHead = null, loTail = null;
                        //高位链表的头节点、尾节点
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;//临时节点 存放e的下一个节点
                        do {
                            next = e.next;
                            //这里又是一个利用位运算 代替常规运算的高效点: 利用哈希值 与 旧的容量,可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位
                            if ((e.hash & oldCap) == 0) {
                                //给头尾节点指针赋值
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }//高位也是相同的逻辑
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }//循环直到链表结束
                        } while ((e = next) != null);
                        //将低位链表存放在原index处,
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //将高位链表存放在新index处
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

扩容源码分析

/**
 * 增加ArrayList实例的容量,如果有必要,确保它至少可以保存由最小容量参数指定的元素数量。
 */
public void ensureCapacity(int minCapacity) {
    //如果元素数组不为默认的空,则 minExpand 的值为0,反之值为10
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        // any size if not default element table
        ? 0
        // larger than default for default empty table. It's already
        // supposed to be at default size.
        : DEFAULT_CAPACITY;
    // 如果最小容量大于已有的最大容量
    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}

/**
 * 计算最小扩容量(被调用)
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
     // 如果元素数组为默认的空
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 获取“默认的容量”和“传入参数 minCapacity ”两者之间的最大值
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

/**
 * 得到最小扩容量
 */
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}


/**
 * 判断是否需要扩容
 */
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    // 如果最小容量比数组的长度还大
    if (minCapacity - elementData.length > 0)
        // 就调用grow方法进行扩容
        grow(minCapacity);
}

/**
 * 要分配的最大数组大小
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * ArrayList 扩容的核心方法
 */
private void grow(int minCapacity) {
    // 将当前元素数组长度定义为 oldCapacity 旧容量
    int oldCapacity = elementData.length;
    // 新容量更新为旧容量的1.5倍
    // oldCapacity >> 1 为按位右移一位,相当于 oldCapacity 除以2的1次幂
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 然后检查新容量是否大于最小需要容量,若还小,就把最小需要容量当作数组的新容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 再检查新容量是否超出了ArrayList 所定义的最大容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        // 若超出,则调用hugeCapacity()
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}
	
/**
 * 比较minCapacity和 MAX_ARRAY_SIZE
 */
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟空打码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值