ArrayList底层的实现原理是什么
- ArrayList底层是用动态的数组实现的
- ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
- ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组
- ArrayList在添加数据的时候
确保数组已使用长度(size)加1后足够存下下一个数据
计算数组的容量,如果当前数组已使用长度+1后的当前数组长度,则调用grow方法扩容
确保新增的数据有地方存储之后,则将新元素添加到位于size的位置
返回添加成功布尔值
ArrayList和LinkedList的区别
- 底层数据结构不同,一个是动态数组,一个是双向链表
- 性能上的差异:
- ArrayList 随机访问是 O(1),因为数组支持下标直接定位,
arr[5]底层就是首地址加偏移量,一步到位。但中间插入删除是 O(n),要把后面的元素整体挪动。 - LinkedList 随机访问是 O(n),得从头或从尾一个个遍历过去。但在头尾插入删除是 O(1),改两个指针就完事。
实际开发中,90% 以上的场景都该用 ArrayList,因为现代 CPU 对连续内存的缓存命中率远高于链表这种跳来跳去的结构。
- ArrayList 随机访问是 O(1),因为数组支持下标直接定位,
- 内存占用,前者内存数据连续节省内存,而后者还要加上两个指针,更占内存
- 两者都不安全,可以是
提问:你说 ArrayList 中间插入不一定比 LinkedList 慢,能展开讲讲吗?
回答:主要原因是 CPU 缓存的影响。ArrayList 的数据在内存里是连续存放的,访问时缓存命中率高,搬运数据用的 System.arraycopy 也是高度优化的原生方法。LinkedList 虽然插入本身是 O(1),但首先得定位到插入位置,这个遍历过程是 O(n),而且每访问一个节点就可能触发一次缓存缺失。实测在中间位置插入大量数据,ArrayList 往往比 LinkedList 更快。只有在头部频繁插入删除时,LinkedList 才有优势。
HashMap
说一下HashMap的实现原理
- 底层使用哈希表数据结构,即数组+链表+红黑表(1.8引入,链表长度大于8且数组长度大于64则会从链表转化为红黑树)
- 添加数据时,先算 Key 的 hashCode,然后用
(table.length-1) & hash算出下标位置 算出的下标位置冲突就用链表串起来,如果同一个下标的链表超过8个节点,则会转化为红黑树
HashMap的put方法的具体流程
- 判断键值对数组的table是否为空或为null,否则执行resize方法进行扩容(初始化)
- 根据键值对key计算hash值得到数组索引
- 判断table[i] == null,条件成立则新建节点添加
- 条件不成立则
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
- 判断table[i]是否为treeNode,即table[i]是否为红黑树,则直接在树中插入键值对
- 遍历table[i],链表尾部插入数据,然后判断长度是否大于8,大于8则转换为红黑树
- 插入成功后判断键值对数量是否超过了最大容量threshold(数组长度 * 0.75)如果超过就要扩容
讲一讲HashMap的扩容机制
- 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了扩容阈值(数组长度 * 0.75)
- 每次扩容的时候都是扩容之前容量的2倍
- 扩容之后会新创建一个数组需要把老数组中的数据挪到新的数组中
- 没有hash冲突的的的节点,则使用e.hash & (newCap-1)计算新数组的索引位置
- 如果是红黑树,走红黑树的添加
- 如果是链表则需要遍历链表,可能需要拆分链表,判断e.hash & oldCap 是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小的位置
HashMap的寻址算法
- 计算对象的hashCode(),再进行调用hash()方法进行二次哈希,hashCode值右移16位再异或运算,让哈希分布更均匀
- 最后(capacity-1)& hash 得到索引
为何HashMap的数组长度一定是2的次幂
- 计算索引的时效率更高:如果是2的n次幂可以使用位与运算代替取模
- 扩容时重新计算索引效率更高:hash & oldCap == 0的元素留在原地,
否则新位置=旧位置+oldCap