前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >jdk源码分析之List--常用实现类分析与对比

jdk源码分析之List--常用实现类分析与对比

作者头像
叔牙
发布2020-11-19 14:53:37
2450
发布2020-11-19 14:53:37
举报
文章被收录于专栏:一个执拗的后端搬砖工

java编码中,集合类算是我们用的最多的,比如HashMap,TreeMap,ArrayList,LinkedList等等,使我们最常用的(并发包中的实现暂不做分析),按照顶级接口分析,有两种,就是Map和Collection(Collection接口继承于Iterable),Collection又分为List和Set分支,也就是列表和集合。今天我们队List分支以及其常用实现子类进行源码层面分析和对比。

List分支中我们最常用的实现有ArrayList,LinkedList和Vector(已经近乎废弃),分析一下其继承关系:

可以看出AbstractList实现了List接口,ArrayList,LinkedList和Vector继承了AbstractList并且实现了List接口;AbstractList提供了对List接口功能的一些通用实现(也就是抽象的概念,不用每个具体实现类再去实现),而三个实现类既然继承了AbstractList,为什么又实现List接口呢?其实是是一种既可以使用AbstractList中的通用实现,又可以对List接口提供原生的实现。

接下来我们从List到具体实现类,详细分析一下其功能和实现:

从List接口中文档描述我们可以得出每个方法的具体作用:

  1. int size(),返回列表中元素的数目.
  2. boolean isEmpty(),返回列表中是否包含元素,如果有返回false,否则true
  3. boolean contains(Object o),返回列表中是否包含元素o,包含返回true,否则false
  4. Iterator<E> iterator(),返回当前列表的迭代器,用于遍历列表元素
  5. Object[] toArray(),将当前列表转换为数组并返回
  6. <T> T[] toArray(T[] a),将列表转换为给定类型的数组并返回
  7. boolean add(E e),将元素增加到列表结尾
  8. boolean remove(Object o),从列表中移除第一个匹配的元素,如果找到移除后返回true,如果没有该元素,返回false
  9. boolean containsAll(Collection<?> c),返回列表是否包含入参中的所有元素
  10. boolean addAll(Collection<? extends E> c),将入参中的所有元素添加到列表结尾
  11. boolean addAll(int index, Collection<? extends E> c),将入参中的元素添加到列表index位置的元素后
  12. boolean removeAll(Collection<?> c),将入参中的所有元素从列表中移除
  13. boolean retainAll(Collection<?> c),将列表中包含入参的元素留下,其他全部移除
  14. void clear(),清空列表
  15. boolean equals(Object o),返回当前列表和入参是否相等(入参必须满足是一个List,长度相同,每个元素都相等)
  16. int hashCode(),返回列表的hashCode
  17. E get(int index),获取列表中索引位置index的元素
  18. E set(int index, E element),将列表中索引位置index的元素替换为element
  19. void add(int index, E element),将element插入到列表中的index位置(该位置后的元素后移)
  20. E remove(int index),移除列表中index位置的元素
  21. int indexOf(Object o),返回入参o在列表中第一个匹配的位置(如果不存在,返回-1)
  22. int indexOf(Object o),返回入参o在列表中最后一个匹配的位置(如果没找到,返回-1)
  23. ListIterator<E> listIterator(),返回一个迭代器
  24. ListIterator<E> listIterator(int index),返回一个列表中从index位置开始的迭代器

然后我们看一下抽象类AbstractList的实现和要做的事情。AbstractList实现了上边我们描述的List接口,并且继承了AbstractCollection类,AbstractCollection提供了Collection层面更加通用的实现,看一下AbstractList的实现:

我们接着对AbstractList做一下分析:

  1. public boolean add(E e) { add(size(), e); return true; } size()是列表的长度,add(E e)也就是在列表结尾增加元素e
  2. abstract public E get(int index) ,get是一个抽象方法,提供了一个模板留给子类去实现
  3. public E set(int index, E element) { throw new UnsupportedOperationException(); } 明确声明不支持set操作,也就是说子类必须要自己实现该方法后才能够使用,否则抛异常
  4. public void add(int index, E element) { throw new UnsupportedOperationException(); } 也是add(e)所依赖的方法,抽象类中不提供具体实现,子类根据需要自己实现
  5. public E remove(int index) { throw new UnsupportedOperationException(); } 移除指定位置的元素,抽象类不提供实现,子类自己实现
  6. public int indexOf(Object o) { ListIterator<E> it = listIterator(); if (o==null) { while (it.hasNext()) if (it.next()==null) return it.previousIndex(); } else { while (it.hasNext()) if (o.equals(it.next())) return it.previousIndex(); } return -1; } 查找入参在列表中的位置。首先返回列表的迭代器,如果入参为null,直接遍历列表寻找等于null的元素,如果找到返回位置;如果入参不为null,遍历列表寻找相等的元素,如果找到返回位置;最后如果没找到返回-1
  7. public int lastIndexOf(Object o) { ListIterator<E> it = listIterator(size()); if (o==null) { while (it.hasPrevious()) if (it.previous()==null) return it.nextIndex(); } else { while (it.hasPrevious()) if (o.equals(it.previous())) return it.nextIndex(); } return -1; } 返回入参在列表中的最后匹配的位置。首先返回从列表最后一个元素开始的列表迭代器,如果入参为null,从尾部向前遍历,找到为null的元素并返回位置;如果入参非null,也是从尾部向首部遍历找到相等的元素并返回位置;如果没有找到返回-1
  8. public void clear() { removeRange(0, size()); } protected void removeRange(int fromIndex, int toIndex) { ListIterator<E> it = listIterator(fromIndex); for (int i=0, n=toIndex-fromIndex; i<n; i++) { it.next(); it.remove(); } } 清空列表。返回从索引0开始的列表迭代器,从开始到尾部遍历并且逐个移除元素
  9. public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); boolean modified = false; for (E e : c) { add(index++, e); modified = true; } return modified; } private void rangeCheckForAdd(int index) { if (index < 0 || index > size()) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } 将入参集合的元素插入到列表元素index的位置。首先检查index位置是否合法,如果位置<0或者大于长度,下标越界;然后遍历入参列表,循环在index向后逐个插入元素
  10. public Iterator<E> iterator() { return new Itr(); }

返回列表的迭代器。Itr是AbstractList的一个私有内部类,提供了遍历、移除和检查是否有修改的方法

  1. public ListIterator<E> listIterator(final int index) { rangeCheckForAdd(index); return new ListItr(index); } 返回从index位置开始的列表迭代器。首先检查index位置是否合法,然后返回一个列表迭代器ListItr,其实现了ListIterator接口并继承了Itr类,其实也就是提供了列表中任何一个位置元素向前和向后遍历的方式
  2. public List<E> subList(int fromIndex, int toIndex) { return (this instanceof RandomAccess ? new RandomAccessSubList<>(this, fromIndex, toIndex) : new SubList<>(this, fromIndex, toIndex)); } 截取列表中从fromIndex开始到toIndex结束的子列表。会根据当前列表是不是RandomAccess接口实例,返回不同的子列表实现,其实可以简单理解为区分ArrayList(实现RandomAccess)和LinkedList(未实现RandomAccess)两种不同的子类实现
  3. public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof List)) return false; ListIterator<E> e1 = listIterator(); ListIterator e2 = ((List) o).listIterator(); while (e1.hasNext() && e2.hasNext()) { E o1 = e1.next(); Object o2 = e2.next(); if (!(o1==null ? o2==null : o1.equals(o2))) return false; } return !(e1.hasNext() || e2.hasNext()); } 检查当前入参是否和当前列表相等。如果满足o==this也就是同一个列表,返回true;如果入参不是List接口实例(类型不匹配),返回false;然后获取两个列表的迭代器,并且遍历元素,如果找到相应位置上不相等的元素,直接返回false。while循环的次数取决于两个列表中数量较少的一个,循环停止后肯定有一个列表到达了结尾,如果这时候还有任何一个列表有元素直接返回false,否则返回true
  4. public int hashCode() { int hashCode = 1; for (E e : this) hashCode = 31*hashCode + (e==null ? 0 : e.hashCode()); return hashCode; } 通过遍历元素,根据每一个元素的hashCode累加上一次计算的值乘以31最终得到hashCode(复杂计算为了尽可能减少hashCode重复)

分析完了List接口和AbstractList抽象类实现,jdk源码中我们最常用到的List实例主要有ArrayList、LinkedList和Vector,我们逐个做分析。

首先分析一下ArrayList的实现:

我们抽一些比较常用的方法做一下分析:

  1. public ArrayList() { super(); this.elementData = EMPTY_ELEMENTDATA; } private static final Object[] EMPTY_ELEMENTDATA = {}; 默认的构造器,我们在编码中经常用到,其实是本质上是创建一个空的Object数组
  2. public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } 带有初始容量的构造器。如果入参小于0报参数非法异常;否则新建一个长度为入参的Object数组并赋值给elementData
  3. public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } 入参是一个集合的构造器。先将入参c转变成Object数组赋值给elementData,size变为数组长度;如果赋值后的elementData不是Object数组,也就是说入参不是Object列表,这时候通过调用Arrays.copyOf方法转变为Object数组
  4. public E get(int index) { rangeCheck(index); return elementData(index); } private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } 返回指定位置的元素。如果入参大于数组最大索引,报数组越界异常;否则返回指定位置的数组元素
  5. public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; } 将指定位置元素替换为入参元素。先检查是否数组越界,将指定位置元素替换成新元素并返回旧元素
  6. public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { if (elementData == EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(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); } 该方法是ArrayList中最重要的方法,也是最复杂的方法,功能就是新增元素在内部数组的尾部。中间用到了一些扩容和数组复制的方法也都贴了出了,接下来对其逐步详细分解。首先要保证当前数组容量至少是当前列表元素个数加1,如果不满足要进行扩容,然后在数组中原来列表尾部索引加1的位置填充入参e;扩容方法ensureCapacityInternal中如果当前数组是空(说明是使用默认构造器新建列表,第一次新增元素),最小容量为默认初始容量private static final int DEFAULT_CAPACITY = 10和入参中较大的一个,然后调用ensureExplicitCapacity 保证数组长度至少为minCapacity;ensureExplicitCapacity 方法中如果最小空间需求量大于当前数组长度,调用grow 方法扩容数组长度至少为minCapacity;grow 方法中,先存储数组的旧长度oldCapacity,暂定新长度newCapacity为旧长度的3/2倍,如果新长度没有最小长度需求minCapacity大,将minCapacity赋值给新长度newCapacity,如果此时的新长度比默认最大数组长度MAX_ARRAY_SIZE大,将Integer最大值赋值给新长度newCapacity,然后将旧数组中的元素复制到新长度的数组中(Arrays.copyOf底层调用System.arraycopy方法)
  7. 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; } 该方法是移除列表中index位置的元素。首先检查是否下标越界;然后modCount加1;接着用oldValue存储该位置的旧元素,numberMoved记录的是数组中从index位置以后所有需要移动的元素个数;如果需要移动元素个数大于0(index不是数组尾部元素),将数组elementData 中的index+1位置以后的numMoved个元素拷贝到elementData 数组中index位置以后numMoved个元素位置,也就是覆盖,效果就是index后的元素向前移动一位,完成删除elementData(index)的效果
  8. public void clear() { modCount++; // clear to let GC do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; } 清空列表中的元素。其实就是遍历elementData数组,将其每一个索引位置指向null,然后把size变成0。注意:for循环上边有段注释// clear to let GC do its work,什么意思呢,其实我们循环中把数组中的元素都指向null,那么原来开辟的数组元素指向的内存空间已经失去引用,GC回收的时候就可以回收这些空间了

以上我们详细分析了ArrayList的代码以及核心的一些方法。总结出一下几点:

  1. ArrayList底层由数组实现
  2. get查询的时候,是随机访问,也就是直接通过数组指针到内存中获取元素值,不会遍历列表
  3. 新增和移除成本比较大。如果列表已经很大,那么在0号位置新增或删除元素会移动size-1个元素,在jvm层面就是新开辟内存空间存放元素,GC回收失去索引的旧空间

接着我们分析List接口另一个实现Vector:

从代码接够以及源码我们可以看到,Vector实现和ArrayList基本一样,只不过对大部分方法强制加上synchronized关键字,也就是线程阻塞访问,和HashTable的做法一样,现在已经基本废弃,这里不再做过多的分析。

然后我们最后一个要分析的List接口实现类,也就是经常被用来和ArrayList做各种比较的LinkedList,我们先看一下代码结构:

在分析源码之前,我们从方法结构中看到了LinkedList有很多和ArrayList不一样的方法,比如说peek(),poll(),offer(),pop()都是队列中拥有的方法,我们猜测LinkedList应该实现了队列的相关接口。看一下LinkedList的声明:

public class LinkedList<E>

extends AbstractSequentialList<E>

implements List<E>, Deque<E>, Cloneable, java.io.Serializable

很明显,LinkedList继承了抽象类AbstractSequentialList并且实现了ListDeque接口,AbstractSequentialList继承了AbstractList并提供了一些通用实现,实现了Deque(Deque继承了Queue接口),也就是说LinkedList拥有从头部和尾部遍历的能力,我们分析一下LinkedList一下常用的方法:

  1. public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; } 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; } } 获取列表的第一个元素。该方法是Deque中定义,首先获取第一个Node节点,如果为空(暂时没有元素)抛出异常,否则返回第一个元素的值;接着看到Node是LinkedList中的一个私有静态内部类,存储了当前节点的值以及前后节点的指针,可以得知LinkedList的存储结构是链表,大致如下:
  1. public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item; } 返回列表中最后一个元素。如果没有元素抛出异常,否则返回最后一个元素的内容
  2. 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; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; } 移除列表中第一个元素。如果列表为空,报异常;否则调用unlinkFirst方法解除指针指向;unlinkFirst方法中先记录首元素内容element,然后记录下一个元素next,将首节点内容指向null(GC回收),后指针指向改为null,然后把第二个节点next置为first,如果next为null,说明原来列表中只有一个Node节点,把last也置为null,否则把next的前节点置为null,然后把size减1,最后返回旧节点内容element。其实流程大概如下:

这样就完成首节点移除操作

  1. public E removeLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return unlinkLast(l); } 移除最后一个节点。类似3中的操作,不再做详细分析
  2. public void addFirst(E e) { linkFirst(e); } private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; } 将给定元素插入到列表第一个位置。linkFirst方法中f暂存首节点first,然后根据入参e新建一个prev指针null,next指针为f的节点,然后把新节点赋值给first,如果f为null(之前没有元素),last指针也指向新节点;否则原来first节点的元素prev指针指向新节点,然后增加size大小。 以上都是LinkedList特有的方法,接下来我们分析一下List接中有的方法
  3. public E get(int index) { checkElementIndex(index); return node(index).item; } private void checkElementIndex(int index) { if (!isElementIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private boolean isElementIndex(int index) { return index >= 0 && index < size; } 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; } } get方法是List中定义的获取指定位置元素的方法。首先检查索引是否合法(>=0&&<size);然后调用node方法查找元素,由于是链表结构,无法直接通过index定位元素,如果index < size>>1(index在size前半部分),从开始节点first节点遍历到index位置返回内容;否则(index在size后半部分)从结尾节点last向前遍历到index位置返回内容。 此处可以看出LinkedList对于查找性能不是太好,不像ArrayList直接通过index定位到内存中的元素,由此可知在查询方面(随机访问)性能不如ArrayList
  4. public boolean add(E e) { linkLast(e); return true; } 同样对List接口中add方法的实现,显得很简单,为什么呢?因为从linkLast方法中我们直接找到last节点然后修改next指针指向我们新元素,把新元素的prev节点指向last节点,最后把last指针指向新节点就好了,中间不牵扯类似ArrayList中的扩容和数组复制问题。此处可以看出LinkedList对于新增节点的性能要优于ArrayList

经过上述一些列的分析,我们队List的三种常见实现类有了更深刻的认识和了解。接下来我们就日常开发中经常纠结的ArrayList和LinkedList查询和插入到底哪个性能比较好展开进一步的实验和验证,运行一下例子:

为了将效果变得明显,我们向两个list中都增加了1000000个元素,然后测试get性能。细心的人发现为什么两次测试效果不一样呢,看一下get方法的索引,第一次我们测试通过索引为999999,第二次索引位置是500000,也就是说第一个是查询列表中最后一个元素,第二次是查询列表中中间位置的元素,为什么性能差异那么大呢?ArrayList的get方法就不用看了,看一下LinkedList中的get方法,有这么一段:

就是如果遍历的方式依赖于index大小,如果index不满足index<(size>>1),遍历查询的时候会走else分支,结合我们例子,size为1000000,index为999999(最后一个元素),那么会从最后一个元素遍历,凑巧一下子找到了元素,这种情况下性能是和ArrayList中的get性能一样的;然后index为500000得时候,ArrayList和之前get性能基本一样,而LinkedList会走else分支从列表中最后一个节点向前遍历,但是index是在此段的最后一个元素,也就是说LinkedList将近遍历了500000次才找到元素,这也就成为了性能差别的原因。

然后我们修改上边例子中的代码测试插入效果:

可以看到耗时是一样的,为什么呢?因为add(e)方法对于LinkedList只需要新建Node节点和改变指针指向,而ArrayList会将size加1然后将数组最后一个元素指针指向新元素如果没有出现扩容和数组赋值,这种操作对于两种List基本没有性能差别。

我们把上述的代码改一下,然后再运行:

为了让效果更明显,我们把初始容量增大成10000000,可以看到LinkedList.add(0,111111)基本没有耗时,而ArrayList.add(0,111111)耗时3,性能差别很明显,为什么呢?对于前者,仍然只需要新建一个Node和改变前后指针指向,而后者会发生数组复制,将原数组所有元素拷贝到自己从第二个位开始,长度为size的对应位置,然后将入参赋值给0号位置,出了数组复制,还可能出现扩容,所以性能一定会比 LinkedList差。

此篇我们队Jdk集合中List以及常用实现做了详细的分析和性能对比,希望帮助大家在实际开发中根据具体的业务场景选用不同的List实现,从而带来性能提升。如果有觉得分析不到位或者理解有偏差的,可以直接留言或者私聊我。

原创不易,请多多支持!

附带公众号:

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-12-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PersistentCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档