分析源码的方式(如何分析源码)?
可从以下四个方面进行分析(基于JDK1.8分析):
- 类继承的父类及实现的接口
- 重要的成员变量
- 构造函数
- 关键方法
ArrayList源码分析
- 类继承的父类及实现的接口
/**
1.继承自AbstractList->AbstractCollection
2.实现了List, RandomAccess, Cloneable, Serializable接口
特别说明(以下知识需要另外学习扩展):
RandomAccess: 标记接口, 表明具有能实现快速随机访问的特性;
Cloneable: 标记接口, 表明具有Clone的特性;
Serializable: 标记接口, 表明具有序列化及反序列化的特性;
*/
ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
- 重要的成员变量
//默认初始容量
private static final int DEFAULT_CAPACITY = 10;
//用于空instanc的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//用于默认大小的空实例的共享空数组实例。将其与EMPTY_ELEMENTDATA区分开来,以了解添加第一个元素时要膨胀多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存储ArrayList元素的数组缓冲区。ArrayList的容量就是这个数组缓冲区的长度。
//添加第一个元素时,任何的空ArrayList
//elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA都将扩展为DEFAULT_CAPACITY。
//transient修饰表明不参与序列化操作
transient Object[] elementData;
//ArrayList的大小(包含的元素个数)
private int size;
- 构造函数
//构造一个初始容量为0的空列表
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//构造一个具有指定初始容量的空列表。
//参数:initialCapacity–列表的初始容量
//如果指定的初始容量为负数会抛出IllegalArgumentException
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//按照集合的迭代器返回的顺序,构造一个包含指定集合的元素的列表。
//参数:c–要将其元素放入此列表的集合
//如果指定的集合为null会抛出NullPointerException
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
- 关键方法(增加及扩容方法(关键中的核心,重点深入理解))
// 添加元素 扩容是在add方法之后才发生的 初始化并不会发生扩容 谨记
public boolean add(E e) {
// 确保数组的容量,保证可以添加该元素
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将该元素放入数组中
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 如果数组是空的,那么会初始化该数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// DEFAULT_CAPACITY 为 10 ,所以调用无参默认 ArrayList 构造方法初始化的话,默认的数组容量为 10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 确保数组的容量,如果不够的话,调用 grow 方法扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 真正的扩容方法 涉及到数组拷贝 Arrays.copyOf()-> System.arraycopy()
private void grow(int minCapacity) {
// 当前数组的容量
int oldCapacity = elementData.length;
// 新数组扩容为原来容量的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新数组扩容容量还是比最少需要的容量还要小的话,就设置扩充容量为最小需要的容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//判断新数组容量是否已经超出最大数组范围,MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 复制元素到新的数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容机制图解说明:
ArrayList实现原理?
简记:
- 动态数组(介绍数组初始化及扩容机制)
- 增删改查特点
- 迭代器特点
- 存储特点
- 使用场景
详细描述:
- 内部数组:
ArrayList使用一个内部数组来存储元素。可通过3种构造函数生成指定容量的数组。 - 动态扩容:当插入新元素时,如果内部数组已满,
ArrayList会分配一个更大的数组,并将原有元素复制到新数组中。默认情况下,新数组的大小是原数组大小的1.5倍,即扩容50%。这种动态扩容策略可以减少频繁扩容带来的性能开销。 - 元素访问:
ArrayList支持通过索引访问元素。由于内部数组是连续存储的,可以通过索引直接访问对应位置的元素,因此获取元素的时间复杂度为O(1)。但是,插入和删除操作可能需要移动其他元素,因此具有更高的时间复杂度。 - 插入和删除:当插入或删除元素时,
ArrayList需要移动其他元素以保持连续性。在插入元素时,需要将插入点之后的元素向后移动一个位置。在删除元素时,需要将删除点之后的元素向前移动一个位置。这些移动操作的时间复杂度是O(n),其中n是元素的数量。 - 迭代器:
ArrayList提供了迭代器(Iterator)来遍历元素。迭代器可以按顺序访问集合中的元素,使用者可以通过调用next()方法获取下一个元素,直到遍历完所有元素。ArrayList的迭代器实现是Fail-fast的,即在迭代过程中,如果有其他线程修改了集合的结构,将抛出ConcurrentModificationException异常。 - 存储数据特点:
ArrayList中可存储任意类型的对象(包括null),并可以有重复的元素。 - 性能注意事项:由于
ArrayList是基于数组实现的,它在随机访问和获取元素方面具有良好的性能。但是,在插入和删除操作中,特别是在列表的开始或中间插入或删除元素时,性能相对较差。如果需要频繁进行这些操作,可以考虑使用LinkedList等其他数据结构
五星级高频面试题: ArrayList和LinkedList的区别是什么?
1、底层数据结构
ArrayList:
- 继承自AbstactList, 实现了List, RandomAccess, Cloneable, Serializable接口。
- 底层使用动态数组实现。
- RandomAccess是一个标记接口, 该标记表明能进行快速的随机访问(存取值)。
LinkedList:
- 继承自AbstractSequentiableList, 实现List, Deque, Cloneable, Serializable接口。
- 底层使用双向链表实现。
- 实现Deque接口, 表明具备队列特性, 同时其也具备栈的特性。
2、操作数据的效率
查询操作:
- ArrayList是内存连续的, 根据寻址公式获取下标, 查找的时间复杂度为O(1)。
- LinkedList的内存不连续的, 不支持下标查询, 查找前驱节点的时间复杂度为O(n),查找后继节点的时间复杂度为O(1)。
插入和删除:
- ArrayList尾部插入和删除, 时间复杂度为O(1), 其他插入和删除需要挪动数组, 时间复杂度为O(n)。
- LinkedList头尾节点插入和删除时间复杂度为O(1), 其他插入和删除需要遍历数组, 时间复杂度为O(n)。
3、内存占用
- Arraylist: 底层是动态数组实现, 内存连续, 节省内存。
- LinkedList: 是双向链表实现, 需要存储数据, 和两个指针, 更占用内存。
4、随机访问及遍历性能
- Arraylist: 支持高效的随机访问,可通过索引直接访问元素。在遍历时,也有较好的性能。
- LinkedList: 不支持高效的随机访问,需要从头开始遍历链表。但在插入和删除操作时性能较好。
5、线程安全
- Arraylist和LinkedList都不是线程安全的, 如何保证线程安全?
- 1、在方法内使用, 局部变量的线程安全较好。
- 2、通过Collections.synchronizedList(list)方法实现。
- 3、并发编程中使用CopyOnWriteArrayList来实现。
综上所述,如果需要频繁进行随机访问和查找操作,可选择ArrayList;如果需要频繁进行插入和删除操作,可以选择LinkedList。