一文掌握ArrayList和LinkedList源码解读
大家好,我是Leo!
今天来看一下ArrayList和LinkedList的源码,主要是看一下常用的方法,包括像add、get、remove方法,大部分都是从源码直接解读的,相信大家读完都会有一定收获。

ArrayList
List<String> list = new ArrayList<>();
list.add("zly");
list.add("coding");
list.add("菜鸟阶段!");
底层是数组:transient Object[] elementData;
构造方法: 一个是支持自定义大小的,一个是直接赋值成{}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 如果用户指定了初始容量
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果用户指定了初始容量为0,就赋值成一个{}
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
// DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个默认大小为0的数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
add方法:
public boolean add(E e) {
// 确保容量足够,判断是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 插入数据
elementData[size++] = e;
return true;
}
对于扩容的解释:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 1.5倍进行扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
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);
}
带索引的add方法:
public void add(int index, E element) {
// 检查索引是否合法!
rangeCheckForAdd(index);
// 判断是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将index和之后的数据都后移一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 插入元素
elementData[index] = element;
size++;
}
get方法
public E get(int index) {
// 检查索引是否合法
rangeCheck(index);
// 根据索引从object[] 获取数据
return elementData(index);
}
set方法
将某个index的数据修改成指定的元素。
public E set(int index, E element) {
rangeCheck(index);
// 旧数据
E oldValue = elementData(index);
// 更新成新数据
elementData[index] = element;
// 返回旧数据
return oldValue;
}
remove方法
public E remove(int index) {
rangeCheck(index);
// 修改次数+1
modCount++;
// 获取原先的值
E oldValue = elementData(index);
// 计算往前移动的下标
int numMoved = size - index - 1;
if (numMoved > 0)
// 移动数据
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将最后一个树设置为null
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
remove某个元素
// 删除第一个匹配的值
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 快速移除值
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
// 修改次数
modCount++;
// 需要移动的长度
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
迭代器iterator
在ArrayList中自己实现了一个Itr迭代器,由于ArrayList是线程不安全的,所以在并发删除的时候,通过会报错Exception in thread "main" java.util.ConcurrentModificationException,并发修改错误。
因为在javac后面,增强for会被编译成iterator的形式,删除调用的是ArrayList的remove方法,而next方法是iterator内的,所以我们可以看iterator内的next方法,然后就大致知道为什么会报这个错了。
public E next() {
// check是否有并发修改
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
// modCount在ArrayList的fastRemove已经修改了
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
那为什么使用iterator器为什么就可以安全删除呢?
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 虽然这里也修改了
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 假修改了
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
elementData被transient修饰
ArrayList本身是支持序列化的,为什么存储的元素用transient修饰呢,其实在ArrayList也提供了序列化的方式,提供了readObject和writeObject两种方法。
// 序列化
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
// 反序列化
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
LinkedList
双向链表结构
LinkedList实现了List、Deque
// 链表的元素个数
transient int size = 0;
// 链表的头节点
transient Node<E> first;
// 链表的尾节点
transient Node<E> last;
我们都说LinkedList是通过双向链表实现的,那它的链表节点的结构长什么样呢?其实相信大家数据结构有了解的话,肯定也知道会有一个前缀节点和指向的下一个节点和指向的数据
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);
}
那常用的方法会有哪些呢?
public E getFirst();
public E getLast();
public E removeFirst();
public E removeLast();
public void addFirst(E e) ;
public void addLast(E e);
public E get(int index);
public E set(int index, E element);
上面的方法都可以看到提供了添加和删除的方法以及获取链表头尾节点的方法,当然这里面也有迭代器的实现,下面我们再一个个开它们源码的实现。
add(E e)
首先我们先来看不指定插入头尾的方法,可以看到默认采用的是尾插法。
public boolean add(E e) {
// 插入到尾部
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = ;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
// 尾插法
if (l == null)
first = newNode;
else
l.next = newNode;
// 元素个数+1
size++;
// 修改次数+1,来自AbstractList中
modCount++;
}
当然看完上面这个方法后,再看addLast应该就是同理了,
addLast(E e)
public void addLast(E e) {
linkLast(e);
}
addFirst(E e)
这个很明显采用的就是头插法了,每次将新插入的节点更新为新的头节点。
public void addFirst(E e) {
linkFirst(e);
}
// null<-e->f
private void linkFirst(E e) {
// 先copy一份当前的头节点
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
// 更新头节点
first = newNode;
// 如果头节点为空,当前节点也是尾节点
if (f == null)
last = newNode;
else
// 如果链表不为空,将原先头节点的前缀节点指向新插入的节点
f.prev = newNode;
size++;
modCount++;
}
add(int index, E element)
在指定位置插入元素,在LinkedList里面有一个default node()方法,用于查询指定索引的节点,根据位置位于大小中间左边还是右边确定用头节点还是尾节点进行遍历查询。
public void add(int index, E element) {
// 检查索引是否合法,是否在[0,size]之间;
checkPositionIndex(index);
// 如果需要在最末尾进行插入
if (index == size)
linkLast(element);
else
// node(index)先查询这个索引下对应的节点
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// 原先位置的前缀节点
final Node<E> pred = succ.prev;
// 插入位置的节点,下一个节点指向原先在这个位置的节点
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
// 如果pred为null,证明该节点是头节点
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
Node<E> node(int index) {
// assert isElementIndex(index);
// 如果查询位置位于中间位置偏左
if (index < (size >> 1)) {
// 遍历,找到索引位置的节点
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// 从后开始遍历,找到索引位置的节点
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
remove(E e)
移除指定元素,在删除的时候需要维护first和last节点,并且只会删除第一个匹配到的元素。
public boolean remove(Object o) {
// 移除第一个匹配上的元素
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 如果prev为null, 说明是头节点,需要更新头节点,直接删除即可
if (prev == null) {
first = next;
} else {
// 不是头节点,将前缀节点的next指向删除节点的下一个节点
prev.next = next;
// GC
x.prev = null;
}
// 判断是否是尾节点,是的话,更新last节点
if (next == null) {
last = prev;
} else {
// 不是,将删除节点的next节点的前缀节点改成删除节点的前缀节点
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
removeFirst()
移除链表的第一个节点,更新新的头节点
public E removeFirst() {
// 链表头节点
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
// 将原先的头节点的数据和指向的下一个节点置null
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
// 将新头节点的前缀节点置null
next.prev = null;
size--;
modCount++;
return element;
}
removeLast()
删除链表的尾节点
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
// 将原先链表的尾节点的值和前缀节点都置null
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
// 如果前缀节点为null,说明链表只有一个元素,删除后为null
if (prev == null)
first = null;
else
// 将原尾节点的下一个节点置null
prev.next = null;
size--;
modCount++;
return element;
}
set(index, Element e)
public E set(int index, E element) {
// 检查节点的索引是否位于[0,size)
checkElementIndex(index);
// 获取index的节点
Node<E> x = node(index);
// 更新值
E oldVal = x.item;
x.item = element;
return oldVal;
}
get(int index)
获取指定位置的节点的信息
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
通过上面大家应该对ArrayList和LinkedList的源码有一定的了解,对于ArrayList的存储是数组的,LinkedList是双向链表,这两个结构非常常见和好用的。
一文掌握ArrayList和LinkedList源码解读的更多相关文章
- java基础解析系列(十)---ArrayList和LinkedList源码及使用分析
java基础解析系列(十)---ArrayList和LinkedList源码及使用分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder jav ...
- LinkedList 源码解读
LinkedList 源码解读 基于jdk1.7.0_80 public class LinkedList<E> extends AbstractSequentialList<E&g ...
- ArrayList和LinkedList源码
1 ArrayList 1.1 父类 java.lang.Object 继承者 java.util.AbstractCollection<E> 继承者 java.util.Abstract ...
- ArrayList 和 LinkedList 源码分析
List 表示的就是线性表,是具有相同特性的数据元素的有限序列.它主要有两种存储结构,顺序存储和链式存储,分别对应着 ArrayList 和 LinkedList 的实现,接下来以 jdk7 代码为例 ...
- LinkedList源码解读
一.内部类Node数据结构 在讲解LinkedList源码之前,首先我们需要了解一个内部类.内部类Node来表示集合中的节点,元素的值赋值给item属性,节点的next属性指向下一个节点,节点的pre ...
- List中的ArrayList和LinkedList源码分析
List是在面试中经常会问的一点,在我们面试中知道的仅仅是List是单列集合Collection下的一个实现类, List的实现接口又有几个,一个是ArrayList,还有一个是LinkedLis ...
- ArrayList和LinkedList源码分析
ArrayList 非线程安全 ArrayList内部是以数组存储元素的.类有以下变量: /*来自于超类AbstractList,使用迭代器时可以通过该值判断集合是否被修改*/ protected t ...
- Java集合(五)--LinkedList源码解读
首先看一下LinkedList基本源码,基于jdk1.8 public class LinkedList<E> extends AbstractSequentialList<E> ...
- java集合之LinkedList源码解读
源自:jdk1.8.0_121 LinkedList继承自AbstractSequentialList,实现了List.Deque.Cloneable.Serializable. LinkedList ...
- [数据结构1.2-线性表] 动态数组ArrayList(.NET源码学习)
[数据结构1.2-线性表] 动态数组ArrayList(.NET源码学习) 在C#中,存在常见的九种集合类型:动态数组ArrayList.列表List.排序列表SortedList.哈希表HashTa ...
随机推荐
- C# 循环给多个连续编号的控件赋值
C# 循环给多个连续编号的控件赋值 我们经常在 winform 界面上用很多文本框用来显示一组数据,文本框前面有Label标识.我们得到的数据也经常是一个list 或者数组的形式的.需要给这些文本框赋 ...
- node.js缓冲区类与node-red向串口发数据
遇到的问题是使用node-red的串口模块向串口发送16进制数据,控制LED灯. 初学者经常想当然的认为只要msg的payload里放上对应的数就行了.其实不是. Node-red是node.js环境 ...
- fatal: unable to access ' ' OpenSSL SSL_read: Connection was reset, errno 10054
描述: git clone ...时报错 fatal: unable to access 'https://github.com/github-eliviate/papers.git/': OpenS ...
- 痞子衡嵌入式:MCUXpresso IDE下生成镜像文件的方法及其与IAR,MDK差异
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是MCUXpresso IDE下生成镜像文件的方法及其与IAR,MDK差异. 痞子衡很久以前写过一篇文章 <ARM Cortex-M ...
- Android笔记--修改Device File Explorer的文件打开方式
在首次打开该文件时,不小心选错了打开方式,导致以后每次打开也是同样的打开方式,也不会弹出第一次那样的打开方式的选择弹窗 在这里提供修改文件的默认打开方式的方法: 首先通过File->settin ...
- 操作系统 && C语言 每日学习记录(day1 ~ day8) 已寄
现在正式工作了,发现之前学的东西,很多一知半解,不通透,准备再好好系统学一些计算机原理的东西,每天学一学,在这里记录一下. 规划(7.17开始): 同学分享了个超级好的操作系统课程,每天看个一节:ht ...
- Linux & 标准C语言学习 <DAY14>
一.头文件 头文件可能会被任意源文件包含,意味着头文件中的内容可能会在多个目标文件中存在,要保证合并时不要冲突 重点:头文件只编写声明语句,不能有定义语句 1.头文件应 ...
- NOIP2022游记
NOIP2022游记 今年是第二次考NOIP了,去年第一次考的时候没学过什么东西,混了个省二.今年以高中生的身份考,不仅仅是要省一,还得拿个不错的名次,任务不小. 考试当天早上校园里的雾很大,不知道会 ...
- Kafka 之 HW 与 LEO
更多内容,前往 IT-BLOG HW(High Watermark):俗称高水位,它标识了一个特定的消息偏移量(offset),消费者只能拉取到这个 offset 之前的消息.分区 ISR 集合中的每 ...
- 使用nw.js打包以后的web项目 发布客户端
一.下载nw.js 直接前往官网下载即可 https://nwjs.io/downloads/ 二.封装最简单的客户端 nw.js下载完成后,在任意位置新建文件夹,例如nwtest,然后在文件夹中新建 ...