【常见集合】ArrayList与LinkedList
1 ArrayList
1.1 底层原理
数组(Array)是一种用连续的内存空间存储相同数据类的线性数据结构
1.2 寻址公式
数组是如何获取其他元素地址的呢
寻址公式:a[i] = baseAddress + i * dataTypeSize
baseAddress : 数组首地址
dataTypeSize :数组中元素类型大小
思考:为什么数组中元素下标从0开始?
如果不是从0开始而是1的话,寻址公式将变成:a[i] = baseAddress + ( i - 1) * dataTypeSize
此时对CPU来说多了一个减法指令,会影响性能
1.3 操作数组的时间复杂度
1.3.1 随机查找
即根据索引查找,时间复杂度为 O(1)
1.3.2 未知索引查找
遍历查找(未排序):时间复杂度为O(n)
二分查找(已排序):时间复杂度为O(logn)
1.3.3 插入、删除
时间复杂度为O(n)
2 ArrayList
2.1ArrayList源码分析(jdk17)
2.1.1 成员变量
/*** 默认初始容量为10,无参构造时首次添加元素会以此值初始化*/
private static final int DEFAULT_CAPACITY = 10;/*** 指定初始容量为0时使用的空数组实例*/
private static final Object[] EMPTY_ELEMENTDATA = {};/*** 无参构造时使用的空数组实例,与EMPTY_ELEMENTDATA区分以实现首次添加元素时扩容至默认容量*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/*** 存储元素的底层数组,容量为该数组的长度* 当elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,首次添加元素会扩容至DEFAULT_CAPACITY* 用transient修饰避免自动序列化,采用自定义序列化逻辑* 非private修饰是为了方便嵌套类访问*/
transient Object[] elementData; /*** ArrayList中实际包含的元素数量(与数组容量elementData.length区分)*/
private int size;
2.1.2 构造方法
// 带初始容量参数的构造方法
public ArrayList(int initialCapacity) {if (initialCapacity > 0) {// 当初始容量大于0时,创建指定容量的Object数组this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {// 当初始容量等于0时,使用EMPTY_ELEMENTDATA空数组this.elementData = EMPTY_ELEMENTDATA;} else {// 当初始容量为负数时,抛出非法参数异常throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}
}/*** 无参构造方法,创建一个初始容量为10的空列表* 注意:实际初始化会延迟到首次添加元素时*/
public ArrayList() {// 使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组作为初始值// 与EMPTY_ELEMENTDATA的区别是首次添加元素时会自动扩容至DEFAULT_CAPACITY(10)this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}/*** 通过集合创建ArrayList的构造方法* @param c 被用来初始化ArrayList的源集合,其元素类型需是E的子类*/
public ArrayList(Collection<? extends E> c) {// 将源集合转换为数组Object[] a = c.toArray();// 将源集合的元素数量赋值给当前ArrayList的sizeif ((size = a.length) != 0) {// 如果源集合是ArrayList类型if (c.getClass() == ArrayList.class) {// 直接复用源集合的内部数组(优化:避免额外复制)elementData = a;} else {// 非ArrayList集合则通过Arrays.copyOf()复制元素到新数组// 第三个参数指定目标数组类型为Object[]elementData = Arrays.copyOf(a, size, Object[].class);}} else {// 源集合为空时,使用EMPTY_ELEMENTDATA空数组elementData = EMPTY_ELEMENTDATA;}
}
2.1.3 逻辑分析
以下面这个程序为例对添加和扩容操作进行分析
public static void main(String[] args) {List<Integer> list = new ArrayList<Integer>();list.add(1);for (int i = 2; i <= 10; i++){list.add(i);}list.add(11);}
核心代码片段:
入口:add(E e) 方法(首次添加元素的调用入口)
/*** 尾部追加元素(首次添加元素从这里进入)*/
public boolean add(E e) {modCount++; // 记录结构修改次数(用于fail-fast迭代器)add(e, elementData, size); // 调用私有重载方法完成实际添加return true;
}
核心逻辑:add(E e, Object[] elementData, int s) 私有方法 java 运行
/*** 私有辅助方法:拆分add(E)逻辑以控制字节码大小(优化JIT内联)* @param e 待添加元素* @param elementData 当前底层数组* @param s 当前元素数量(size)*/
private void add(E e, Object[] elementData, int s) {// 关键判断:当前元素数量 == 底层数组长度 → 容量不足,需要扩容if (s == elementData.length)elementData = grow(); // 扩容(首次添加时触发)// 扩容后,将元素放入数组尾部,size+1elementData[s] = e;size = s + 1;
}
扩容核心:grow() 与 grow(int minCapacity) 方法
/*** 无参扩容:默认扩容到“当前size+1”(首次添加时size=0,minCapacity=1)*/
private Object[] grow() {return grow(size + 1); // 调用有参grow,传入“最小需要容量”
}/*** 有参扩容:根据最小需要容量计算新容量,生成新数组* @param minCapacity 最小需要容量(首次添加时为1)*/
private Object[] grow(int minCapacity) {int oldCapacity = elementData.length; // 旧容量(首次添加时为0)// 分支1:非默认空数组(如new ArrayList(0)创建的集合)if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {// 计算新容量:优先按“旧容量*1.5”扩容,若不足则用minCapacityint newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity, // 最小增长幅度(需要补充的容量)oldCapacity >> 1 // 优先增长幅度(旧容量/2,即1.5倍扩容));// 复制旧数组元素到新数组return elementData = Arrays.copyOf(elementData, newCapacity);} // 分支2:默认空数组(无参构造new ArrayList()创建的集合)else {// 首次添加时,扩容到“默认容量10”与“minCapacity”的较大值(此时minCapacity=1,故取10)return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];}
}
这里我们主要分析首次添加数据和第11次添加数据的情况:
首次添加数据
第11次添加数据
2.2 ArrayList底层实现原理
ArrayList底层由动态数组实现
AarrayList初始容量为0,当插入第一个数据时才会初始化容量为10
ArrayList进行扩容时时原容量的1.5倍(向下取整),每次扩容都要拷贝数组
ArrayList添加数据时:
确保数组已用长度+1后足够下个数据存储
计算数值容量,若当前数组已用长度+1后大于其容量,则用grow()方法进行扩容
确保有地方存储新数据后,将新元素添加到size的位置上
添加成功后返回布尔值true
2.3 如何实现数组与List间的转换
数组转List调用asList()方法,修改数组内容时,由于List和数组指向同一内存地址,因此List也会受影响
List转数组调用toArray方法,无参toArray方法返回Object数组,传入初始化对象数组,返回该对象数组,修改List内容时,由于是通过拷贝创建新对象,数组内容不会受影响
3 LinkedList
3.1 单向链表
3.1.1 时间复杂度分析
查询操作:时间复杂度为O(n)
增删操作:时间复杂度为O(n)
3.2 双向链表
3.2.1 时间复杂度分析
查询操作:时间复杂度为O(n),给定节点时查询前驱节点时间复杂度为O(1)
增删操作:时间复杂度为O(n),给定节点时增删前驱节点时间复杂度为O(1)
3.2.2 与单向链表对比
需要两个额外空间存储前驱节点和后继结点地址
支持双向遍历,操作更灵活
4 ArrayList和LinkedList的区别
4.1 底层数据结构
ArrayList是由动态数组实现的
LinkedList是由双向链表实现的
4.2 操作数据效率
ArrayList可按下标查询,时间复杂度为O(1),LinkedList不支持下标查询
未知索引时ArrayList与LinkedList查询操作时间复杂度均为O(n)
对ArrayList尾部进行增删操作时间复杂度为O(1),其他部分为O(n)
对LinkedList头尾进行增删操作时间复杂度为O(1),其他部分为O(n)
4.3 内存空间占用
ArrayList底层是数组,内存连续,节省内存
LinkedList底层是双向链表,存储数据同时也存储头尾两个指针,更占用内存
4.4 线程安全问题
二者均不是线程安全的
如果想要保证线程安全有以下两种方案:
只在方法内使用,局部变量是安全的
使用线程安全的ArrayList和LinkedList:使用Collections的synchronizedList()方法进行包装,但会影响性能和效率