ArrayList&LinkedList源码解读
ArrayList
概述
ArrayList实现了List接口,是顺序容器,允许放入null,底层通过数组实现,线程不安全,容量不足会自动扩容
transient Object[] elementData;//使用transient关键字不序列化某个变量 private int size;
构造函数
可以指定容量,默认为空,所以最少扩容一次,可以提前预估容量提升性能
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); } } public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } 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; } }
自动扩容逻辑
一般扩容1.5倍,超出最大值后扩容为Integer.MAX_VALUE
在可以预估扩容大小的时候可以使用内部公开方法ensureCapacity,提前扩容,以减少扩容次数。
private void grow(int minCapacity) { //拿到旧的长度 int oldCapacity = elementData.length; //设置新的为旧的1.5倍, 右移运算除以2 int newCaacity = oldCapacity + (oldCapacity >> 1); //如果仍然满足不了最小需求,就设置为最小需求 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //如果已经超出了最大限制 if (newCapacity - MAX_ARRAY_SIZE > 0) //将最大限制设置为Integer.MAX_VALUE newCapacity = hugeCapacity(minCapacity); //复制数组,本地方法 elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
添加逻辑
每次添加之前会检查容量是否足够,不够会使用grow方法进行扩容
如果指定添加的位置,则需要先对元素进行移动,O(n)的复杂度
同样的删除指定位置也需要移动,删除最后一个位置不需要。
需要注意的是为了让GC起作用,防止内存泄漏,需要为删除位置显示置空
private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; } public void add(int index, E element) { rangeCheckForAdd(index); modCount++; final int s; Object[] elementData; if ((s = size) == (elementData = this.elementData).length) elementData = grow(); System.arraycopy(elementData, index, elementData, index + 1, s - index); elementData[index] = element; size = s + 1; }
缩容
指的是可以手动将数组空间调整为当前实际大小,防止空间浪费,也是通过移动数组实现的。
public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
快速失败
任何结构性修改操作都会引发modCount++
当迭代器发现modCount被修改后,就会抛出ConcurrentModificationException异常(当前修改异常)
注意是结构性修改,例如add、remove等改变数组大小的操作
set操作修改某个特定位置的引用时,不会触发迭代器快速失败
LinkedList
概述
LinkedList使用双向链表实现了List和Deque接口,因此它可以作为顺序容器、队列和栈,但是这个类几乎不会被使用(但是我经常使用),一般定义栈或者队列可以使用ArrayDeque(我也得改过来),比LinkedList性能好很多。
因为使用了双向链表,所以决定了所有跟下标相关的操作都是线性时间。队首队尾操作是常数时间,线程不安全。
Deque<String> stack1 = new LinkedList<>(); Deque<String> stack2 = new ArrayDeque<>();
内部元素
LinkedList底层通过定义first和last指针指向第一个和最后一个元素,但时候链表为空的时候没有dummy节点,都指向null。
transient int size; transient Node<E> first; transient Node<E> last; private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
构造函数
public LinkedList() { } public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
涉及方法
getFirst() getLast() removeFirst() removeLast() remove(e) remove(index)
因为是双向链表,所以删除时只需要unlink就行,由JVM进行垃圾回收
E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final ode<E> prev = x.prev; if (prev == null) {// 第一个元素 first = next; } else { prev.next = next; x.prev = null; } if (next == null) {// 最后一个元素 last = prev; } else { next.prev = prev; x.next = null; } x.item = null; // GC size--; modCount++; return element; }
清空链表
LinkedList为了让GC可以更快速回收元素,提供了clear方法,清除所有节点间的引用关系。
public void clear() { // Clearing all of the links between nodes is "unnecessary", but: // - helps a generational GC if the discarded nodes inhabit // more than one generation // - is sure to free memory even if there is a reachable Iterator for (Node<E> x = first; x != null; ) { Node<E> next = x.next; x.item = null; x.next = null; x.prev = null; x = next; } first = last = null; size = 0; modCount++; }
快速失败
在遍历过程中对集合进行了结构性修改(如添加或删除元素),LinkedList
的迭代器会抛出ConcurrentModificationException
异常。
原理仍然是维护了一个modCount变量,每次结构性修改时++。
发散
在ArrayList中大量使用了Arrays.copyOf方法
方法传入(原始数组、新数组长度、新数组类型)进行数组扩容
这个方法底层其实是一个native方法System.arraycopy
System.arraycopy利用了底层系统的内存复制功能进行数组复制
-
首先创建一个新数组,定义复制参数
-
src
:源数组,即要从中复制元素的数组。srcPos
:源数组中的起始位置,从这个位置开始复制元素。dest
:目标数组,即要将元素复制到的数组。destPos
:目标数组中的起始位置,复制到这个位置。length
:要复制的元素数量。
为什么在ArrayList和HashMap中都要定义最大容量,超过了之后又设置为Integer.MAX_VALUE,而不直接设置为MAX_VALUE?
-
实际应用中达不到Integer.MAX_VALUE,硬件不够
-
数据结构自身会有空间开销,用于存储额外的信息,所以实际可用长度会小于Integer.MAX_VALUE
-
Integer.MAX_VALUE长度的集合性能很低
-
防止意外分配巨大空间
在 HashMap
和 ArrayList
中,设置最大容量为 Integer.MAX_VALUE
是为了确保系统的稳定性和性能,同时考虑到Java虚拟机的内存管理限制。即使理论上可以定义更大的容量,实际使用中这是不切实际的,因而设置一个合理的容量上限是必要的。在超过一定大小后,集合会被限制在 Integer.MAX_VALUE
以防止因意外操作导致系统资源耗尽或性能问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构