Java 容器的相关API在笔试题和Leetcode面试题中用得比较多。
2、ArrayList 和 LinkedList 的区别?
ArrayList: 底层是基于数组实现的,查找快,增删较慢;
LinkedList: 底层是基于链表实现的。确切的说是循环双向链表(JDK1.6 之前是双向循
环链表、1.7 之后取消了循环),查找慢、增删快。LinkedList 链表由一系列表项连接而
成,一个表项包含 3 个部分:元素内容、前驱表和后驱表。链表内部有一个 header 表
项,既是链表的开始也是链表的结尾。header 的后继表项是链表中的第一个元素,
header 的前驱表项是链表中的最后一个元素。
3、ArrayList 的扩容机制?
https://juejin.im/post/5d42ab5e5188255d691bc8d6
1、当使用 add 方法的时候首先调用 ensureCapacityInternal 方法,传入 size+1 进去,
检查是否需要扩充 elementData 数组的大小;
2、newCapacity = 扩充数组为原来的 1.5 倍(不能自定义),如果还不够,就使用它指定要
扩充的大小 minCapacity ,然后判断 minCapacity 是否大于
MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8) ,如果大于,就取 Integer.MAX_VALUE
3、扩容的主要方法:grow
3、ArrayList 中 copy 数组的核心就是 System.arraycopy 方法,将 original 数组的所有
数据复制到 copy 数组中,这是一个本地方法。
要点:1.5倍扩容,有一个拷贝过程,底层得拷贝,效率更快。
5、HashMap相关【重点】
1、数据结构
Jdk1.7:Entry数组 + 链表
Jdk1.8:Node 数组 + 链表/红黑树,当链表上的元素个数超过 8 个并且数组长度 >= 64
时自动转化成红黑树,节点变成树节点,以提高搜索效率和插入效率到 O(logN)。entry
和 Node 都包含 key、value、hash、next 属性。
2、get 和 put 方法流程
(1)put方法的流程:
当我们想往一个 HashMap 中添加一对 key-value 时,系统首先会计算 key 的 hash
值,然后根据 hash 值确认在 table 中存储的位置。若该位置没有元素,则直接插入。否
则迭代该处元素链表并依次比较其 key 的 hash 值。如果两个 hash 值相等且 key 值相等
(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的 Entry 的 value
覆盖原来节点的 value。如果两个 hash 值相等但 key 值不等 ,则将该节点插入该链表的
链头。
(2)get方法的流程
通过 key 的 hash 值找 到在 table 数组中的索引处的 Entry,然后返回该 key 对应的
value 即可。
在这里能够根据 key 快速的取到 value 除了和 HashMap 的数据结构密不可分外,还
和 Entry 有莫大的关系。HashMap 在存储过程中并没有将 key,value 分开来存储,而
是当做一个整体 key-value 来处理的,这个整体就是Entry 对象。同时 value 也只相当于
key 的附属而已。在存储的过程中,系统根据 key 的 hashcode 来决定 Entry 在 table 数
组中的存储位置,在取的过程中同样根据 key 的 hashcode 取出相对应的 Entry 对象
(value 就包含在里面)。
3、resize 方法【重点】
有两种情况会调用 resize 方法:
1、第一次调用 HashMap 的 put 方法时,会调用 resize 方法对 table 数组进行初始
化,如果不传入指定值,默认大小为 16。
2、扩容时会调用 resize,即 size > threshold 时,table 数组大小翻倍。
每次扩容之后容量都是翻倍。扩容后要将原数组中的所有元素找到在新数组中合适的
位置。
当我们把 table[i] 位置的所有 Node 迁移到 newtab 中去的时候:这里面的 node 要
么在 newtab 的 i 位置(不变),要么在 newtab 的 i + n 位置。也就是我们可以这样处
理:把 table[i] 这个桶中的 node 拆分为两个链表 l1 和 l2:如果 hash & n == 0,那么
当前这个 node 被连接到 l1 链表;否则连接到 l2 链表。这样下来,当遍历完 table[i] 处
resize方法的逻辑:
的所有 node 的时候,我们得到两个链表 l1 和 l2,这时我们令 newtab[i] = l1,newtab[i
+ n] = l2,这就完成了 table[i] 位置所有 node 的迁移(rehash),这也是 HashMap 中
容量一定的是 2 的整数次幂带来的方便之处。
要点,HashMap是两倍扩容。容量是二次幂的好处
4、size 必须是 2 的整数次方原因?
这样做总是能够保证 HashMap 的底层数组长度为 2 的 n 次方。当 length 为 2 的 n
次方时,h & (length - 1) 就相当于对 length 取模,而且速度比直接取模快得多,这是
HashMap 在速度上的一个优化。而且每次扩容时都是翻倍。
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。
4、HashMap 多线程死循环问题
主要是多线程同时 put 时,如果同时触发了 rehash 操作,会导致 HashMap 中的链
表中出现循环节点,进而使得后面 get 的时候,会死循环。
5、影响 HashMap 的性能因素
key 的 hashCode 函数实现、loadFactor、初始容量
6、HashMap 中 key 的 hash 值计算方法以及原因
对
对于 HashMap 的 table 而言,数据分布需要均匀(最好每项都只有一个元素,这样
就可以直接找到),不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。在
HashMap 内部计算是使用 indexFo r方法来计算 key 的 hash 值,该方法只有一条语句:
h & (length - 1),该方法相当于 length 的取模运算,加快运算速度,并可以均匀分布
table数据和充分利用空间。
8、table[i] 位置的链表什么时候会转变成红黑树?
在 JDK1.8 中,Node 数组中每一个桶中存储的是 Node 链表,当链表长度 >= 8 的
时候并且 Node 数组的大小 >= 64,链表会变为红黑树结构【因为红黑树的增删改查复杂
度是 O(logn),链表是 O(n),红黑树结构比链表代价更小】。
9、HashMap 主要成员属性
threshold、loadFactor、HashMap 的懒加载(第一次用到的时候才对 map进行初
始化)。
10、HashMap 的 get 方法能否判断某个元素是否在 map 中?
HashMap 的 get 函数的返回值不能判断一个 key 是否包含在 map 中,因为 get 返
回 null 有可能是不包含该 key,也有可能该 key 对应的 value 为 null。因为 HashMap
中允许 key 为 null,也允许 value 为 null。
11、HashMap 线程安全吗,哪些环节最有可能出问题,为什么?
这里先简要回答这个问题:HashMap 的 put 操作是不安全的,因为没有使用任何
锁。HashMap 在多线程下最大的安全隐患发生在扩容的时候。想想一个场合:HashMap
使用默认容量 16,这时 100 个线程同时往 HashMap 中 put 元素,会发生什么?扩容混
乱,因为扩容也没有任何锁来保证并发安全。
HashMap 在 JDK1.8 中的优化?
1、resize 时不需要重新计算 hash,只要看看原来的 hash 新增的那个 bit 是 0 还是 1。0
的话索引没变,1 的话索引变成原索引加 oldcap。找到新数组下标,确定索引位置,增加
随机性;
2、不会形成闭环,扩容时不再使用头插法改成尾插法。
HashTable
HashTable 和 HashMap 的实现原理几乎一样,差别无非是
1、HashTable 不允许 key 和 value 为 null;
2、HashTable 是线程安全的。但是 HashTable 线程安全的策略实现代价却太大了,简单
粗暴,get/put 所有相关操作都是 synchronized 的,这相当于给整个哈希表加了一把大
锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于
将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
ConcurrentHashMap
HashTable 性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,
每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样
便可以有效地提高并发效率。这就是 ConcurrentHashMap 所采用的"分段锁"思想。
7、HashSet 的底层实现是什么?
put 方法
get 方法
关于 segmentShift 和 segmentMask
通过看源码知道 HashSet 的实现是依赖于 HashMap 的,HashSet 的值都是存储在
HashMap 中的。在 HashSet 的构造法中会初始化一个 HashMap 对象,HashSet 不允许
值重复。因此,HashSet 的值是作为 HashMap 的 key 存储在 HashMap 中的,当存储的
值已经存在时返回 false。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}8、LinkedHashMap 的实现原理?
LinkedHashMap 也是基于 HashMap 实现的,不同的是它定义了一个 Entry
header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 通
过继承 hashMap 中的 Entry,并添加两个属性 Entry before,after 和 header 结合起
来组成一个双向链表,来实现按插入顺序或访问顺序排序。LinkedHashMap 定义了排序
模式 accessOrder,该属性为 boolean 型变量,对于访问顺序,为 true;对于插入顺
序,则为 false 。 一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。