本系列文章:
Java集合(一)集合框架概述
Java集合(二)List、ArrayList、LinkedList使用及源码分析
Java集合(三)CopyOnWriteArrayList、Vector、Stack
Java集合(四)Map、HashMap、HashTable
Java集合(五)LinkedHashMap、TreeMap、ConcurrentHashMap
Java集合(六)Set、HashSet、LinkedHashSet、TreeSet
Java集合(七)BlockingQueue、ArrayBlockingQueue、LinkedBlockingQueue
Java集合(八)PriorityBlockingQueue、DelayQueue
List是有序集合
,使用List可以控制列表中每个元素的插入位置,可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。
List通常允许重复的元素。
使用List存储的特点:元素有序、可重复。
List最常见的实现方式是ArrayList和LinkedList。以下是List接口常见实现类的对比:
具体实现 | 优点 | 缺点 |
---|---|---|
ArrayList | 底层数据结构是数组,查询快,效率高 |
增删慢; 线程不安全 |
LinkedList | 底层数据结构是链表,增删快,效率高 |
查询慢; 线程不安全 |
Vector | 底层数据结构是数组,查询快; 线程安全 |
增删慢,效率低 |
CopyOnWriteArrayList | 底层数据结构是数组,读写分离,效率高; 线程安全, |
内存消耗较大; 只能保证数据的最终一致性,不能保证数据的实时一致性 |
ArrayList是一个动态数组,随着容器中的元素不断增加,容器的大小也会随着增加。同时由于ArrayList底层是数组实现,所以可以随机访问元素。
Vector与ArrayList类似,不过是同步的,因此Vector是线程安全的动态数组。
Stack继承自Vector,实现一个后进先出的堆栈。
LinkedList是一个双向链表,LinkedList不能随机访问,增删元素比较方便。
CopyOnWriteArrayList是一个线程安全、并且在读操作时无锁的 ArrayList。当需要修改容器中的元素时,会首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
数据元素在内存中,主要有2种存储方式:
在List的实现类中,顺序存储以ArrayList为代表
。在List的实现类中,链式存储以LinkedList为代表
。 for (int i = 0; i < list.size(); i++) {
list.get(i);
}
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
iterator.next();
}
for (ElementType element : list) {
}
基于元素的位置,按位置读取
。for循环遍历适合实现了RandomAccess接口的集合
。如果一个数据集合实现了该接口,就意味着它支持顺序访问,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。如果没有实现该接口,表示不支持顺序访问,如LinkedList。 综合而言,推荐的做法就是,支持顺序访问的List可用for循环遍历,否则建议用Iterator或foreach遍历
。
使用Arrays.asList(array)进行转换。示例:
String[] myArray = { "Apple", "Banana", "Orange" };
List<String> myList = Arrays.asList(myArray);
//上面两个语句等价于下面一条语句
List<String> myList = Arrays.asList("Apple","Banana", "Orange");
Arrays.asList的源码:
/**
* 返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,
* 与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。
*/
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
需注意:此处的ArrayList不是java.util包下的,而是java.util.Arrays.ArrayList。它是Arrays类自己定义的一个静态内部类,这个内部类没有实现add()、remove()方法,而是直接使用它的父类AbstractList的相应方法。而AbstractList中的add()和remove()是直接抛出java.lang.UnsupportedOperationException异常。
《阿里巴巴Java开发手册》对这个方法的描述:
Integer[] myArray = { 1, 2, 3 };
List myList = Arrays.asList(1, 2, 3);
myList.add(4);//运⾏时报错:UnsupportedOperationException
myList.remove(1);//运⾏时报错:UnsupportedOperationException
myList.clear();//运⾏时报错:UnsupportedOperationException
如果List只是用来遍历,就用Arrays.asList()
。如果List还要添加或删除元素,还是new一个java.util.ArrayList,然后一个一个的添加元素
。使用List自带的toArray()方法。
ArrayList | LinkedList | |
---|---|---|
线程安全性 | 线程不安全 | 线程不安全 |
底层实现 | 动态数组 | 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) |
访问方式 | 支持通过元素的下标访问元素 | 不支持通过元素的下标访问元素 |
增加和删除效率 | 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。 如果要在指定位置 i 插入和删除元素的话,时间复杂度就为 O(n)。 |
采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1) |
是否支持快速随机访问 |
支持 | 不支持 |
内存空间 | ArrayList的空间浪费主要体现在列表的结尾会预留⼀定的容量空间 | 更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素 |
LinkedList是一个双链表,在添加和删除元素时具有比ArrayList更好的性能,但在get与set方面弱于ArrayList。当然,这些对比都是指数据量很大或者操作很频繁。
综合来说,在需要频繁读取集合中的元素时,更推荐使用ArrayList,而在插入和删除操作较多时,更推荐使用LinkedList
。
public interface RandomAccess {
}
RandomAccess接口用来标识其实现类具有快速随机访问的特点
。
数组 | 链表 | |
---|---|---|
查询 | O(1) | O(n) |
插入、删除 | O(n) | O(1) |
这两个类都实现了List接口,他们都是有序集合。相同点:
- ArrayList和Vector都是继承了相同的父类和实现了相同的接口。
- 底层都是数组实现的。
- 初始默认长度都为10。
- 迭代器的实现都是fail-fast的。
ArrayList和Vector的不同点:
ArrayList | Vector | |
---|---|---|
线程安全性 | 线程不安全 | 使用Synchronized来修饰大部分方法,线程安全 |
性能 | 更优 | 由于使用了Synchronized,性能较差 |
扩容 | 1.5倍扩容 | 2倍扩容 |
public class ArrayListTest {
private static List<String> list = new ArrayList<String>();
public static void main(String[] args) {
// 同时启动两个线程对list进行操作
new ThreadOne().start();
new ThreadTwo().start();
}
private static void printAll() {
System.out.println();
Iterator<String> iter = (Iterator<String>) list.iterator();
while(iter.hasNext()) {
System.out.print(iter.next()+", ");
}
}
/* 向list中依次添加0,1,2,3,4,5,每添加一个数之后,就通过printAll()遍历整个list */
private static class ThreadOne extends Thread {
public void run() {
int i = 0;
while (i<6) {
list.add(String.valueOf(i));
printAll();
i++;
}
}
}
/* 向list中依次添加10,11,12,13,14,15,每添加一个数之后,就通过printAll()遍历整个list */
private static class ThreadTwo extends Thread {
public void run() {
int i = 10;
while (i<16) {
list.add(String.valueOf(i));
printAll();
i++;
}
}
}
}
将上述代码运行多次,就有可能出现如下ConcurrentModificationException:
产生该异常的原因是:两个线程在同时操作同一个ArrayList对象,当一个线程在遍历的时候,另一个线程去修改了ArrayList对象中的元素,就会触发fail-fast机制,抛出异常
。
private class Itr implements Iterator<E> {
//迭代器游标
int cursor;
//最后一个元素索引为-1
int lastRet = -1;
//初始化"期望的modCount"expectedModCount,如果在迭代过程中集合元素
//发生了变化,则modCount就会跟着变化,此时用expectedModCount和modCount
//比较,就能知道这次集合元素变化
int expectedModCount = modCount;
//是否还有下一个元素
public boolean hasNext() {
return cursor != size;
}
//获取下一个元素
public E next() {
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];
}
//删除元素
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();
}
}
//省略一些代码
//检测在迭代过程中,集合元素是否发生了变化
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
modCount是对ArrayList 真正的修改次数,对ArrayList 内容的修改都将增加这个值,在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。
在迭代过程中,会判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了ArrayList,此时就会抛出异常。
上述代码中,modCount声明为volatile,保证线程之间修改的可见性
。
1)使用Collections.sort默认正序,可以传第二个参数自定义排序;
2)自定义对象实现Comparable接口;
3)实现Comparator接口自定义比较器。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
简单来说,ArrayList是动态数组(数组的容量会随着元素数量的增加而增加,即动态变化
)。它的底层结构就是数组。
同时,在ArrayList中,并没有规定特殊的元素操作规则(比如只能在数组两端进行增删等),所以ArrayList是操作非常自由的动态数组:
- 可以在数组头部 / 尾部 / 中间等任意位置插入、删除元素。
- 添加单个或集合中的元素时,未指明添加位置时,都是添加在尾部。
- 不会像队列那样在增加、取出元素时可能会产生阻塞现象。
1.5倍扩容,即:原来大小+原来大小*0.5
)。 //构造一个初始容量为10的空ArrayList
//(其实是构造了一个容量为空的ArrayList,第一次添加元素时,扩容为10)
public ArrayList()
//构造具有指定初始容量的空ArrayList
public ArrayList(int initialCapacity)
//将元素追加到此ArrayList的尾部
public boolean add(E e)
//在指定位置插入元素
public void add(int index, E element)
//删除指定位置的元素
public E remove(int index)
//从ArrayList中删除第一个出现指定元素的(如果存在)
public boolean remove(Object o)
//清空ArrayList
public void clear()
//返回指定位置的元素
public E get(int index)
//返回指定元素的第一次出现的索引,如果此ArrayList不包含元素,则返回-1
public int indexOf(Object o)
//返回指定元素的最后一次出现的索引,如果此ArrayList不包含元素,则返回-1
public int lastIndexOf(Object o)
public E set(int index, E element)
public List< E > subList(int fromIndex, int toIndex)
//判断ArrayList是否为空
public boolean isEmpty()
//返回ArrayList中的元素数
public int size()
//判断是否包含某个元素
public boolean contains(Object o)
先看变量:
//版本号,用于序列化机制。简单来说,反序列化时,需要验证此变量,
//变量一致才能进行相应的反序列化操作。
private static final long serialVersionUID = 8683452581122892189L;
//默认的初始容量
private static final int DEFAULT_CAPACITY = 10;
//初始容量为0时,elementData指向该数组。
private static final Object[] EMPTY_ELEMENTDATA = {};
//也是个空数组。简单来说,和EMPTY_ELEMENTDATA差异的地方在于:没有定义初始容量时,
//elementData指向该数组。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//元素数组,存储具体数组元素。
transient Object[] elementData;
//元素个数
private int size;
elementData是存储数据的数组,元素类型为Object类型,即可以存放所有类型数据,对ArrayList类的实例的所有的操作底层都是基于该数组的。
elementData通常会预留一些容量,等容量不足时再扩充容量。elementData用transient修饰,其实是处于空间利用的考虑,ArrayList根据真实元素个数size去序列化真实的元素,而不是根据数组的长度去序列化元素,这样就减少了空间占用。
public ArrayList() {
//DEFAULTCAPACITY_EMPTY_ELEMENTDATA是个空数组,但在添加元素时,第一次对数组
//进行扩容时,会将数组容量扩展为10,所以我们此时可以把ArrayList的容量看作10
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
此时可以假设ArrayList的容量为10(因为首次扩容时容量为10),数组中元素的数量size为0:
//构造具有指定初始容量的空列表
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
//初始容量为0时,elementData指向EMPTY_ELEMENTDATA
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
//初始容量<0,抛异常
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public boolean add(E e) {
//判断是否需要扩容,确保处理后的容器能存下size + 1个元素
ensureCapacityInternal(size + 1);
//将元素追加到数组尾部
elementData[size++] = e;
return true;
}
以之前创建的10个容量的ArrayList为例,添加一个元素后的ArrayList:
如果此时再调用add方法先后添加2、3两个元素后,ArrayList会变成:
//在指定位置添加元素
public void add(int index, E element) {
//先判断index位置是否合法
rangeCheckForAdd(index);
//判断是否需要扩容
ensureCapacityInternal(size + 1);
//将index到尾部的元素向后移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//将元素放置在index位置上
elementData[index] = element;
size++;
}
//index参数不合法,就抛IndexOutOfBoundsException
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
之前的"1"、“2”、"3"都是直接追加到ArrayList现有元素的后面的,ArrayList当然还支持在指定位置添加元素,假设调用add(1,4)
方法,就会在下标索引为1的位置添加元素4
。这个过程分为以下几步:
public E remove(int index) {
//校验index是否合法
rangeCheck(index);
modCount++;
//取出index位置元素(为了返回该元素)
E oldValue = elementData(index);
//删除元素后,要移动的元素数
int numMoved = size - index - 1;
//如果删除的元素不是最后一个元素,就要将被删除元素后面的元素向前移动
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将数组元素的最后一个位置置为null,为了GC
elementData[--size] = null;
return oldValue;
}
//index参数超过数组元元素数的最大位置,抛IndexOutOfBoundsException
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
在删除元素后,需要将对应的一个位置或多个位置设置为Null,是为了GC。
- 找出元素所在位置;
- 删除对应位置的元素,其后位置的元素前移。
//删除在数组第一次出现的指定元素
public boolean remove(Object o) {
//判断要删除的元素是否为null
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;
}
以上的两个删除元素的方法,其实大致思路相似,先找到index,再删除对应位置上的元素,图示:
public void clear() {
modCount++;
//清空数组元素,置为null,为了GC
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
public E get(int index) {
//检验位置参数是否合法
rangeCheck(index);
//将该位置元素返回
return elementData(index);
}
//替换指定位置的元素
public E set(int index, E element) {
//先校验位置元素是否合法
rangeCheck(index);
//取出该位置旧元素,放置新元素,再将旧元素返回
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
检索元素的方法有两个:检索元素第一次出现的位置和检索元素最后一次出现的位置。
//从前向后遍历,返回指定元素(或null)第一次出现的位置
public int indexOf(Object o) {
//判断要检索的元素是否为null
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
//从后向前遍历,返回指定元素(或null)最后一次出现的位置
public int lastIndexOf(Object o) {
//判断要检索的元素是否为null
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
自动扩容机制是每次向数组中添加元素前,必须要做的判断操作。先看最常见的添加元素的方法public boolean add(E e) :
public boolean add(E e) {
//自动扩容判断的最开始调用的方法
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
//先判断一下是否是使用无参构造方法创建的ArrayList对象,如果是的话,
//就设置默认数组大小为DEFAULT_CAPACITY(10)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//如果修改后的(最小)数组容量 minCapacity 大于当前的数组长度 elementData.length,
//那么就需要调用grow方法进行扩容,反之则不需要
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
//目前数组长度
int oldCapacity = elementData.length;
//先将当前数组容量扩充至1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//newCapacity:扩容1.5倍后的新数组容量
//minCapacity:数组存储元素所需要的最小容量
//检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么
//就把最小需要容量当作数组的新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新容量大于 MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8),调用
//hugeCapacity方法来
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//将源数组复制到newCapcity大小的新数组上
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//对minCapacity和MAX_ARRAY_SIZE进行比较
//若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
//若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
综上所述,ArrayList自动扩容主要经历了以下几步:
1)
newCapacity
:扩容1.5倍后的新数组容量;
2)minCapacity
:数组存储元素所需要的最小容量,由现有数组元素数量size+1得来。
取两者较大者作为newCapacity,然后与MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)比较。
如果minCapacity > MAX_ARRAY_SIZE,则minCapacity=Integer.MAX_VALUE;
如果minCapacity < MAX_ARRAY_SIZE,则minCapacity=MAX_ARRAY_SIZE。
Array | ArrayList | |
---|---|---|
存储的数据类型 | 可以存储基本数据类型和对象(其实是对象的引用) | 只能存储对象 |
大小 | 指定固定大小 | 动态扩展 |
当存储基本类型数据时,集合使用自动装箱的方式,来减少编码工作量。
常见的方法有以下几种:
List<String> synchronizedList = Collections.synchronizedList(list);
synchronized(list.get()) {
list.get().add(model);
}
List<Object> list1 = new CopyOnWriteArrayList<Object>();
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
Object src : 原数组
int srcPos : 从元数据的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length : 要copy的数组的长度
看个例子:
int[] arrs = {1,2,3,4,5,6,7,8,9,10};
for(int i=0;i<arrs.length;i++)
System.out.print(arrs[i]+" "); //1 2 3 4 5 6 7 8 9 10
System.out.println();
//将arrs数组从下标为5的位置开始复制,总共复制5个元素,
//将上面的元素复制到arrs数组,起始位置的下标为0
System.arraycopy(arrs, 5,arrs, 0,5);
for(int i=0;i<arrs.length;i++)
System.out.print(arrs[i]+" "); //6 7 8 9 10 6 7 8 9 10
public static < T > T[ ] copyOf(T[ ] original, int newLength)
original:要复制的原数组
newLength指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值
返回值:新的数组对象,改变传回数组中的元素值,不会影响原来的数组
看个例子:
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = Arrays.copyOf(arr1, 5);
int[] arr3 = Arrays.copyOf(arr1, 10);
for(int i = 0; i < arr2.length; i++)
System.out.print(arr2[i] + " ");
System.out.println(); //1 2 3 4 5
for(int i = 0; i < arr3.length; i++)
System.out.print(arr3[i] + " "); //1 2 3 4 5 0 0 0 0 0
}
关于扩容因子,有一个很通俗的解释,扩容因子最适合范围为(1, 2)。
为什么不取扩容固定容量呢?扩容的目的需要综合考虑这两种情况:
1、扩容容量不能太小,防止频繁扩容,频繁申请内存空间 + 数组频繁复制
2、扩容容量不能太大,需要充分利用空间,避免浪费过多空间;
扩容固定容量,很难决定到底取多少值合适,取任何具体值都不太合适,因为所需数据量往往由数组的客户端在具体应用场景决定。依赖于当前已经使用的量 * 系数, 比较符合实际应用场景。
为什么是1.5,而不是1.2,1.25,1.8或者1.75?因为1.5可以充分利用移位操作,减少浮点数或者运算时间和运算次数
。
// 新容量计算
int newCapacity = oldCapacity + (oldCapacity >> 1);
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
简单来说,LinkedList 是一个双向链表:
LinkedList中存放的不是普通的某个中类型的元素,而是节点(Node)
,一个节点中包含前驱指针、节点值和后继指针三个部分:
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;
}
}
LinkedList通过prev、next将不连续的内存块串联起来使用。LinkedList是双向链表,除头节点的每一个元素都有prev(前驱指针)同时再指向它的上一个元素,除尾节点的每一个元素都有next(后继指针)同时再指向它的下一个元素。
//构造一个空链表
public LinkedList()
//将元素追加到此链表末尾
public boolean add(E e)
//在链表的指定位置插入元素
public void add(int index, E element)
//在链表的首部插入元素
public void addFirst(E e)
//将元素追加到链表末尾
public void addLast(E e)
//将元素添加到链表的尾部
public boolean offer(E e)
//在链表的首部插入元素
public boolean offerFirst(E e)
//在链表的末尾插入元素
public boolean offerLast(E e)
//检索但不删除链表的首部元素
public E peek()
//在链表的头部添加元素
public void push(E e)
//删除链表中指定位置的元素
public E remove(int index)
//从链表中删除指定元素的第一个出现(如果存在)
public boolean remove(Object o)
//从链表中删除并返回第一个元素
public E removeFirst()
//删除链表中指定元素的第一个出现(从头到尾遍历列表时)
public boolean removeFirstOccurrence(Object o)
//从链表中删除并返回最后一个元素
public E removeLast()
//删除链表中指定元素的最后一次出现(从头到尾遍历列表时)
public boolean removeLastOccurrence(Object o)
//清空链表
public void clear()
//检索并删除链表的首部元素
public E poll()
//检索并删除链表的第一个元素,如果此LinkedList为空,则返回 null
public E pollFirst()
//检索并删除链表的最后一个元素,如果链表为空,则返回 null
public E pollLast()
//删除并返回链表的第一个元素
public E pop()
//检索并删除链表的首部元素
public E remove()
//检索但不删除链表首部元素
public E element()
//返回链表指定位置的元素
public E get(int index)
//返回链表中的第一个元素
public E getFirst()
//返回链表中的最后一个元素
public E getLast()
//检索但不删除链表的第一个元素,如果链表为空,则返回 null
public E peekFirst()
//检索但不删除链表的最后一个元素,如果链表为空,则返回 null
public E peekLast()
//返回链表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1
public int indexOf(Object o)
//返回链表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1
public int lastIndexOf(Object o)
//用指定的元素替换链表中指定位置的元素
public E set(int index, E element)
//返回链表中的元素数
public int size()
//判断链表中是否包含某个元素
public boolean contains(Object o)
//返回ListIterator
public ListIterator< E > listIterator(int index)
变量:
//元素个数
transient int size = 0;
//链表的头结点
transient Node<E> first;
//链表的尾结点
transient Node<E> last;
可以看出:头结点和尾节点的节点类型,都是内部类Node。
因为LinkedList是基于链表的,所以不需要指定初始容量。
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
LinkedList的存储单元为一个名为Node的内部类,包含pre指针,next指针,和item元素。所以向List中添加元素时,会先将要添加的元素封装成Node,再添加到List中。
//add(E e)和addLast(E e)都是将元素添加到链表尾部,
//区别就是一个有boolean返回值,一个没有
public boolean add(E e) {
linkLast(e);
return true;
}
public void addLast(E e) {
linkLast(e);
}
void linkLast(E e) {
//用临时变量l存储尾节点
final Node<E> l = last;
//构造了一个新节点newNode,该节点的前一个节点为l,节点值为e,后一个节点为null
final Node<E> newNode = new Node<>(l, e, null);
//将新创建的newNode作为尾节点
last = newNode;
//如果添加newNode前,之前的尾节点为null,代表链表为bull,因此newNode同时也就成了首节点
if (l == null)
first = newNode;
//否则将newNode追加到原来的尾节点之后
else
l.next = newNode;
//链表容量+1
size++;
//链表操作数+1
modCount++;
}
add(E e) / addLast(E e)
方法的作用都是将元素追加到链表尾部,图示:
push(E e)
方法的作用都是将元素追加到链表首部。 //在链表的头部添加元素
public void push(E e) {
addFirst(e);
}
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
//创建临时变量f存储链表原来的首节点
final Node<E> f = first;
//构造一个新节点newNode,前一个节点为null,该节点值为e,后一个节点为f
final Node<E> newNode = new Node<>(null, e, f);
//将新节点newNode作为first节点
first = newNode;
//如果原有first节点为null,则代表原来链表为null,因此新创建的节点newNode同时为头节点和尾节点
if (f == null)
last = newNode;
//否则新节点newNode作为原来头结点的前一个节点
else
f.prev = newNode;
//链表元素数+1
size++;
modCount++;
}
add(int index, E element)
方法的作用都是将元素追加到链表任意位置。 //在链表的任意位置插入一个元素
public void add(int index, E element) {
//校验index参数
checkPositionIndex(index);
//判断位置参数是否与链表长度一致,如果一致的话,直接将元素追加到链表末尾
if (index == size)
linkLast(element);
//否则就添加到index位置,原来的index位置的元素成为新插入元素的后继元素
else
linkBefore(element, node(index));
}
//在指定节点succ之前插入指定元素e。指定节点succ不能为null
void linkBefore(E e, Node<E> succ) {
//获得指定节点的前驱节点
final Node<E> pred = succ.prev;
//创建新节点newNode,其前驱节点为pred,节点值为e,后继节点为succ
final Node<E> newNode = new Node<>(pred, e, succ);
//succ的前驱节点为新节点newNode
succ.prev = newNode;
//判断原来该位置节点的前驱节点是否为null,如果为null,代表原来该位置的节点succ为首节点
//因此,新创建的节点newNode就成为头节点
if (pred == null)
first = newNode;
//否则为原来该位置节点的前驱节点的后继节点
else
pred.next = newNode;
size++;
modCount++;
}
//在链表的末尾添加集合的全部元素
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
//在链表的任意位置插入集合的全部元素
public boolean addAll(int index, Collection<? extends E> c) {
//检验index参数合法性
checkPositionIndex(index);
//将集合转化成了数组
Object[] a = c.toArray();
//获取要插入集合的元素个数numNew
int numNew = a.length;
if (numNew == 0)
return false;
//pred代表要插入位置的前驱节点,succ代表要插入位置的节点
Node<E> pred, succ;
//如果index和链表的长度相等,那么此时的pred就是原有的last节点,succ为null
//因为链表在get(index)元素时,下标也是从0开始的
if (index == size) {
succ = null;
pred = last;
//否则succ就是原来index位置上的节点,pred就是succ的前驱节点
} else {
succ = node(index);
pred = succ.prev;
}
//迭代的插入元素过程
for (Object o : a) {
@SuppressWarnings("unchecked")
E e = (E) o;
//将集合中的元素取出来,作为新创建的节点的节点值,pred代表要插入位置的前驱节点,后继节点为null
Node<E> newNode = new Node<>(pred, e, null);
//如果pred为null,代表要插入的位置前面没有节点了,所以该节点就要成为头结点
if (pred == null)
first = newNode;
//否则前一个节点的后继节点是新节点
else
pred.next = newNode;
//至关重要的一步,将新插入的节点作为pred节点,以便继续向后增加节点
pred = newNode;
}
//当succ==null,也就是新添加的节点位于LinkedList集合的最后一个元素的后面,
//那么在之前添加的最后一个元素pred就成为了尾节点
if (succ == null) {
last = pred;
//否则succ是pred的后继节点,pred是succ的前驱节点
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
//在链表尾部添加元素
public boolean offer(E e) {
return add(e);
}
//在链表首部添加元素
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
//在链表尾部添加元素
public boolean offerLast(E e) {
addLast(e);
return true;
}
//按索引删除指定元素
public E remove(int index) {
//检验index参数是否合法,不合法就抛出IndexOutOfBoundsException
checkElementIndex(index);
return unlink(node(index));
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//删除节点
E unlink(Node<E> x) {
// assert x != null;
//取出该节点的数据(用于方法最后作为返回值)
final E element = x.item;
//取出该节点的前后节点:prev、next
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//判断该节点的前驱节点是否存在,不存在的话,该节点的后继节点就是first节点
if (prev == null) {
first = next;
//否则就将next节点(要删除的节点的后继节点)赋于原有的前驱节点的后继节点,
//index位置的前驱节点置空
} else {
prev.next = next;
x.prev = null;
}
//判断该节点的后继节点是否存在,如果为空,要删除的节点的前驱节点就变成last节点
if (next == null) {
last = prev;
//否则后继节点的前驱节点直接prev,index位置的后继节点置空
} else {
next.prev = prev;
x.next = null;
}
//将index位置节点的数据置空。这样原有index位置节点的前驱、后继指针及数据
//全都为空,便于GC回收,最后size--,返回原有位置的数据
x.item = null;
size--;
modCount++;
return element;
}
//删掉第一个出现指定数据的节点
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
public boolean remove(Object o) {
//判断要删除的元素是否为null
//如果是null,删除null
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
//如果不是null,则删除对应的元素
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
remove(int index)、removeFirstOccurrence(Object o) /、remove(Object o)
,这些方法可以理解为删除链表中间某个节点,图示:
//删除链表首部元素
public E pop() {
return removeFirst();
}
//删除链表首部元素
public E remove() {
return 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;
final Node<E> next = f.next;
//将原来first节点的数据域和next指针都置空
f.item = null;
f.next = null;
first = next;
//判断原有first的next节点是否为空,如果为空,说明此时链表已空,last = null
if (next == null)
last = null;
//否则不处理last节点,只需将原来的next节点的前驱指针置空即可(这样这个节点就变成了first节点)
else
next.prev = null;
size--;
modCount++;
return element;
}
pop() / remove() / removeFirst()
这些方法的作用是删除链表首部元素,图示:
//删除最后一个节点
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;
//先保存一下last节点的前驱节点,然后将last节点的数据和前驱指针置空
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null;
last = prev;
//判断前驱节点是否为空,如果是的话,就将first节点也置空(此时为空链表)
if (prev == null)
first = null;
//否则将原有前驱节点的后继指针置空
else
prev.next = null;
//size减1
size--;
modCount++;
//返回原有last节点值
return element;
}
removeLast()
方法的作用是删除链表尾部元素,与上面的过程相似,不过操作的位置是链表的尾部,图示:
//删除最后一个出现的指定数据
public boolean removeLastOccurrence(Object o) {
//如果指定元素为null,就从后向前删除null
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
if (x.item == null) {
unlink(x);
return true;
}
}
//如果指定元素不为null,就从后向前删除对应的元素
} else {
for (Node<E> x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
//清空链表
public void clear() {
//每个节点的前驱、后继指针和数据域全部清空,置为null
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
size = 0;
modCount++;
}
//获取指定位置节点元素
public E get(int index) {
//检验index参数合法
checkElementIndex(index);
//按索引获取对应元素
return node(index).item;
}
//获取首节点
public E element() {
return getFirst();
}
//获取首节点
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
//获取尾节点
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
//获取但不删除首节点
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//获取但不删除首节点
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//获取但不删除尾节点
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
//删除并返回首节点
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//删除并返回首节点
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//删除并返回尾节点
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
public E set(int index, E element) {
//检验index参数合法性
checkElementIndex(index);
//取出旧的节点值,替换为新的节点值
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
//将旧的节点值返回
return oldVal;
}
此处有个node(index)方法,之前一直没介绍:
//链表中按索引查找一个节点
Node<E> node(int 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;
}
}
node(index)
方法是根据索引获取链表中元素的方法。由于LinkedList是双向链表,存在从前向后、从后向前两种遍历方式。所以在获取某个位置元素时,就可以根据这个元素的索引在前半部还是后半部来决定采用哪种方式获取元素,这也就是node方法的全部,图示:
//从first节点开始,从前向后检索某个元素所在位置
public int indexOf(Object o) {
int index = 0;
//判断要检索的元素是否为null,是的话检索null,否则检索元素
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
//从last节点开始,从后向前检索某个元素所在位置
public int lastIndexOf(Object o) {
int index = size;
//判断要检索的元素是否为null,是的话检索null,否则检索元素
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
public ListIterator<E> listIterator(int index) {
//检验index参数合法性
checkPositionIndex(index);
//返回一个ListItr对象
return new ListItr(index);
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private class ListItr implements ListIterator<E>
ListItr 类方法较多,不再详细展开,大致介绍一下作用即可,从这些方法中也能看出链表具有双向操作的特点:
//从指定位置开始,构造一个ListItr迭代器对象
ListItr(int index)
//是否还有下一个节点
public boolean hasNext()
//获取下一个节点
public E next()
//是否有上一个节点
public boolean hasPrevious()
//获取上一个节点
public E previous()
//获取下一个节点的索引
public int nextIndex()
//获取前一个节点的索引
public int previousIndex()
//删除最后一个节点
public void remove()
//设置最后一个节点的数据
public void set(E e)
//在制定位置添加元素
public void add(E e)
//按lambda表达式操作链表中的元素
public void forEachRemaining(Consumer<? super E> action)
//获取一个与原链表元素顺序相反的迭代器
public Iterator<E> descendingIterator() {
return new DescendingIterator();
}
private class DescendingIterator implements Iterator<E> {
private final ListItr itr = new ListItr(size());
//判断是否有前驱元素
public boolean hasNext() {
return itr.hasPrevious();
}
//next取的是前驱元素
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
}