Java集合框架-List总结

266 阅读5分钟

一、前言

站在巨人的肩膀上,本系列的Java集合框架文章参考了skywang12345——Java 集合系列,真心讲的很好,可以参考。但是不足的是,时间过于长久了,大佬的文章是基于JDK1.6.0_45,对于现在来说至少都用JDK 8.0以上了,而且JDK 6.0与JDK 8.0中集合框架的改动挺大的,所以本系列的文章是基于JDK_1.8.0_161进行说明的。

二、List概括

前面的文章解析了List的全部内容(ArrayList, LinkedList, Vector, Stack):

Java集合框架-ArrayList的源码解析

Java集合框架-LinkedList的源码解析

Java集合框架-Vector的源码解析

Java集合框架-Stack的源码解析

那我们先来回顾一下List的框架图:

  • List是一个接口,它继承于Collection的接口。它代表着有序的队列。
  • AbstractList是一个抽象类,它继承于AbstractCollection。AbstractList实现List接口中除size()、get(int location)之外的函数。
  • AbstractSequentialList是一个抽象类,它继承于AbstractList。AbstractSequentialList 实现了“链表中,根据index索引值操作链表的全部函数”。
  • ArrayList,LinkedList,Vector,Stack是List的4个实现类。
  • ArrayList是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。
  • LinkedList是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。
  • Vector是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。
  • Stack是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。

三、List使用场景

如果涉及到“栈”、“队列”、“链表”等操作,应该考虑用List,具体的选择哪个List,根据下面的标准来取舍。
1、对于需要快速插入,删除元素,应该使用LinkedList。
2、对于需要快速随机访问元素,应该使用ArrayList。
3、对于单线程环境,如果List仅仅只会被单个线程操作,此时应该使用非同步的类(如ArrayList)。 4、对于多线程环境,且List可能同时被多个线程操作,此时,应该使用同步的类(如Vector)。

下面通过一个具体的例子,来验证上面的(1)和(2)结论。代码如下:

public class Main {

	private static final int COUNT = 100000;

	private static LinkedList<Integer> linkedList = new LinkedList<>();
	private static ArrayList<Integer> arrayList = new ArrayList<>();
	private static Vector<Integer> vector = new Vector<>();
	private static Stack<Integer> stack = new Stack<>();

	public static void main(String[] args) {
		// 插入
		insertByPosition(stack);
		insertByPosition(vector);
		insertByPosition(linkedList);
		insertByPosition(arrayList);
		System.out.println();
		
		// 随机读取
		readByPosition(stack);
		readByPosition(vector);
		readByPosition(linkedList);
		readByPosition(arrayList);
		System.out.println();
		
		// 删除
		deleteByPosition(stack);
		deleteByPosition(vector);
		deleteByPosition(linkedList);
		deleteByPosition(arrayList);
	}

	// 获取list的名称
	private static String getListName(List<Integer> list) {
		if (list instanceof LinkedList) {
			return "LinkedList";
		} else if (list instanceof ArrayList) {
			return "ArrayList";
		} else if (list instanceof Stack) {
			return "Stack";
		} else if (list instanceof Vector) {
			return "Vector";
		} else {
			return "List";
		}
	}

	// 向list的指定位置插入COUNT个元素,并统计时间
	private static void insertByPosition(List<Integer> list) {
		long startTime = System.currentTimeMillis();
		// 向list的位置0插入COUNT个数
		for (int i = 0; i < COUNT; i++) {
			list.add(0, i);
		}
		long endTime = System.currentTimeMillis();
		long interval = endTime - startTime;
		System.out.println(getListName(list) + " : insert " + COUNT + " elements into the 1st position use time:" + interval + " ms");
	}

	// 从list的指定位置删除COUNT个元素,并统计时间
	private static void deleteByPosition(List<Integer> list) {
		long startTime = System.currentTimeMillis();
		// 删除list第一个位置元素
		for (int i = 0; i < COUNT; i++) {
			list.remove(0);
		}
		long endTime = System.currentTimeMillis();
		long interval = endTime - startTime;
		System.out.println(getListName(list) + " : delete " + COUNT + " elements from the 1st position use time:" + interval + " ms");
	}

	// 根据position,不断从list中读取元素,并统计时间
	private static void readByPosition(List<Integer> list) {
		long startTime = System.currentTimeMillis();
		// 读取list元素
		for (int i = 0; i < COUNT; i++) {
			list.get(i);
		}
		long endTime = System.currentTimeMillis();
		long interval = endTime - startTime;
		System.out.println(getListName(list) + " : read " + COUNT + " elements by position use time:" + interval + " ms");
	}
}

运行结果如下:

Stack : insert 100000 elements into the 1st position use time420 ms
Vector : insert 100000 elements into the 1st position use time433 ms
LinkedList : insert 100000 elements into the 1st position use time7 ms
ArrayList : insert 100000 elements into the 1st position use time425 ms

Stack : read 100000 elements by position use time4 ms
Vector : read 100000 elements by position use time3 ms
LinkedList : read 100000 elements by position use time4327 ms
ArrayList : read 100000 elements by position use time1 ms

Stack : delete 100000 elements from the 1st position use time436 ms
Vector : delete 100000 elements from the 1st position use time422 ms
LinkedList : delete 100000 elements from the 1st position use time4 ms
ArrayList : delete 100000 elements from the 1st position use time431 ms

根据输出结果,我们可以发现:
插入10万个元素,LinkedList所花时间最短:7ms。
删除10万个元素,LinkedList所花时间最短:4ms。
遍历10万个元素,LinkedList所花时间最长:4327ms;而ArrayList、Stack和Vector则相差不多,都只用了几毫秒。

考虑到Vector是支持同步的,而Stack又是继承于Vector的;因此,得出结论:
1、对于需要快速插入,删除元素,应该使用LinkedList。
2、对于需要快速随机访问元素,应该使用ArrayList。
3、对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类。

四、LinkedList和ArrayList性能差异分析

下面我们看看为什么LinkedList中插入元素很快,而ArrayList中插入元素很慢!先来看看LinkedList向指定位置插入元素的方法:add(int index, E element)。

public void add(int index, E element) {
	checkPositionIndex(index);
	if (index == size)
    	//如果索引等于长度,则直接添加到列表末尾
		linkLast(element);
	else
		linkBefore(element, 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;
	}
}

// 将节点(节点数据是e)添加到succ节点之前。
void linkBefore(E e, Node<E> succ) {
	//获取succ的前节点
	final Node<E> pred = succ.prev;
    //创建一个新节点,并把新节点的前节点设置成succ的前节点,把新节点的后节点设置成succ节点
	final Node<E> newNode = new Node<>(pred, e, succ);
    //把succ的前节点设置成新节点
	succ.prev = newNode;
	if (pred == null)
		first = newNode;
	else
		pred.next = newNode;
	size++;
	modCount++;
}

从中,我们可以看出:通过add(int index, E element)向LinkedList插入元素时。先是在双向链表中找到要插入节点的位置index;找到之后,再插入一个新节点。双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。

接着,我们看看ArrayList.java中向指定位置插入元素的代码。如下:

// 将element添加到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++;
}

ensureCapacityInternal(size + 1)的作用是:确认ArrayList的容量,若容量不够,则增加容量。真正耗时的操作是:System.arraycopy(elementData, index, elementData, index + 1,size - index)这句代码。

public static native void arraycopy(Object src,  int  srcPos,Object dest, int destPos,int length);

由于arraycopy方法是个JNI函数,看不到其中的源码,想要继续了解System.arraycopy源码的可以自己去找找。这就意味着,ArrayList的add(int index, E element)函数,会引起index之后所有元素移动!

通过上面的分析,我们就能理解为什么LinkedList中插入元素很快,而ArrayList中插入元素很慢。是由于LinkedList插入元素只需要改变节点的前后节点的指向,无需对数组数据进行移动。删除元素与插入元素的原理类似,这里就不再过多说明。

接下来,我们看看 “为什么LinkedList中随机访问很慢,而ArrayList中随机访问很快”。先看看LinkedList随机访问的代码:

//根据特定的索引,返回元素值
public E get(int index) {
	//检查下标是否合法
	checkElementIndex(index);
    //返回节点的元素值
	return node(index).item;
}

//根据下标返回元素节点
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;
	}
}

从中,我们可以看出:通过get(int index)获取LinkedList第index个元素时。先是在双向链表中找到要index位置的元素;找到之后再返回。双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。

下面看看ArrayList随机访问的代码:

//根据特定的索引,返回元素值
public E get(int index) {
	//检查下标是否合法
	rangeCheck(index);
	return elementData(index);
}

//返回数组中index索引的值
E elementData(int index) {
	return (E) elementData[index];
}

从中,我们可以看出:通过get(int index)获取ArrayList第index个元素时。直接返回数组中index位置的元素,而不需要像LinkedList一样进行遍历查找。

五、Vector和ArrayList比较

相同的地方:

  • 它们都是List。它们都继承于AbstractList,并且实现List接口。
  • 它们都实现了RandomAccess、Cloneable、Serializable接口。实现RandomAccess接口,意味着它们都支持快速随机访问;实现Cloneable接口,意味着它们能克隆自己;实现了Serializable接口,意味着它们能进行序列化传输。
  • 它们都是通过数组实现的,本质上都是动态数组。
  • 它们数组的初始容量都是10。只不过Vector是在构造方法中,而ArrayList是在第一次添加元素的时候,当然前提都是调用无参数的构造方法。
  • 它们都支持Iterator和listIterator遍历。它们都继承于AbstractList,而AbstractList中分别实现了iterator()接口返回Iterator迭代器listIterator()返回ListIterator迭代器

不同的地方:

  • 线程安全性不一样。ArrayList是非线程安全的;而Vector是线程安全的,它的函数都是synchronized的,即都是支持同步的。ArrayList适用于单线程,Vector适用于多线程。
  • 构造方法个数不同。Vector有4个构造方法,ArrayList有3个。
  • 容量增加方式不同。当ArrayList容量不足时,新容量=旧容量+旧容量/2,即扩容为原来的1.5倍;当Vector容量不足时分2种情况,一种是调用了构造方法时传入了容量增量,那么新容量=旧容量+容量增量,一种是调用无参的构造方法或者其他构造方法时,新容量=旧容量+旧容量,即扩容为原来的2倍。
  • 对Enumeration的支持不同。Vector支持通过Enumeration去遍历,而List不支持。

六、参考

Java 集合系列08之 List总结(LinkedList, ArrayList等使用场景和性能分析)