ArrayList
扩容机制
- ArrayList() 会使用长度为零的数组【无参构造】
- ArrayList(int initialCapacity) 会使用指定容量的数组 【有参构造】
- public ArrayList(Collection<? extends E> c) 会使用c的大小作为数组容量
- add(Object o) 首次扩容为 10,再次扩容为上次容量的 1.5 倍
- addAll(Collection c) 没有元素时,扩容为 Math.max(10,实际元素个数),有元素时, Math.max(原容量 1.5 倍,实际元素个数)
fail-fast 与 fail-safe
- ArrayList 是 fail-fast的典型代表,遍历的同时不能修改,尽快失败
- CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离
LinkedList
-
掌握 ArrayList 和 LinkedList 的比较
ArrayList 和 LinkedList
-
ArrayList
- 基于数组,需要连续内存
- 随机访问快 (指根据下标访问)
- 尾部插入,删除性能可以;其他部分插入,删除都会移动数据,因此性能会低
- 可以利用 CPU 缓存,局部性原理
-
LinkedList
- 基于双向链表,无需连续内存
- 随机访问慢 (指要沿着链表遍历)
- 头尾插入删除性能高
- 占用内存多
HashMap
-
初始容量16,负载因子0.75,每次扩容成之前的两倍
-
底层数据结构,1.7 和1.8 有何不同?
- 1.7 数组 + 链表,1.8 数组 + (链表 | 红黑树) 【当链表长度超过一定阈值时,会转化为红黑树,当红黑树数量减小时,会转化为链表】
-
为何要用红黑树,为何一上来不树化,树化阈值为何是8,为何会树化,何时会退化为链表?
- 红黑树用来避免 DOS 攻击,防止链表超长时性能下降,树化应当是偶然情况
- hash表的查找,更新的时间复杂度是O(1),而红黑树的查找,更新的时间复杂度是O(log2 n),TreeNode 占用空间也比普通Node的大,如非必要,尽量还是使用链表 【红黑树是防止攻击用的】
- hash 值如果足够随机,则在 hash 表内按泊松分布,在加(?负)载因子 0.75 的情况下,长度超过 8 的链表出现概率是0.00000006,选择 8 就是为了让树化几率足够小
- 树化两个条件:链表长度超过树化阈值 (大于8),并且数组长度必须 >= 64
- 退化情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
- 退化情况2:在remove树节点前,若树的root、root.left、root.right、root.left.left 有一个为null,也会退化为链表
- 红黑树用来避免 DOS 攻击,防止链表超长时性能下降,树化应当是偶然情况
-
索引如何计算?hashCode都有了,为何还要提供 hash() 方法?数组容量为何是 2 的 n 次幂?
- 计算对象的 hashCode(),再进行调用 HashMap() 的 hash() 方法进行二次哈希,最后 &(capacity - 1) 得到索引 【capacity必须是2的n次幂】
- 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
- 计算索引时,如果是 2 的 n 次幂可以使用位与运算代替取模,效率更高;扩容时 hash & oldCap(旧容量) == 0 的元素留在原来位置,否则新位置 = 旧位置 + oldCap
- 但前三点都是为了配合容量为 2 的 n 次方幂时的优化手段,例如 Hashtable 的容量就不是 2 的 n 次方幂,并不能说哪种设计更优,应该是设计者综合了各种因素,最终选择了使用 2 的 n 次幂作为容量
-
介绍一下 put 方法流程,1.7 和 1.8 有何不同?
-
HashMap 是懒惰创建数组的,首次使用才创建数组
-
计算索引 (桶下标)
-
如果桶下标还没人占用,创建 Node 站位返回
-
如果桶下标已经有人占用
- 已经是 TreeNode 走红黑树的添加或更新逻辑
- 是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
-
返回前检查容量是否超过阈值,一旦超过进行扩容
-
不同
- 链表插入节点时,1.7 是头插法,1.8 是尾插法
- 1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容
- 1.8 在扩容计算 Node 索引时,会优化
-
加载因子为何默认是0.75f?
- 在空间占用与查询时间之间取得较好的权衡
- 大于这个值,空间节省了,但链表就会比较长,影响性能
- 小于这个值,冲突减少了,但扩容就会更频繁,空间占用多
-
多线程下会有啥问题
- 扩容死链 (1.7)
- 数据错乱 (1.7,1.8)
-
key 能否为 null,作为 key 的对象有什么要求?
- HashMap 的 key 可以为 null,但 Map 的其他实现则不然
- 作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改 (不可变) 【hashcode相同不一定equals,而两个对象equals,则hashcode一定相同】
-
String 对象的 hashCode() 如何设计的,为啥每次乘的是 31?
- 目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特
- 字符串中的每个字符都可以表现为一个数字,称为 Si,其中 i 的范围是 0 ~ n - 1
- 散列公式
- 31 带入公式有较好的散列特性
- 目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特