java集合之List源码解析
List是java重要的数据结构之一,我们经常接触到的有ArrayList、Vector和LinkedList三种,他们都继承来自java.util.Collection接口,类图如下

接下来,我们对比下这三种List的实现和不同:
一、基本实现
1、ArrayList和Vector使用了数组实现,可以认为它们封装了对内部数组的操作;它们两个底层的实现基本可以认为是一致的,主要的一点区别在于对多线程的支持上面。ArrayList没有对内部的方法做线程的同步,它不是线程安全的,而Vector内部做了线程同步,是线程安全的。
2、LinkedList使用了双向链表数据结构,与基于数组实现的ArrayList和Vector相比,这是一种不同的实现方式,这也决定了他们不同的应用场景。LinkedList链表由一系列列表项构成,一个表项包含三个部分:元素内容、前驱表项和后驱表项,如下图所示

在JDK的实现中,增加了两个节点指针first、last分别指向首尾节点

二、不同之处
在这里我们主要对比下ArrayList与LinkedList的不同之处
1、增加元素到列表尾端:
在ArrayList中增加元素到列表尾端
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保内部数组有足够的空间
elementData[size++] = e; //将元素加入到数组的末尾,完成添加
return true;
}
在这个过程当时,add的性能主要是由ensureCapacityInternal方法的实现,我们继续往下跟踪代码
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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);
}
calculateCapacity方法会根据你对ArrayList初始化的不同,对当前elmentData这个初始化数组进行非空判断。如果它是一个空数组,则返回ArrayList默认容量(10)和所需最小容量(minCapacity)比较的最大值,如果不为空则直接返回所需最小容量。接下来在ensureExplicitCapacity方法中判断,如果所需容量大于当前对象数组的长度则调用grow方法对数组进行扩容。
在这里我们可以看到如果ArrayList容量满足需求时,add()其实就是直接对数组进行赋值,性能很高。而当ArraList容量无法满足要求扩容时,需要对之前的数组进行复制操作。因此合理的数组大小有助于减少数组的扩容次数,如果使用时能够预估ArrayList数组大小,并进行初始化,指定容量大小对性能会有所提升。
在LinkedList中增加元素到列表尾端
//尾端插入,即将节点值为e的节点设置为链表的尾节点
void linkLast(E e) {
final Node<E> l = last;
//构建一个前驱prev值为l,节点值为e,后驱next值为null的新节点newNode
final Node<E> newNode = new Node<>(l, e, null);
//将newNode作为尾节点
last = newNode;
//如果原尾节点为null,即原链表为null,则链表首节点也设置为newNode
if (l == null)
first = newNode;
else //否则,原尾节点的next设置为newNode
l.next = newNode;
size++;
modCount++;
}
LinkedList由于使用了链表结构,因此不需要维护容量的大小,这是相比ArrayList的优势。但每次元素的增加都需要新建一个node对象,并进行更多的赋值操作。在大数据量频繁的调用过程中,对性能会有所影响。
2、增加元素到任意位置:
void add(int index, E element)
由于实现上的不同,ArrayList和LinkedList在这个方法上存在存在一定的性能差异。由于ArrayList是基于数组实现的,而数组是一块连续的内存空间,如果在数组的任意位置插入元素,必然导致在该位置后的所有元素需要重新排列,因此效率会比较低。
ArrayList代码实现如下:
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//数组复制
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
可以看到,ArrayList每次插入操作,都会进行一次数组复制。并且插入的元素在List中位置越靠前,数组重组的开销也越大。
再开LinkedList代码实现
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
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;
}
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//指定节点的前驱prev
final Node<E> pred = succ.prev;
//当前节点的前驱为指点节点的前驱,后继为指定的节点
final Node<E> newNode = new Node<>(pred, e, succ);
//更新指定节点的前驱为当前节点
succ.prev = newNode;
//更新前驱节点的后继
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
LinkedList中定位一个节点需要遍历链表,如果新增的位置处于List的前半段,则从前往后找;若其位置处于后半段,则从后往前找。因此指定操作元素的位置越靠前或这靠后,效率都是非常高效的。但如果位置越靠中间,需要遍历半个List,效率较低。因此LinkedList中定位一个节点需要遍历链表,所以下标有关的插入、删除时间复杂度都变为O(n) ;
3、删除任意位置元素
public E remove(int index)
对ArrayList来说,remove()方法和add()方法是相同的,在删除指定位置元素后,都要对数组进行重组。代码如下
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}
可见,在进行一次有效删除后,都要进行数组的重组。并且跟add()指定位置的元素一样,删除元素的位置越靠前,重组时的开销就越大,删除的元素位置越靠后,开销越小
再看LinkedList中代码的实现如下
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
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;
}
}
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; //当前节点的前驱
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;
size--;
modCount++;
return element;
}
可见跟之前的插入任意位置一样,LinkedList中定位一个节点需要遍历链表,效率跟删除的元素的具体位置有关,所以删除任意位置元素时间复杂度也为O(n) ;
4、随机访问
public E get(int index)
首先看ArrayList的实现代码如下
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
可见ArrayList随机访问是直接读取数组第几个下标,效率很高。
LinkedList实现代码如下
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
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;
}
}
相反LinkedList随机访问,每次都需要遍历半个List确定元素位置,效率较低。
5、总结
通过比较与分析ArrayList与LinkList两种不同实现的的List的功能代码后,我个人感觉两种List的具体使用真的要看实际的业务场景,有些具体的功能如新增删除等操作根据实际情况,效率不可一概而论。在这里进行简单的分析只是为了个人加强理解,如有不正确的地方还望指出与海涵。
参考资料:《Java程序性能优化》
关注微信公众号,查看更多技术文章。

java集合之List源码解析的更多相关文章
- Java集合---Array类源码解析
Java集合---Array类源码解析 ---转自:牛奶.不加糖 一.Arrays.sort()数组排序 Java Arrays中提供了对所有类型的排序.其中主要分为Prim ...
- Java集合:LinkedList源码解析
Java集合---LinkedList源码解析 一.源码解析1. LinkedList类定义2.LinkedList数据结构原理3.私有属性4.构造方法5.元素添加add()及原理6.删除数据re ...
- 【Java集合】HashSet源码解析以及HashSet与HashMap的区别
HashSet 前言 HashSet是一个不可重复且元素无序的集合.内部使用HashMap实现. 我们可以从HashSet源码的类注释中获取到如下信息: 底层基于HashMap实现,所以迭代过程中不能 ...
- Java集合---Arrays类源码解析
一.Arrays.sort()数组排序 Java Arrays中提供了对所有类型的排序.其中主要分为Primitive(8种基本类型)和Object两大类. 基本类型:采用调优的快速排序: 对象类型: ...
- java集合之HashMap源码解析
Map是java中的一种数据结构,围绕着Map接口,有一系列的实现类如Hashtable.HashMap.LinkedHashMap和TreeMap.而其中HashMap和Hashtable我们平常使 ...
- Java集合之LinkedList源码解析
LinkedList简介 LinkedList基于双向链表,即FIFO(先进先出)和FILO(先进后出)都是支持的,这样它可以作为堆栈,队列使用 继承AbstractSequentialList,该类 ...
- 死磕 java集合之DelayQueue源码分析
问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...
- 死磕 java集合之PriorityBlockingQueue源码分析
问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...
- 死磕 java集合之PriorityQueue源码分析
问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...
随机推荐
- Python学习笔记第十一周
目录: 1.RabbitMQ 2.Redis 内容: 1.RabbitMQ 实现简单的队列通信 send端 import pika credentials = pika.PlainCredent ...
- jvm内存增长问题排查简例
jvm内存增长问题排查 排查个jvm 内存占用持续增加的问题,纪录一下,引以为戒. 运维发现应用jvm内存占用在发布后回落,然后持续增高,,dump后分析一下: 占内存的大部分是这种名字相似的bean ...
- 安装Nmon方法
1. 登录IBM官方网站(http://nmon.sourceforge.net/pmwiki.php?n=Site.Download)下载相应版本的nmon工具:nmon_linux_14g.tar ...
- day python 010 函数(1)
一 函数 定义 : def () 函数是对功能或者动作的封装 def yue (): # 形参列表 # print("拿出手机") # print("打开陌陌" ...
- js framework comparation
starting a new project:(finance project for p2p -- like lending club, or prosper ) ,we considering a ...
- 【shell编程】之基础知识-语法
一.shell变量 1.定义变量 定义变量时,变量名不加美元符号($,PHP语言中变量需要), 如: your_name="runoob.com" 注意,变量名和等号之间不能有空格 ...
- golang for 循环变量取内存地址
前几天提交的代码进行测试的时候发现变量无法赋值,原始代码如下: for _, asset := range dspInfo.native.Assets { var resAsset protocol. ...
- msyql开启慢查询以及分析慢查询
慢查询的用途是用来发现执行时间长的查询语句,以便对这些语句进行优化 [mysqld] #在这里面增加,其它地方无效 #server-id=1 #log-bin=master-bin slow_quer ...
- smb文件共享实现
samba文件共享 首先安装软件 yum install samba -y 编辑配置文件 /etc/samba/smb.conf ,在文章最后添加以下内容 [smbtest] content = do ...
- hive GenericUDF1
和UDF相比,通用GDF(GenericUDF)支持复杂类型(比如List,struct等)的输入和输出. 下面来看一个小示例. Hive中whereme表中包含若干人的行程如下: A 2 ...