java.util.List接口是Java Collections Framework的一个重要组成部分,List接口的架构图如下:

本文将通过剖析List接口的三个实现类——ArrayListLinkedListVector的源码,带你走近List的世界。

ArrayList

ArrayList是List接口可调整数组大小的实现。实现所有可选列表操作,并允许放入包括空值在内的所有元素。每个ArrayList都有一个容量(capacity,区别于size),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。

底层实现

java.util.ArrayList类的继承关系如下:

public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable

其中需要注意的是RandomAccess接口,这是一个标记接口,没有定义任何具体的内容,该接口的意义是随机存取数据。在该接口的注释中有这样一段话:

/** for (int i=0, n=list.size(); i < n; i++) { list.get(i); } runs faster than this loop: for (Iterator i=list.iterator(); i.hasNext(); ) { i.next(); } **/

这说明在数据量很大的情况下,采用迭代器遍历实现了该接口的集合,速度比较慢。

实现了RandomAccess接口的集合有:ArrayList, AttributeList, CopyOnWriteArrayList, RoleList, RoleUnresolvedList, StackVector等。

ArrayList一些重要的字段如下:

    private static final int DEFAULT_CAPACITY = 10;private static final Object[] EMPTY_ELEMENTDATA = {};private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};transient Object[] elementData; // non-private to simplify nested class accessprivate int size;//底层数组中实际元素个数,区别于capacity

可以看到,默认第一次插入元素时创建数组的大小为10。当向容器中添加元素时,如果容量不足,容器会自动增加50%的容量。增加容量的函数grow()源码如下:

private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1);//右移一位代表增加50%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);}private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}

值得注意的是,由于集合框架用到了编译器提供的语法糖——泛型,而Java泛型的内在实现是通过类型擦除和类型强制转换来进行的,其实存储的数据类型都是Raw Type,因此集合框架的底层数组都是Object数组,可以容纳任何对象。

数组复制

ArrayList的实现中大量地调用了Arrays.copyof()System.arraycopy()方法。在此介绍一下这两个方法。

System.arraycopy()方法是一个native方法,调用了系统的C/C++代码,在openJDK中可以看到其源码。该方法最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java强烈推荐在复制大量数组元素时使用该方法,以取得更高的效率。

Arrays.copyOf()方法有很多重载版本,但实现思路都是一样的,其泛型版本源码如下:

public static <T> T[] copyOf(T[] original, int newLength) {  return (T[]) copyOf(original, newLength, original.getClass());
}

其调用了copyof()方法:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {  T[] copy = ((Object)newType == (Object)Object[].class)  ? (T[]) new Object[newLength]  : (T[]) Array.newInstance(newType.getComponentType(), newLength);  System.arraycopy(original, 0, copy, 0,  Math.min(original.length, newLength));  return copy;
}

该方法实际上是在其内部创建了一个类型为newType、长度为newLength的新数组,调用System.arraycopy()方法,将原来数组中的元素复制到新的数组中。

非线程安全

ArrayList的实现是不同步的,如果多个线程同时访问ArrayList实例,并且至少有一个线程修改list的结构,那么它就必须在外部进行同步。如果没有这些对象, 这个list应该用Collections.synchronizedList()方法进行包装。 最好在list的创建时就完成包装,防止意外地非同步地访问list:

List list = Collections.synchronizedList(new ArrayList(...));

除了未实现同步之外,ArrayList大致相当于Vector。

size(), isEmpty(), get(),set()方法均能在常数时间内完成,add()方法的时间开销跟插入位置有关(adding n elements requires O(n) time),addAll()方法的时间开销跟添加元素的个数成正比。其余方法大都是线性时间。

常用API

ArrayList常用的size(), isEmpty(), get(),set()方法实现都比较简单,读者可自行翻阅源码,它们均能在常数时间内完成,性能很高,这也是数组实现的优势所在。add()方法的时间开销跟插入位置有关(adding n elements requires O(n) time),addAll()方法的时间开销跟添加元素的个数成正比。其余方法大都是线性时间。

add()方法

public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;
}
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++;
}

前者是在ArrayList尾部插入一个元素,后者是在指定位置插入元素。值得注意的是,将元素的索引赋给elementData[size]时可能会出现数组越界,这里的关键就在于ensureCapacity(size+1)的调用,这个方法的作用是确保数组的容量,它的源码如下:

ensureCapacity()ensureExplicitCapacity()方法:

public void ensureCapacity(int minCapacity) {int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)// any size if not default element table? 0// larger than default for default empty table. It's already// supposed to be at default size.: DEFAULT_CAPACITY;if (minCapacity > minExpand) {ensureExplicitCapacity(minCapacity);}
}private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);
}

其中有一个重要的实例变量modCount,它是在AbstractList类中定义的,在使用迭代器遍历的时候,用modCount来检查列表中的元素是否发生结构性变化(列表元素数量发生改变)了,如果modCount值改变,则代表列表中元素发生了结构性变化。

前面说过,ArrayList是非线程安全的,modCount主要在多线程环境下进行安全检查,防止一个线程正在迭代遍历,另一个线程修改了这个列表的结构。如果在使用迭代器进行遍历ArrayList的时候modCount值改变,则会报ConcurrentModificationException异常。

可以看出,直接在数组后面插入一个元素add(e)效率也很高,但是如果要按下标来插入元素,则需要调用System.arraycopy()方法来移动部分受影响的元素,这会导致性能低下,这也是使用数组实现的ArrayList的劣势

同理,remove()方法也会改变modCount的值,效率与按下标插入元素相似,在此不加赘述。

addAll()方法

public boolean addAll(Collection<? extends E> c) {Object[] a = c.toArray();int numNew = a.length;ensureCapacityInternal(size + numNew);  // Increments modCountSystem.arraycopy(a, 0, elementData, size, numNew);size += numNew;return numNew != 0;
}
public boolean addAll(int index, Collection<? extends E> c) {rangeCheckForAdd(index);Object[] a = c.toArray();int numNew = a.length;ensureCapacityInternal(size + numNew);  // Increments modCountint numMoved = size - index;if (numMoved > 0)System.arraycopy(elementData, index, elementData, index + numNew,numMoved);System.arraycopy(a, 0, elementData, index, numNew);size += numNew;return numNew != 0;
}

addAll方法也分在末尾插入和在指定位置插入,先将入参中的集合c转换成数组,根据转换后数组的程度和ArrayList的size拓展容量,之后调用System.arraycopy()方法复制元素到相应位置,调整size。根据返回的内容分析,只要集合c的大小不为空,即转换后的数组长度不为0则返回true。

容易看出,addAll()方法的时间开销是跟添加元素的个数成正比的。

trimToSize()方法

下面来看一个简单但是很有用的方法trimToSize()

public void trimToSize() {modCount++;if (size < elementData.length) {elementData = (size == 0)? EMPTY_ELEMENTDATA: Arrays.copyOf(elementData, size);}
}

由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。trimToSize()将返回一个新的数组给elementData,元素内容保持不变,length和size相同,节省空间。

在实际应用中,考虑这样一种情形,当某个应用需要,一个ArrayList扩容到比如size=10000,之后经过一系列remove操作size=15,在后面的很长一段时间内这个ArrayList的size一直保持在<100以内,那么就造成了很大的空间浪费,这时候建议显式调用一下trimToSize()方法,以优化一下内存空间。     或者在一个ArrayList中的容量已经固定,但是由于之前每次扩容都扩充50%,所以有一定的空间浪费,可以调用trimToSize()消除这些空间上的浪费。

LinkedList

LinkedList与ArrayList一样也实现了List接口,LinkedList使用双向链表实现,允许存储元素重复,链表与ArrayList的数组实现相比,在进行插入和删除操作时效率更高,但查找操作效率更低,因此在实际使用中应根据自己的程序计算需求来从二者中取舍。

与ArrayList一样,LinkedList也是非线程安全的。

底层实现

java.util.LinkedList的继承关系如下:

public class LinkedList<E>extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList继承自抽象类AbstractSequenceList,其实AbstractSequenceList已经实现了List接口,这里标注出List只是更加清晰而已。AbstractSequenceList提供了List接口骨干性的实现以减少从而减少了实现受“连续访问”数据存储(如链表)支持的此接口所需的工作。对于随机访问数据(如数组),则应该优先使用抽象类AbstractList。

可以看到,LinkedList除了实现了List接口外,还实现了Deque接口,Deque即“Double Ended Queue”,是可以在两端插入和移动数据的线性数据结构,我们熟知的队列皆可以通过实现Deque接口来实现。因此在LinkedList的实现中,除了提供了列表相关的方法如add()remove()等,还提供了栈和队列的pop()peek()poll()offer()等相关方法。这些方法中有些彼此之间只是名称的区别,内部实现完全相同,以使得这些名字在特定的上下文中显得更加的合适。

LinkedList定义的字段如下:

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

Size代表的是链表中存储的元素个数,而first和last分别代表链表的头节点尾节点。 其中Node是LinkedList定义的静态内部类:

private static class Node<E> {E item;Node<E> nextNode<E> prev;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}
}

Node是链表的节点类,其中的三个属性item、next、prev分别代表了节点的存储属性值、前继节点和后继节点。

常用API

add(e)方法

public boolean add(E e) {linkLast(e);return true;
}void linkLast(E e) {final Node<E> l = last;final Node<E> newNode = new Node<>(l, e, null);last = newNode;if (l == null)first = newNode;elsel.next = newNode;size++;modCount++;
}

由上述代码可见,LinkedList在表尾添加元素,只要直接修改相关节点的前后继节点信息,而无需移动其他元素的位置,因此执行添加操作时效率很高。此外,LinkedList也是非线程安全的

remove(o)方法

public boolean remove(Object o) {if (o == null) {for (Node<E> x = first; x != null; x = x.next) {if (x.item == null) {unlink(x);return true;}}} else {for (Node<E> x = first; x != null; x = x.next) {if (o.equals(x.item)) {unlink(x);return true;}}}return false;
}E unlink(Node<E> x) {// assert x != null;final E element = x.item;final Node<E> next = x.next;final Node<E> prev = x.prev;if (prev == null) {first = next;} else {prev.next = next;x.prev = null;}if (next == null) {last = prev;} else {next.prev = prev;x.next = null;}x.item = null;size--;modCount++;return element;
}

add方法一样,remove方法的底层实现也无需移动列表里其他元素的位置,而只需要修改被删除节点及其前后节点的prevnext属性即可。

get(index)方法

该方法可以返回指定位置的元素,下面来看一看代码

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;}
}

可以看到,LinkedList要想找到index对应位置的元素,必须要遍历整个列表,在源码实现中已经使用了二分查找size >> 1即是除以2)的方法来进行优化,但查找元素的开销依然很大,并且与查找的位置有关。相比较ArrayList的常数级时间的消耗而言,差距很大。

clear()方法

public void clear() {// Clearing all of the links between nodes is "unnecessary", but:// - helps a generational GC if the discarded nodes inhabit// more than one generation// - is sure to free memory even if there is a reachable Iteratorfor (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;modCount++;
}

该方法并不复杂,作用只是遍历列表,清空表中的元素和节点连接而已。之所以单独拿出来讲,是基于GC方面的考虑,源码注释中讲道,该方法中将所有节点之间的“连接”都断开并不是必要的,但是由于链表中的不同节点可能位于分代GC的不同年代中,如果它们互相引用会给GC带来一些额外的麻烦,因此执行此方法断开节点间的相互引用,可以帮助分代GC在这种情况下提高性能

Vector

作为伴随JDK早期诞生的容器,Vector现在基本已经被弃用,不过依然有一些老版本的代码使用到它,因此也有必要做一些了解。VectorArrayList的实现基本相同,它们底层都是基于Object数组实现的,两者最大的区别在于ArrayList是非线程安全的,而Vector是线程安全的。由于Vector与ArrayList的实现非常相近,前面对于ArrayList已经进行过详细介绍了,这里很多东西就不在赘述,重点介绍Vector与ArrayList的不同之处。

容量扩展

Vector与ArrayList还有一处细节上的不同,那就是Vector进行添加操作时,如果列表容量不够需要扩容,每次增加的大小是原来的100%,而前面已经讲过,ArrayList一次只增加原有容量的50%。具体代码如下:

private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;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);
}

线程安全

Vector类内部的大部分方法与ArrayList均相同,区别仅在于加上了synchronized关键字,比如:

public synchronized void trimToSize() {modCount++;int oldCapacity = elementData.length;if (elementCount < oldCapacity) {elementData = Arrays.copyOf(elementData, elementCount);}
}

这也保证了同一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问Vector比访问ArrayList要慢。

前面说过,由于性能和一些设计问题,Vector现在基本已被弃用,当涉及到线程安全时,可以如前文介绍ArrayList时所说的,对ArrayList进行简单包装,即可实现同步。

Stack类

Vector还有一个子类叫Stack,其实现了栈的基本操作。这也是在JDK早期出现的容器,很多设计不够规范,现在已经过时,使用Queue接口的相关实现可以完全取代它。

总结

  • ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要讲已经有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
  • LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
  • Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢,现在基本已弃用。

Java集合框架源码解读(1)——ArrayList、LinkedList和Vector相关推荐

  1. Java集合框架源码解析之ArrayList

    ArrayList 可能是很多人使用得最为频繁的容器类了,ArrayList 实现了 List 接口,是一个有序容器,即存放元素的顺序与添加顺序相同,允许添加相同元素,包括 null ,底层通过数组来 ...

  2. 【java集合框架源码剖析系列】java源码剖析之ArrayList

    注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 本博客将从源码角度带领大家学习关于ArrayList的知识. 一ArrayList类的定义: public class Arr ...

  3. Java集合框架源码详解系列(一)

     写在前面:大家好!我是晴空๓.如果博客中有不足或者的错误的地方欢迎在评论区或者私信我指正,感谢大家的不吝赐教.我的唯一博客更新地址是:https://ac-fun.blog.csdn.net/.非常 ...

  4. 【java集合框架源码剖析系列】java源码剖析之java集合中的折半插入排序算法

    注:关于排序算法,博主写过[数据结构排序算法系列]数据结构八大排序算法,基本上把所有的排序算法都详细的讲解过,而之所以单独将java集合中的排序算法拿出来讲解,是因为在阿里巴巴内推面试的时候面试官问过 ...

  5. Java集合框架源码剖析:LinkedHashSet 和 LinkedHashMap

    Java LinkedHashMap和HashMap有什么区别和联系?为什么LinkedHashMap会有着更快的迭代速度?LinkedHashSet跟LinkedHashMap有着怎样的内在联系?本 ...

  6. Java集合类框架源码分析 之 LinkedList源码解析 【4】

    上一篇介绍了ArrayList的源码分析[点击看文章],既然ArrayList都已经做了介绍,那么作为他同胞兄弟的LinkedList,当然必须也配拥有姓名! Talk is cheap,show m ...

  7. Slim 框架源码解读

    0x00 前言 Slim 是由<PHP The Right Way>作者开发的一款 PHP 微框架,代码量不算多(比起其它重型框架来说),号称可以一下午就阅读完(我觉得前提是熟悉 Slim ...

  8. Java Review - PriorityQueue源码解读

    文章目录 Pre PriorityQueue 概述 PriorityQueue 继承关系 PriorityQueue通过用数组表示的小顶堆实现 时间复杂度 构造函数 方法 add()和offer() ...

  9. java 并发框架源码_Java并发编程高阶技术-高性能并发框架源码解析与实战

    Java并发编程高阶技术-高性能并发框架源码解析与实战 1 _0 Z' @+ l: s3 f6 r% t|____资料3 Z9 P- I2 x8 T6 ^ |____coding-275-master ...

  10. Java源码 JavaWeb开发框架 代码 SSH SSM OA ERP CRM Java项目[Java通用框架源码及开发视频教程]

    Java源码 JavaWeb开发框架 代码 SSH SSM OA ERP CRM Java项目 功能简介: A.代码生成器(开发利器) 生成Java各层次的类和JSP等文件,提高开发效率 B.阿里巴巴 ...

最新文章

  1. react中ref的使用
  2. 影像组学视频学习笔记(42)-影像组学特征提取问题解决过程复现、Li‘s have a solution and plan.
  3. JavaWeb-网站在线用户信息、网站访问量以及管理踢出用户实例
  4. OpenCV在相机或图像中检测QR码的实例(附完整代码)
  5. Project Tango 的一些应用
  6. 用 Visual Studio 和 ASP.NET Core MVC 创建首个 Web API
  7. jquery.validate+jquery.form提交的三种方式
  8. Graphviz样例之有向图
  9. llvm Pass简单实例
  10. C语言绘制单项正弦电压波形图,正弦交流电压波形图为例讲解“五点法”画波形图的方法...
  11. phpstorm配置phpunit单元测试及PHPunit断言函数
  12. 台式计算机怎么加声音,台式电脑音量加到最大还是没有声音怎么办,教你一招立马解决...
  13. 4 anbox 树莓派_Anbox让你在Linux上“原生运行”Android应用
  14. pprint和print区别
  15. typedef的用法。
  16. 私有和公开IP地址的区别是什么?
  17. python实现Dice系数
  18. HPP结合PHP解析URL特性的SQL注入题目
  19. 10进制、16进制(十进制、十六进制)与ASCII码流互转
  20. laravel8实现图片验证码

热门文章

  1. 58. Attribute item() 方法
  2. 当使用 position 属性时,请始终设置 !DOCTYPE 声明:当使用 float 属性时,请始终设置 !DOCTYPE 声明:
  3. 测试Leader应该做哪些事
  4. C++ 编译发现 error C2146: syntax error : missing ';' before identifier 'm_ctrlserver'
  5. JavaScript数组的某些操作(一)
  6. php 写入.csv文件注意点
  7. zend studio 免注册无限试用
  8. Java编程:按照指定的字段顺序,将 Bean 转换为 Json
  9. VC预定义常量WIN32、_WIN32、_WIN64分析(转载)
  10. VS2015 LINK : warning LNK4068: 未指定 /MACHINE;默认设置为 X86