List集合在我们编码过程中用的比较多的是ArrayList和LinkedList,前文中我们已经讲过两个集合的底层数据结构 这里我们在来回顾一下
ArrayList 底层数据结构是一个可变的数组,当数组长度不够继续存储元素的时候,他会自己扩容,扩容的倍数是0.5倍。
LinkedList 底层数据结构是无界链表,由于他实现了Deque接口可以当做一个双端队列 和 一个堆栈。
大部分开发人员都应该直到ArrayList的插入速度不如LinkedList,这是为什么呢?这就是和这两个集合的底层数据结构有关系,因为ArrayList是一个动态的数组结构,当当前长度不够存放元素的时候 ArrayList会调用System的arrayCopy方法对数组进行扩容,这个方法是很消耗性能的。他要在内存中构建一个新的连续存放空间,然后将老数组的数据 依次移入到新空间 这个过程是很慢的。而LinkedList是一个链表 链表的每个结点都有一个指向下个一个结点的指针,当我们要插入数据只需要把原来的指针断开指向需要插入的结点 然后将新的结点指向 被断开后 另外一部分链表,这个过程就很简单 只是更改一下原来结点中指针数据。所以速度要比ArrayList快的多。删除的话 ArrayList删除末尾元素还是比较快 直接将末尾索引位置设置为null即可,但是删除数组中间元素的话,后部分元素都要往前移动一位,这个过程也是比较慢的。那看看LinkedList删除元素 也是将被删除的结点位置的前后结点指向这个被删除的结点指针断开,然后前后两个节点的指针重新链接就可以了,这个时候被删除的结点就游离在链表之外了。
现在我们来分析一下ArrayList和LinkedList的查询速度
ArrayList是一个数组 数组在内存中式一个连续的线性存储空间,所以我们可以通过所以位置直接定位到数组中元素 这是非常快的。
LinkedList是一个链表 ,在内存中是非连续的存储空间,所以在查询元素的时候只能从链表的头部或者尾部依次遍历过去寻找。如果要查找的元素在头部或者尾部那和数组就差不多,但是如果要查找的元素在链表中间 那这个查询过程就是需要花费一定的时间下面附上LinkedList的查询源码
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
//判断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;
}
}
综上比较分析 当我们在使用ArrayList的时候要避免扩容操作的发生,所以在构建ArrayList对象的时候给他一个容量初始值,这个值也不能太大,太大了浪费内存,太小了频繁扩容影响效率,要根据自己的业务量设置合理的初始容量值。如果当前操作主要是查询操作就选用ArrayList,如果主要操作是增删操作就选用LinkedList
ArrayList和LInkedList的遍历分析
ArrayList和LinkedList都有两种遍历方式 一种是通过迭代器遍历 一种就是for循环遍历,但是当我们需要遍历List集合的时候到底选用哪种遍历方式呢?是用迭代还是用for循环呢?还是两种随意使用呢?抛出了这个问题,下面先看一段Collections的源码预热一下
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
//对于是否实现RandomAccess接口采用两种查找方式
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
//直接通过索引去定位
return Collections.indexedBinarySearch(list, key);
else
//通过迭代器去定位
return Collections.iteratorBinarySearch(list, key);
}
private static <T>
int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key)
{
int low = 0;
int high = list.size()-1;
while (low <= high) {
int mid = (low + high) >>> 1;
Comparable<? super T> midVal = list.get(mid);
int cmp = midVal.compareTo(key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found
}
private static <T>
int iteratorBinarySearch(List<? extends Comparable<? super T>> list, T key)
{
int low = 0;
int high = list.size()-1;
ListIterator<? extends Comparable<? super T>> i = list.listIterator();
while (low <= high) {
int mid = (low + high) >>> 1;
Comparable<? super T> midVal = get(i, mid);
int cmp = midVal.compareTo(key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found
}
在集合的操作工具类Collections中 有一个二分查找法,在这个方法中对于是否实现了RandomAccess接口的List集合分别采用了两个方法去查找。在jdk中RandomAccess是一个空接口 没有抽象方法,只是当做一个标记mark,这个标记代表集合支持快速的随机访问。在ArrayList中实现了此接口,表示ArrayList可以快速随机访问,LinkedList没有实现此接口,说明LinkedList不支持快速随机访问。快速随机访问最好的表现就是通过数组索引下标去获取元素,和ArrayList的get(int index)方法是一样的。上面的代码中为什么没有实现RaddomAccess方法的List集合要使用迭代器去查找元素呢,而不使用for循环呢。接下来我附上LinkedList的迭代器实现大家应该就会很清楚了。
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned = null;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
}
//两个迭代器遍历比较
LinkedList list = new LinkedList();
for(int i=0;i<100000;i++){
list.add(i);
}
Iterator it = list.listIterator();
long time = System.currentTimeMillis();
//迭代器遍历
while (it.hasNext()){
it.next();
}
System.out.println("迭代器遍历花费的时间"+(System.currentTimeMillis()-time));
//for循环遍历
long time2 = System.currentTimeMillis();
for(int i=0;i<list.size();i++){
//get方法底层调用的是node(int index)方法,这个方法在前文分析LInkedList方法的时
//分析过了,就是从链表的头部或者尾部查询,直至查询到索引为index位置
list.get(i);
}
System.out.println("for循环遍历花费的时间"+(System.currentTimeMillis()-time2));
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;
}
}
迭代器遍历花费的时间0
for循环遍历花费的时间5148
上面很明显for循环的时间要远远大于迭代器,从代码中发现for循环因为获取元素使用的是get(int index) 方法 这个方法调用的是linkedList的node的方法 这个node(int index)方法 是每次都要从链表的表头或者表尾开始寻找 一直寻找到index位置,然而通过迭代器的next()方法,只是通过Node结点的指针获取下一个元素就这么简单。for循环就相当于走的是两层for循环 所有for循环时间花费的要长。所以在LinkedList遍历的时候最好使用迭代器,这也是Collections.binarySearch(List<? extends Comparable<? super T>> list, T key) 方法的时候先要判断接口是否实现了RandomAeeess,没有实现的话走迭代器查找。
下面我们看看ArrayList的for循环和迭代器循环耗时情况
在这里插入代码片
ArrayList list = new ArrayList();
for(int i=0;i<10000000;i++){
list.add(i);
}
Iterator it = list.listIterator();
long time = System.currentTimeMillis();
//迭代器遍历
while (it.hasNext()){
//next()方法和下面for循环中的get方法最终调用的是elementData[index]
//但是next()方法中间多了几步其他操作所以耗时要比get方法长
it.next();
}
System.out.println("迭代器遍历花费的时间"+(System.currentTimeMillis()-time));
//for循环遍历
long time2 = System.currentTimeMillis();
for(int i=0;i<list.size();i++){
list.get(i);
}
System.out.println("for循环遍历花费的时间"+(System.currentTimeMillis()-time2));
迭代器遍历花费的时间11
for循环遍历花费的时间8
//arrayList中迭代器的next方法
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
//ArrayList中的get()方法
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
通过上面代码我们可以反向在ArrayList中迭代器要比for循环花费的时间还要长一点,这是因为it.next()方法执行的过程中比get()方法多了几步其他操作,所以迭代器所花费的时间比for循环要长。
通过以上比较,在List集合中for循环和迭代器循环,不能直接断点谁的速度更快一点。例如LinkedList的迭代器就比for循环快,但是ArrayList的for循环却比迭代器要快,所以具体使用那种循环方式要看List集合的具体实现,从官方api中发现实现了RandomAccess接口的List集合都支持快速随机访问,所以在Conllections中的二分查找方法中Collections.binarySearch(List<? extends Comparable<? super T>> list, T key) 发现没有实现RadomAccess的List集合在遍历查询的时候选择的是迭代器去遍历。所以我们可以根据List的集合有没有实现RandomAccess接口来判断for循环和迭代器的快慢。
LinkedList和ArrayList都是线程不安全的List集合,在多线程环境下使用我们可以通过Collections.synchronizedList(List list) 来构造出一个线程安全的List集合
以上这些就是LinkedList和ArrayList的异同。
下面我们来看看ArrayList和Vector的区别
Vector也是一个List集合 他和ArrayList具有相同的继承机构体系,最大的不同之处就是Vector是线程安全的,Vector和ArrayList的增删改查代码实现几乎一致,只是在Vector中的方法上加了synchronized,所以Vector是一个线程安全的List集合。
值得注意的是Vector的扩容机制和ArrayList是不同的
//Vector的扩容方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//capacityIncrement这个是否为0 去判断扩容是扩大一倍还是扩大capacityIncrement值代表的长度
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
//ArrrayList的扩容方法
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);
}
ArrayList就是按照0.5倍扩容 而 Vector先要判断capacityIncrement值是否为0
如果不为0 那么 Vector的扩容就是原先数组长度length+capacityIncrement,如果
capacityIncrement等于0 就是原先数组长度length+length 就是增加1倍
Vector在遍历的方法中除了for循环 迭代器Iterator 还有一个Enumeration 遍历方法
Vector list = new Vector();
for(int i=0;i<1000;i++){
list.add(i);
}
Enumeration e = list.elements();
while(e.hasMoreElements()){
e.nextElement();
}
这个遍历方法是比较古老的,在jdk早起版本的集合中使用 在1.2 版本之后就基本后出现了Itreator 后 集合遍历都开始使用Itreator 而放弃使用Enumeration。
比较常见的不同之处就是以上几点,在具体的细节就得从源码逐一分析。在这里我在总结一下,加深一点印象。
- ArrayList 底层是可变数组,所以查询比较快,同时由于他的扩容机制,导致数据插入比较慢,他是一个线程不安全的List集合。
- LinkedList 底层是一个无界的双向链表,链表的查询都是从表头或者表尾开始,所以链表的查询比较慢,但是链表的增删和数组不一样,只是更改一下结点指针的指向,所以链表增删快。
- 在遍历的情况上来说都支持for循环迭代器遍历,从有无实现RandomAccess接口来看,LInkedList不支持快速的随机访问,所以LinkedList的for循环效率远远低于迭代器遍历。然而ArrayList的for循环速度要稍稍优于迭代器的遍历
- LinkedList和ArrayList都是线程不安全的List的集合 可以通过Collections的同步方法构造出线程安全的List集合
- Vector 底层是可变数组 他的实现基本上和ArrayList一样,但是Vector是一个线程安全的类,他的增删改方法都加了synchornzied关键字进行同步。同时他比ArrayList多了一个遍历的方式 通过Enumration进行遍历