在路上

 找回密码
 立即注册
在路上 站点首页 学习 查看内容

分析Java中ArrayList与LinkedList列表结构的源码

2016-7-29 15:34| 发布者: zhangjf| 查看: 673| 评论: 0

摘要: 一、ArrayList源码分析(JDK7) ArrayList内部维护了一个动态的Object数组,ArrayList的动态增删就是对这个对组的动态的增加和删除。 1、ArrayList构造以及初始化 ArrayList实例变量//ArrayList默认容量private stat ...

一、ArrayList源码分析(JDK7)

ArrayList内部维护了一个动态的Object数组,ArrayList的动态增删就是对这个对组的动态的增加和删除。

1、ArrayList构造以及初始化

  1. ArrayList实例变量
  2. //ArrayList默认容量
  3. private static final int DEFAULT_CAPACITY = 10;
  4. //默认空的Object数组, 用于定义空的ArrayList
  5. private static final Object[] EMPTY_ELEMENTDATA = {};
  6. //ArrayList存放存放元素的Object数组
  7. private transient Object[] elementData;
  8. //ArrayList中元素的数量
  9. private int size;
复制代码

ArrayList构造函数:

无参构造函数: 即构造一个空的Object[]

  1. public ArrayList() {
  2. super();
  3. this.elementData = EMPTY_ELEMENTDATA;
  4. }
复制代码

指定容量大小构造:

  1. public ArrayList(int initialCapacity) {
  2. super();
  3. if (initialCapacity < 0)
  4. throw new IllegalArgumentException("Illegal Capacity: "+
  5. initialCapacity);
  6. this.elementData = new Object[initialCapacity];
  7. }
复制代码

指定某一实现Collection接口的集合构造:

  1. public ArrayList(Collection<? extends E> c) {
  2. elementData = c.toArray();
  3. size = elementData.length;
  4. // c.toArray might (incorrectly) not return Object[] (see 6260652)
  5. if (elementData.getClass() != Object[].class)
  6. elementData = Arrays.copyOf(elementData, size, Object[].class);
  7. }
复制代码

这里也说明了Collection的作用, java-collection-framwork设计Collection接口而不是直接使用List,Set等接口的原因。

2、ArrayList的容量分配机制

ArrayList的容量上限: ArrayList容量是有上限的,理论允许分配Integer.Max_VALUE - 8大小的容量。但是能分配多少还跟堆栈设置有关, 需要设置VM参数

  1. private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
复制代码

调用Add方法时扩容规则

  1. public boolean add(E e) {
  2. ensureCapacityInternal(size + 1); // Increments modCount!!
  3. elementData[size++] = e;
  4. return true;
  5. }
复制代码

ensureCapacityInternal(int)方法实际上确定一个最小扩容大小。

  1. private void ensureCapacityInternal(int minCapacity) {
  2. if (elementData == EMPTY_ELEMENTDATA) {
  3. minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
  4. }
  5. ensureExplicitCapacity(minCapacity);
  6. }
  7. private void ensureExplicitCapacity(int minCapacity) {
  8. modCount++;
  9. // overflow-conscious code
  10. if (minCapacity - elementData.length > 0)
  11. grow(minCapacity);
  12. }
复制代码

关于modCount: modCount定义在抽象类AbstratList中, 源码的注释基本说明了它的用处:在使用迭代器遍历的时候,用来检查列表中的元素是否发生结构性变化(列表元素数量发生改变的一个计数)了,主要在多线程环境下需要使用,防止一个线程正在迭代遍历,另一个线程修改了这个列表的结构。
grow方法为真正的扩容方法

  1. private void grow(int minCapacity) {
  2. // overflow-conscious code
  3. int oldCapacity = elementData.length;
  4. int newCapacity = oldCapacity + (oldCapacity >> 1);
  5. if (newCapacity - minCapacity < 0)
  6. newCapacity = minCapacity;
  7. if (newCapacity - MAX_ARRAY_SIZE > 0)
  8. newCapacity = hugeCapacity(minCapacity);
  9. // minCapacity is usually close to size, so this is a win:
  10. elementData = Arrays.copyOf(elementData, newCapacity);
  11. }
复制代码

其中对大容量扩容多少还有个hugeCapacity方法

  1. private static int hugeCapacity(int minCapacity) {
  2. if (minCapacity < 0) // overflow
  3. throw new OutOfMemoryError();
  4. return (minCapacity > MAX_ARRAY_SIZE) ?
  5. Integer.MAX_VALUE :
  6. MAX_ARRAY_SIZE;
  7. }
复制代码

总结:
每次扩容都会伴随着数组的复制操作, 因此一次给定恰当的容量会提高一点性能。
下图是我归纳的整个扩容流程:

201651185223963.png (3184×650)

3.ArrayList迭代器

ArrayList的迭代器主要有两种Itr和ListItr, 但是在jDK1.8中还添加了一个ArrayListSpliterator, 下面分别学一下Itr和ListItr的源码分析。

(1)Itr:只能向后遍历

  1. private class Itr implements Iterator<E> {
  2. int cursor; // index of next element to return
  3. int lastRet = -1; // index of last element returned; -1 if no such
  4. //expectedModCount 是modCount的一个副本
  5. int expectedModCount = modCount;
  6. public boolean hasNext() {
  7. return cursor != size;
  8. }
  9. @SuppressWarnings("unchecked")
  10. public E next() {
  11. checkForComodification();
  12. //记录当前位置
  13. int i = cursor;
  14. if (i >= size)
  15. throw new NoSuchElementException();
  16. Object[] elementData = ArrayList.this.elementData;
  17. if (i >= elementData.length)
  18. throw new ConcurrentModificationException();
  19. //下一个元素的位置
  20. cursor = i + 1;
  21. return (E) elementData[lastRet = i];
  22. }
  23. //使用迭代器的remove方法
  24. public void remove() {
  25. if (lastRet < 0)
  26. throw new IllegalStateException();
  27. checkForComodification();
  28. try {
  29. //注意内部类调用外部类的方式
  30. ArrayList.this.remove(lastRet);
  31. //remove之后需要重新调整各个指针的位置
  32. cursor = lastRet;
  33. lastRet = -1;
  34. expectedModCount = modCount;
  35. } catch (IndexOutOfBoundsException ex) {
  36. throw new ConcurrentModificationException();
  37. }
  38. }
  39. final void checkForComodification() {
  40. if (modCount != expectedModCount)
  41. throw new ConcurrentModificationException();
  42. }
  43. }
复制代码

从源码中可以看出Itr迭代器是向前迭代器, 它提供了一个next方法用于获取ArrayList中的元素。
checkForComodification是java-collection-framwork中的一种fail-fast的错误检测机制。在多线程环境下对同一个集合操作,就可能触发fail-fast机制, 抛出ConcurrentModificationException异常。

Itr迭代器定义了一个expectedModCount记录modCount副本。在ArrayList执行改变结构的操作的时候例如Add, remove, clear方法时modCount的值会改变。

通过Itr源码可以看出调用next和remove方法会触发fail-fast检查。此时如果在遍历该集合时, 存在其他线程正在执行改变该集合结构的操作时就会发生异常。

(2)ListItr:支持向前和向后遍历,下面看看ListItr的源码:

  1. private class ListItr extends Itr implements ListIterator<E> {
  2. ListItr(int index) {
  3. super();
  4. cursor = index;
  5. }
  6. public boolean hasPrevious() {
  7. return cursor != 0;
  8. }
  9. public int nextIndex() {
  10. return cursor;
  11. }
  12. public int previousIndex() {
  13. return cursor - 1;
  14. }
  15. @SuppressWarnings("unchecked")
  16. public E previous() {
  17. checkForComodification();
  18. //arrayList前一个元素的位置
  19. int i = cursor - 1;
  20. if (i < 0)
  21. throw new NoSuchElementException();
  22. Object[] elementData = ArrayList.this.elementData;
  23. if (i >= elementData.length)
  24. throw new ConcurrentModificationException();
  25. cursor = i;
  26. return (E) elementData[lastRet = i];
  27. }
  28. //该迭代器中添加了set方法
  29. public void set(E e) {
  30. if (lastRet < 0)
  31. throw new IllegalStateException();
  32. checkForComodification();
  33. try {
  34. ArrayList.this.set(lastRet, e);
  35. } catch (IndexOutOfBoundsException ex) {
  36. throw new ConcurrentModificationException();
  37. }
  38. }
  39. //该迭代器添加了add方法
  40. public void add(E e) {
  41. checkForComodification();
  42. try {
  43. int i = cursor;
  44. ArrayList.this.add(i, e);
  45. //重新标记指针位置
  46. cursor = i + 1;
  47. lastRet = -1;
  48. expectedModCount = modCount;
  49. } catch (IndexOutOfBoundsException ex) {
  50. throw new ConcurrentModificationException();
  51. }
  52. }
  53. }
复制代码

ListItr的实现基本与Itr一致, 添加了可以先前遍历的方法以及add与set方法。

(3)使用java.util.concurrent中的CopyOnWriteArrayList解决fast-fail问题

CopyOnWriteArrayList是线程安全的, 具体看一下它的add方法源码:

  1. public boolean add(E e) {
  2. final ReentrantLock lock = this.lock;
  3. lock.lock();
  4. try {
  5. Object[] elements = getArray();
  6. int len = elements.length;
  7. Object[] newElements = Arrays.copyOf(elements, len + 1);
  8. newElements[len] = e;
  9. setArray(newElements);
  10. return true;
  11. } finally {
  12. lock.unlock();
  13. }
  14. }
复制代码

CopyOnWriteArrayList就是写时复制的ArrayList。当开始写数据的操作时候, Arrays.copyOf一个新的数组, 这样不会影响读操作。
这样的代价就是会损耗内存, 带来性能的问题。CopyOnWriteArrayList写的时候在内存中生成一个副本对象, 同时原来的对象仍然存在。
CopyOnWriteArrayList无法保证数据实时的一致, 只能保证结果的一致。适用于并发下读多写少得场景, 例如缓存。

(4)ArrayList的其他方法源码:

一个私有方法batchRemove(Collectionc, boolean complement), 即批量移除操作

  1. private boolean batchRemove(Collection<?> c, boolean complement) {
  2. //下面会提到使用final的原因
  3. final Object[] elementData = this.elementData;
  4. int r = 0, w = 0;
  5. boolean modified = false;
  6. try {
  7. //遍历List中的元素,进行验证
  8. for (; r < size; r++)
  9. if (c.contains(elementData[r]) == complement)
  10. elementData[w++] = elementData[r];
  11. } finally {
  12. //try中如果出现异常,则保证数据一致性执行下面的copy操作
  13. if (r != size) {
  14. System.arraycopy(elementData, r,
  15. elementData, w,
  16. size - r);
  17. w += size - r;
  18. }
  19. //清理无用的元素, 通知GC回收
  20. if (w != size) {
  21. // clear to let GC do its work
  22. for (int i = w; i < size; i++)
  23. elementData[i] = null;
  24. modCount += size - w;
  25. size = w;
  26. modified = true;
  27. }
  28. }
  29. return modified;
  30. }
复制代码

final修饰的变量指的是同一个引用, 为了后面保持数据的一致性。
此方法,想保留Collection c中的元素时, complement值为true; 想移除c中的元素时, complement值为false。这样就分别变成了retainAll和removeAll方法。

swap:交换arrayList中的某两个位置的

二、LinkedList源码分析(JDK7)

LinkedList即链表, 相对于顺序表, 链表存储数据不需要使用地址连续的内存单元。减少了修改容器结构而带来的移动元素的问题,顺序访问相对高效。

1、结点(Node)的定义

JDK中的LinkedList是双向链表, 每个结点分别存有上一个结点和下一个结点的信息。它的定义如下:

  1. private static class Node<E> {
  2. E item;
  3. Node<E> next;
  4. Node<E> prev;
  5. Node<E> (Node<E> prev, E element, Node<E> next) {
  6. this.item = element;
  7. this.next = next;
  8. this.prev = prev;
  9. }
  10. }
复制代码

2、LinkedList构造以及初始化

成员: LinkedList中维护了3个成员变量, 用以记录链表中结点的个数, 结点的前驱以及后继

  1. transient int size = 0;
  2. transient Node<E> first;
  3. transient Node<E> last;
复制代码

构造函数: 默认构造函数即构造一个空的LinkedList

  1. public LinkedList() {}
复制代码

或者根据其他容器进行构造, 后面我们会自己写一个构造一个有序的链表

  1. public LinkedList(Collection<? extends E> c) {
  2. this();
  3. addAll(c);
  4. }
复制代码

这里给出一点补充, 关于泛型修饰符? super T 与 ? extends T的区别,参见这篇文章泛型中? super T和? extends T的区别

3、LinkedList的结构操作

头插法: 即在链表头插入一个元素

  1. private void linkFirst(E e) {
  2. final Node<E> f = first;
  3. final Node<E> newNode = new Node<>(null, e, f);
  4. first = newNode;
  5. //判断是否是空链表
  6. if (f == null)
  7. last = newNode;
  8. else
  9. f.prev = newNode;
  10. size++;
  11. modCount++;
  12. }
复制代码

尾插法: 即在链表尾部插入一个元素

  1. void linkLast(E e) {
  2. final Node<E> l = last;
  3. final Node<E> newNode = new Node<>(l, e, null);
  4. last = newNode;
  5. if (l == null)
  6. first = newNode;
  7. else
  8. l.next = newNode;
  9. size++;
  10. modCount++;
  11. }
复制代码

插入到当前结点之前: 找当前结点的前驱

  1. void linkBefore(E e, Node<E> succ) {
  2. //确定当然结点非空
  3. final Node<E> pred = succ.prev;
  4. final Node<E> newNode = new Node<>(pred, e, succ);
  5. succ.prev = newNode;
  6. //判断当前结点是否是第一个结点
  7. if (pred == null)
  8. first = newNode;
  9. else
  10. pred.next = newNode;
  11. size++;
  12. modCount++;
  13. }
复制代码

头删法: 删除链表的第一个结点

  1. private E unlinkFirst(Node<E> f) {
  2. // assert f == first && f != null;
  3. final E element = f.item;
  4. final Node<E> next = f.next;
  5. f.item = null;
  6. f.next = null; // help GC
  7. first = next;
  8. if (next == null)
  9. last = null;
  10. else
  11. next.prev = null;
  12. size--;
  13. modCount++;
  14. return element;
  15. }
复制代码

尾删法:删除链表的最后一个结点

  1. private E unlinkLast(Node<E> l) {
  2. //保证l==last 并且l != null
  3. final E element = l.item;
  4. final Node<E> prev = l.prev;
  5. l.item = null;
  6. l.prev = null; // help GC
  7. last = prev;
  8. if (prev == null)
  9. first = null;
  10. else
  11. prev.next = null;
  12. size--;
  13. modCount++;
  14. return element;
  15. }
复制代码

4、保持List接口与Deque的一致性

List接口允许使用下标来实现对容器的随机访问,对于数组这种实现随机访问是很容易的。对于链表,JDK也从逻辑上利用链表中结点的计数给出了随机访问的实现

  1. Node<E> node(int index) {
  2. //确保index的正确性
  3. if (index < (size >> 1)) {
  4. Node<E> x = first;
  5. for (int i = 0; i < index; i++)
  6. x = x.next;
  7. return x;
  8. } else {
  9. Node<E> x = last;
  10. for (int i = size - 1; i > index; i--)
  11. x = x.prev;
  12. return x;
  13. }
  14. }
复制代码

index 属于前半部分的计数, 从头遍历查找。index属于后半部分的计数, 从末尾遍历查找。充分利用双向链表的特点。
因此,add(int index, T t), get(int), set(int)等方法就可以很容易的实现。

LinkedList实现了Deque接口, 也就是LinkedList实现了双端队列容器的方法,下面给出一些API的总结。

5、LinkedList的遍历

既然LinkedList是双向链表, 自然就可以前后遍历。与ArrayList同样, 涉及到多线程操作容器的时候LinkedList也会出现fail-fast问题。
对于fail-fast问题上篇已经讲解过, 这里就不说了。

关于迭代器,LinkedList有listIterator双向迭代器, 和DescendingIterator逆序迭代器。都很简单。源码不在分析

如果遍历元素的话, 随机访问的代价是比较大得。

三、LinkedList,ArrayList, Vector总结

1、LinkedList与ArrayList

ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。

对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。

对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。

2、ArrayList与Vector

vector是线程同步的,所以它也是线程安全的,而arraylist是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用arraylist效率比较高。

如果集合中的元素的数目大于目前集合数组的长度时,vector增长率为目前数组长度的100%,而arraylist增长率为目前数组长度的50%.如过在集合中使用数据量比较大的数据,用vector有一定的优势。

如果查找一个指定位置的数据,vector和arraylist使用的时间是相同的,都是0(1),这个时候使用vector和arraylist都可以。而如果移动一个指定位置的数据花费的时间为0(n-i)n为总长度,这个时候就应该考虑到使用linklist,因为它移动一个指定位置的数据所花费的时间为0(1),而查询一个指定位置的数据时花费的时间为0(i)。

ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,都允许直接序号索引元素,但是插入数据要设计到数组元素移动等内存操作,所以索引数据快插入数据慢,Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差,LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入数度较快!

最新评论

小黑屋|在路上 ( 蜀ICP备15035742号-1 

;

GMT+8, 2025-5-4 02:20

Copyright 2015-2025 djqfx

返回顶部