ArrayList 源码分析(转载)
不知道各位朋友,还记得开工前制定的学习目标么? 有没有一直为了那个目标废寝忘食呢?继 搞懂 Java 内部类 后开始探索总结 Java 集合框架源码的知识,希望能给自己夯实基础,也希望能为自己实现目标更近一步。
ArrayList 源码分析思路
ArrayList 是我们 App 开发中常用的 Java 集合类,从学习 Java 开始我们基本上就对它天天相见了,但是通过探索ArrayList 源码,我们将会把它从普通朋友变成知根知底的老朋友,本文将从以下几部分开始分析 ArrayList:
- ArrayList 概述
- ArrayList 的构造函数,也就是我们创建一个 ArrayList 的方法
- ArrayList 的添加元素的方法, 以及 ArrayList 的扩容机制
- ArrayList 的删除元素的常用方法
- ArrayList 的 改查常用方法
- ArrayList 的 toArray 方法
- ArrayList 的遍历方法,以及常见的错误操作即产生错误操作的原因
ArrayList 概述
ArrayList的基本特点
- ArrayList 底层是一个动态扩容的数组结构
- 允许存放(不止一个) null 元素
- 允许存放重复数据,存储顺序按照元素的添加顺序
- ArrayList 并不是一个线程安全的集合。如果集合的增删操作需要保证线程的安全性,可以考虑使用 CopyOnWriteArrayList 『使用的是 ReentrantLock 保证同步,但是有弱一致性,因为写数据时是先拷贝再复制回去。』或者使用 collections.synchronizedList(List l) 函数返回一个线程安全的ArrayList类。当然了,没有绝对的线程安全,这里的线程安全只能保证单个方法的线程安全,如果复合操作,比如一个线程不断读取一个数,另外一个线程不断删除一个数,两个方法的复合操作,也会导致线程不安全,这块我在 jvm 的最后一节也有讲。
ArrayList 的继承关系
1 | public class ArrayList<E> extends AbstractList<E> |
从 ArrayList 的继承关系来看, ArrayList 继承自 AbstractList ,实现了List\
- 其中 AbstractList和 List\
是规定了 ArrayList 作为一个集合框架必须具备的一些属性和方法,ArrayList 本身覆写了基类和接口的大部分方法,这就包含我们要分析的增删改查操作。 - ArrayList 实现 RandomAccess 接口标识着其支持随机快速访问,查看源码可以知道 RandomAccess 其实只是一个标识,标识某个类拥有随机快速访问的能力,针对 ArrayList 而言通过 get(index) 去访问元素可以达到 O(1) 的时间复杂度。有些集合类不拥有这种随机快速访问的能力,比如 LinkedList 就没有实现这个接口。
- ArrayList 实现 Cloneable 接口标识着他可以被克隆/复制,其内部实现了 clone 方法供使用者调用来对 ArrayList 进行克隆,但其实现只通过 Arrays.copyOf 完成了对 ArrayList 进行「浅拷贝」,也就是你改变 ArrayList clone后的集合中的元素,源集合中的元素也会改变,对于深浅拷贝我已经单独整理一篇文章来讲述这里不再过多的说。
- 对于 java.io.Serializable 标识着集合可被被序列化。
我们发现了一些有趣的事情,除了 List\
ArrayList 的构造方法
在说构造方法之前我们要先看下与构造参数有关的几个全局变量:
1 | /** |
其中最需要关注的就是 transient 修饰了 elementData,我们知道 transient 修饰的变量是防止变量被序列化和反序列化,那为何 ArrayList 继承了 Serializable,却将其内部元素变为 transient 呢?因为 elementData 是一个缓存数组,会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上面的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
还有就是,那 ArrayList 是如何实现序列化的呢?是因为其写了 writeObject() 和 readObject() ,虽然这两个方法都是使用的私有方法,但如何调用的呢?就是通过反射机制得以调用的。反射里面是有 setAccessible() 可以忽略方法前的 private。
具体见:
作者:汪和呆喵
链接:https://www.jianshu.com/p/14876ef38721
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
对于上述几个成员变量,我们只是在注释中简单的说明,对于他们具体有什么作用,在下边分析构造方法和扩容机制的时候将会更详细的讲解。
ArrayList 一共三种构造方式,我们先从无参的构造方法来开始:
无参构造方法
1 | /** |
这是我们经常使用的一个构造方法,其内部实现只是将 elementData 指向了我们刚才讲得 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这个空数组,这个空数组的容量是 0, 但是源码注释却说这是构造一个初始容量为10的空列表。这是为什么?其实在集合调用 add 方法添加元素的时候将会调用 ensureCapacityInternal 方法,在这个方法内部判断了:
1 | if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { |
可见,如果采用无参数构造方法的时候第一次添加元素肯定走进 if 判断中 minCapacity 将被赋值为 10,所以构造一个初始容量为10的空列表 也就是这个意思。
指定初始容量的构造方法
1 | /** |
如果我们预先知道一个集合元素的容纳的个数的时候推荐使用这个构造方法,比如我们有个FragmentPagerAdapter 一共需要装 15 个 Fragment ,那么我们就可以在构造集合的时候生成一个初始容量为 15 的一个集合。有人会认为 ArrayList 自身具有动态扩容的机制,无需这么麻烦,下面我们讲解扩容机制的时候我们就会发现,每次扩容是需要有一定的内存开销的,而这个开销在预先知道容量的时候是可以避免的。
源代码中指定初始容量的构造方法实现,判断了如果 我们指定容量大于 0 ,将会直接 new 一个数组,赋值给 elementData 引用作为集合真正的存储数组,而指定容量等于 0 的时候使用成员变量 EMPTY_ELEMENTDATA 作为暂时的存储数组,这是 EMPTY_ELEMENTDATA 这个空数组的一个用处(不必太过于纠EMPTY_ELEMENTDATA 的作用,其实它的在源码中出现的频率并不高)。
使用另个一个集合 Collection 的构造方法
1 | /** |
看完这个代码我最疑惑的地方是 Collection.toArray() 和 Arrays.copyOf() 这两个方法的使用,看来想明白这个构造参数具体做了什么必须理解这两个方法了。
Object[] Collection.toArray() 方法
我们都知道 Collection 是集合框架的超类,其实 Collection.toArray 是交给具体的集合子类去实现的,这就说明不同的集合可能有不同的实现。他用来将一个集合转化为一个 Object[] 数组,事实上的真的是这样的么?参见 jdk 的 bug 列表(6260652)又是什么意思呢 ?我们来看下下边的这个例子:
1 | List<String> subClasses = Arrays.asList("abc","def"); |
咦?为啥这里并不是一个 Object 数组呢?其实我们注意到,list.getClass 得到的并不是我们使用的 ArrayList 而是 Arrays 的内部类 Arrays$ArrayList。
1 | ArrayList(E[] array) { |
而我们调用的 toArray 方法就是这个内部对于 Collection.toArray 的实现, a.clone() ,这里 clone 并不会改变一个数组的类型,所以当原始数组中放的 String 类型的时候就会出现上边的这种情况了。
其实我们可以认为这是 jdk 的一个 bug,早在 05年的时候被人提出来了,但是一直没修复,但是在新的 「jdk 1.9」 种这个 bug 被修复了。
有兴趣的可以追踪 bug 6260652 看下。
Arrays.copyOf 方法
这个方法是在集合源码中常见的一个方法,他有很多重载方式,我们来看下最根本的方法:
1 | public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { |
上边的注释也看出来了,Arrays.copyOf 方法复制数组的时候先判断了指定的数组类型是否为 Object[] 类型,否则使用反射去构造一个指定类型的数组。最后使用 System.arraycopy 这个 native 方法,去实现最终的数组赋值,newLength 如果比 original.length 大的时候会将多余的空间赋值为 null 由下边的例子可见:
1 | String[] arrString = {"abc","def"}; |
当然 ArrayList(Collection<? extends E> c) 复制的时候传递的是 c.size() 所以不会出现 null。
ex: 对于 System.arraycopy 该方法,本文不再展开讨论,有一篇对于其分析很好的文章大家可以去参考System:System.arraycopy方法详解
ok,绕了这么大的圈子终于明白了,ArrayList(Collection<? extends E> c)干了啥了,其实就是将一个集合中的元素塞到 ArrayList 底层的数组中。至此我们也将 ArrayList 的构造研究完了。
ArrayList的添加元素 & 扩容机制
敲黑板了!这块是面试的常客了,所以必须仔细研究下了。我们先看下如何给一个 ArrayList 添加一个元素:
在集合末尾添加一个元素的方法
1 | //成员变量 size 标识集合当前元素个数初始为 0 |
调用 add 方法的时候总会调用 ensureCapacityInternal 来判断是否需要进行数组扩容, ensureCapacityInternal 参数为当前集合长度 size + 1,这很好理解,是否需要扩充长度,需要看当前底层数组是否够放 size + 1个元素的。
扩容机制
1 | //扩容检查 |
上边的源码主要做了扩容前的判断操作,注意参数为当前集合元素个数+1,第一次添加元素的时候 size + 1 = 1 ,而 elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA, 长度为 0 ,1 - 0 > 0, 所以需要进行 grow 操作也就是扩容。
1 | /** |
由此看来 ArrayList 的扩容机制的知识点一共又两个
每次扩容的大小为原来大小的 1.5 倍 (当然这里没有包含 1.5倍后大于 MAX_ARRAY_SIZE 的情况)
扩容的过程其实是一个将原来元素拷贝到一个扩容后数组大小的长度新数组中。所以 ArrayList 的扩容其实是相对来说比较消耗性能的。
在指定角标位置添加元素的方法
1 | /** |
我们知道一个数组是不能在角标位置直接插入元素的,ArrayList 通过数组拷贝的方法将指定角标位置以及其后续元素整体向后移动一个位置,空出 index 角标的位置,来赋值新的元素。
将一个数组 src 起始 srcPos 角标之后 length 长度间的元素,赋值到 dest 数组中 destPos 到 destPos + length -1长度角标位置上。只是在 add 方法中 src 和 dest 为同一个数组而已。
1 | public static native void arraycopy(Object src, int srcPos, |
批量添加元素
由于批量添加和添加一个元素逻辑大概相同则这里不详细说了,代码注释可以了解整个添加流程。
在数组末尾添加
1 | public boolean addAll(Collection<? extends E> c) { |
在数组指定角标位置添加
1 | public boolean addAll(int index, Collection<? extends E> c) { |
两个方法不同的地方在于如果移动角标即之后的元素,addAll(int index, Collection<? extends E> c)里做了判断,如果要 numMoved > 0 代表插入的位置在集合中间位置,和在 numMoved == 0 最后位置 则表示要在数组末尾添加 如果 numMoved < 0 ,rangeCheckForAdd 就抛出了角标越界异常了。
与单一添加的 add 方法不同的是批量添加有返回值,如果 numNew == 0 表示没有要添加的元素则需要返回 false。
ArrayList 删除元素
根据角标移除元素
1 | /** |
根据角标移除元素的方法源码如上所示,值得注意的地方是:
rangeCheck 和 rangeCheckForAdd 方法不同 ,rangeCheck 只检查了 index是否大于等于 size,因为我们知道 size 为 elementData 已存储数据的个数,我们只能移除 elementData 数组中 [0 , size -1] 的元素,否则应该抛出角标越界。
但是为什么没有和 rangeCheckForAdd 一样检查小于0的角标呢,是不是remove(-1) 不会抛异常呢? 其实不是的,因为 rangeCheck(index); 后我们去调用 elementData(index) 的时候也会抛出 IndexOutOfBoundsException 的异常,这是数组本身抛出的,不是 ArrayList 抛出的。那为什么要检查>= size 呢? 数组本身不也会检查么? 哈哈.. 细心的同学肯定知道 elementData.length 并不一定等于 size,比如:
1 | ArrayList<String> testRemove = new ArrayList<>(10); |
new ArrayList<>(10) 表示 elementData 初始容量为10,所以 elementData.length = 10 而我们只给集合添加了两个元素所以 size = 2 这也就是为啥要 rangeCheck 的原因了。
移除指定元素
1 | /** |
由上边代码可以看出来,移除元素和移除指定角标元素一样最终都是通过 System.arraycopy 将 index 之后的元素前移一位,并释放原来位于 size 位置的元素。
还可以看出,如果数组中有指定多个与 o 相同的元素只会移除角标最小的那个,并且 null 和 非null 的时候判断方法不一样。至于 equals 和 == 的区别,还有 hashCode 方法,我会之后在总结一篇单独的文章。等不急的可以先去网上找找喽。
注意,以下几点:
- Integer 重写的 equals 方法有自动拆箱;
- equals如果不重写,那跟 == 是一样的,如果要重写,必须 equal 和 hashcode 都重写;
- 最好的例子就是 HashSet,如果key不重写 hashcode(),那可能equals相等但是会导致存入两个一样的。
批量移除/保留 removeAll/retainAll
ArrayList 提供了 removeAll/retainAll 操作,这两个操作分别是 批量删除与参数集合中共同享有的元素 和 批量删除与参数集合中不共同享有的元素,保留共同享有的元素,两个方法只有一个参数不同!!!
1 | /** 批量删除与参数集合中共同享有的元素*/ |
可以看到移除指定集合中包含的元素的方法代码量是目前分析代码中最长的了,但是逻辑也很清晰:
从 0 开始遍历 elementData 如果 r 位置的元素不存在于指定集合 c 中,那么我们就将他复制给数组 w 位置, 整个遍历过程中 w <= r。
由于 c.contains(o)可能会抛出异常ClassCastException/NullPointerException,如果因为异常而终止(这两个异常是可选操作,集合源码中并没有显示生命该方法一定会抛异常),那么我们将会产生一次错误操作,所以 finally 中执行了判断操作,如果 r!= size 那么肯定是发生了异常,那么则将 r 之后的元素不在比较直接放入数组。最终得到的结果并不一定正确是删除了所有与 c 中的元素。
批量删除和保存中,涉及高效的保存/删除两个集合公有元素的算法,是值得我们学习的地方,写的真好哈哈哈哈哈!!!
ArrayList 的改查
对于一个ArrayList 的改查方法就很简单了,set 和 get 方法。下面我们看下源码吧:
修改指定角标位置的元素
1 | public E set(int index, E element) { |
查询指定角标的元素
1 | public E get(int index) { |
查询指定元素的角标或者集合是否包含某个元素
1 | //集合中是否包含元素 indexOf 返回 -1 表示不包含 return false 否则返回 true |
ArrayList 集合的 toArry 方法
其实 Object[] toArray(); 方法,以及其重载函数 \
1 | public Object[] toArray() { |
可以看到 Object[] toArray() 只是调用了一次 Arrays.copyOf() 将集合中元素拷贝到一个新的 Object[] 数组并返回。这个 Arrays.copyOf() 方法前边已经讲了。所以 toArray() 方法并没有什么疑问,有疑问的地方在于toArray(T[] a) 。
我们可以传入一个指定类型的标志数组作为参数,toArray(T[] a) 方法最终会返回这个类型的包含集合元素的新数组。但是源码判断了 :
如果 a.length < size 即当前集合元素的个数与参数 a 数组元素的大小的时候将和 toArray() 一样返回一个新的数组。
如果 a.length == size 将不会产生新的数组直接将集合中的元素调用 System.arraycopy() 方法将元素复制到参数数组中,返回 a。
a.length > size 也不会产生新的数组,但是值得注意的是 a[size] = null; 这一句改变了原数组中 index = size 位置的元素,被重新设置为 null 了。
下面我们来看下第三种情况的例子:
1 | SubClass[] sourceMore = new SubClass[4]; |
ArrayList 的遍历
ArrayList 的遍历方式 jdk 1.8 之前有三种 :for 循环遍历, foreach 遍历,迭代器遍历,jdk 1.8 之后又引入了forEach 操作,我们先来看看迭代器的源码实现:
迭代器
迭代器 Iterator 模式是用于遍历各种集合类的标准访问方法。它可以把访问逻辑从不同类型的集合类中抽象出来,从而避免向客户端暴露集合的内部结构。 ArrayList 作为集合类也不例外,迭代器本身只提供三个接口方法:
1 | public interface Iterator { |
ArrayList 中调用 iterator() 将会返回一个内部类对象 Itr 其实现了 Iterator 接口。
1 | public Iterator<E> iterator() { |
下面让我们看下其实现的源码:
正如我们的 for 循环遍历一样,数组角标总是从 0 开始的,所以 cursor 初始值为 0 , hasNext 表示是否遍历到数组末尾,即 i < size 。对于 modCount 变量之所以一直没有介绍是因为他集合并发访问有关系,用于标记当前集合被修改(增删)的次数,如果并发访问了集合那么将会导致这个 modCount 的变化,在遍历过程中不正确的操作集合将会抛出 ConcurrentModificationException ,这是 Java 「fail-fast 的机制」,对于如何正确的在遍历过程中操作集合稍后会有说明。
1 | private class Itr implements Iterator<E> { |
next 方法是我们获取集合中元素的方法,next 返回当前遍历位置的元素,如果在调用 next 之前集合被修改,并且迭代器中的期望操作数并没有改变,将会引发ConcurrentModificationException。next 方法多次调用 checkForComodification 来检验这个条件是否成立。
1 | "unchecked") ( |
只有 Iterator 的 remove 方法会在调用集合的 remove 之后让 期望 操作数改变使expectedModCount与 modCount 再相等,所以是安全的。
1 | // 实质调用了集合的 remove 方法移除元素 |
检查期望的操作数与当前集合的操作数是否相同。Java8 发布了很多函数式编程的特性包括 lamada 和Stream 操作。迭代器也因此添加了 forEachRemaining 方法,这个方法可以将当前迭代器访问的元素(next 方法)后的元素传递出去还没用到过,源码就不放出来了,大家有兴趣自己了解下。
1 |
|
ListIterator 迭代器
ArrayList 可以通过以下两种方式获取 ListIterator 迭代器,区别在于初始角标的位置。不带参数的迭代器默认的cursor = 0
1 | public ListIterator<E> listIterator(int index) { |
ListItr对象继承自前边分析的 Itr,也就是说他拥有 Itr 的所有方法,并在此基础上进行扩展,其扩展了访问当前角标前一个元素的方法。以及在遍历过程中添加元素和修改元素的方法。
ListItr 的构造方法如下:
1 | private class ListItr extends Itr implements ListIterator<E> { |
ListItr 的 previous 方法:
1 | public boolean hasPrevious() { |
ListItr 的 add 方法
1 | public void add(E e) { |
可能对比两个迭代器后,会对 cursor 指向的位置有所疑惑,现在我们来看下一段示例代码对应的图:
1 | private void testListItr(){ |
由此可以看 cursor 于 数组角标不同,它可以处的位置总比角标多一个,因为在我们使用 Iterator 操作集合的时候,总是要先操作 cursor 移动, listIterator.previous 也好 iterator.next() 也好,都是一样的道理,如果不按照规定去进行操作,带给使用者的只有异常。
java8 新增加的遍历方法 forEach
java8增加很多好用的 API,工作和学习中也在慢慢接触这些 API,forEach 操作可能是我继 lambda 后,第一个使用的 API 了(囧),jdk doc 对这个方法的解释是:
对此集合的每个条目执行给定操作,直到处理完所有条目或操作抛出异常为止。 除非实现类另有规定,否则按照条目集迭代的顺序执行操作(如果指定了迭代顺序)。操作抛出的异常需要调用者自己处理。
其实其内部实现也很简单,只是一个判断了操作数的 for 循环,所以在效率上不会有提升,但是在安全性上的确有提升,也少些很多代码不是么?
1 |
|
对于高级 for 循环以及最普通的 fori 方法这里不再赘述。下面我们看下面试会问到一个问题,也是我们在单线程操作集合的时候需要注意的一个问题,如果正确的在遍历过程中修改集合。
错误操作 1 在 for循环修改集合后继续遍历
第一个例子:
1 | List<SubClass> list2 = new ArrayList<>(); |
这个例子我们会发现,程序并没有抛出异常,但是从运行经过上来看并不是我们想要的,因为还有 SubClass.test = 3的数据在,这是因为 remove 操作改变了list.size(),而 fori 中每次执行都会重新调用一次lists2.size(),当我们删除了倒数第二个元素后,list2.size() = 3,i = 3 < 3 不成立则没有在进行 remove 操作,知道了为什么以后我们试着这样改变了循环方式:
1 | int size = list2.size(); |
果真程序抛出了角标越界的异常,因为这样每次 fori 的时候我们不去拿更新后的 list 元素的 size 大小,所以当我们删除一个元素后,size = 3 当我们 for 循环去list2.get(3)的时候就会被 rangeCheck方法抛出异常。
错误操作导致 ConcurrentModificationException 异常
我们分析迭代器的时候,知道 ConcurrentModificationException是指因为迭代器调用 checkForComodification 方法比较 modCount 和 expectedModCount 方法大小的时候抛出异常。我们在分析 ArrayList 的时候在每次对集合进行修改, 即有 add 和 remove 操作的时候每次都会对 modCount ++。
modCount 这个变量主要用来记录 ArrayList 被修改的次数,那么为什么要记录这个次数呢?是为了防止多线程对同一集合进行修改产生错误,记录了这个变量,在对 ArrayList 进行迭代的过程中我们能很快的发现这个变量是否被修改过,如果被修改了 ConcurrentModificationException 将会产生。下面我们来看下例子,这个例子并不是在多线程下的,而是因为我们在同一线程中对 list 进行了错误操作导致的:
1 | Iterator<SubClass> iterator = lists.iterator(); |
我们对操作1,2分别运行程序,可以看到,操作1很快就抛出了 java.util.ConcurrentModificationException 异常,操作2 则顺利运行出正常结果,如果对 modCount 注意了的话,我们很容易理解,list.remove(index) 操作会修改List 的 modCount,而 iterator.next() 内部每次会检验 expectedModCount != modCount,所以当我们使用 list.remove 下一次再调用 iterator.next() 就会报错了,而iterator.remove为什么是安全的呢?因为其操作内部会在调用 list.remove 后重新将新的 modCount 赋值给 expectedModCount。所以我们直接调用 list.remove 操作是错误的。对于多线程的影响这里不在展开这里推荐有兴趣的朋友看下这个文章 Java ConcurrentModificationException异常原因和解决方法;
经过了一轮分析我们我们知道了错误产生原因了,但是大家是否能真的分辨出什么操作是错误的呢?我们来看下边这个面试题,这是我在网上无意中看到的一道大众点评的面试题:
1 | ArrayList<String> list = new ArrayList<String>(); |
一道面试题
相信大家肯定知道这样操作是会产生错误的,但是最终会抛出角标越界还是ConcurrentModificationException呢?
其实这里会抛出角标越界异常,为什么呢,因为 for 循环的条件 list.iterator().hasNext(),我们知道 list.iterator() 将会new 一个新的 iterator 对象,而在 new 的过程中我们将 每次 list.remove 后的 modCount 赋值给了新的 iterator 的 expectedModCount,所以不会抛出 ConcurrentModificationException 异常,而 hasNext 内部只判断了 size 是否等于 cursor != size 当我们删除了一半元素以后,size 变成了 5 而新的 list.iterator() 的 cursor 等于 0 ,0!=5 for 循环继续,那么当执行到 list.remove(5)的时候就会抛出角标越界了。
总结
ArrayList 底层是一个动态扩容的数组结构,每次扩容需要增加1.5倍的容量
ArrayList 扩容底层是通过 Arrays.CopyOf 和 System.arraycopy 来实现的。每次都会产生新的数组,和数组中内容的拷贝,所以会耗费性能,所以在多增删的操作的情况可优先考虑 LinkList 而不是 ArrayList。
ArrayList 的 toArray 方法重载方法的使用。
允许存放(不止一个) null 元素
允许存放重复数据,存储顺序按照元素的添加顺序
ArrayList 并不是一个线程安全的集合。如果集合的增删操作需要保证线程的安全性,可以考虑使用 CopyOnWriteArrayList 或者使collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类.
不正确访问集合元素的时候 ConcurrentModificationException和 java.lang.IndexOutOfBoundsException 异常产生的时机和原理。
本文又长篇大论的分析了一波 ArrayList 的源码,对我个人而言这很有意义,在查看源码的过程中,注意到了平时很少有机会接触的知识点。当然这只是集合源码分析的开端,以后还会更细,其他常用集合源码的分析。如果大家感觉我写的还可以, 请留言 + 点赞 + 关注。
LinkedList
先吐槽一下,感觉LinkedList源码真的很简单啊哈哈哈哈哈,总共也才1000来行代码…而且连个扩容都没有,基本上就是对双向链表的一顿操作,所以它是为了凑代码量所以顺带利用双向链表的性质实现一下双向队列嘛…算了,还是耐着性质稍微分析一下吧…(就是尼玛方法有点多,名字我都看晕啦…)
话说回来,这些里面的函数就是 对双向链表做某些操作的标准答案吖!里面写的代码都挺优美的!可以欣赏一哈~
算了……我懒得分析了,直接丢个链接吧……
注意点
继承自
AbstrackSequentialList
并实现了List
接口以及Deque
双向队列接口,因此 LinkedList 不但拥有 List 相关的操作方法,也有队列的相关操作方法。LinkedList
集合底层实现的数据结构为双向链表LinkedList
集合中元素允许为 nullLinkedList
允许存入重复的数据LinkedList
中元素存放顺序为存入顺序。LinkedList
是非线程安全的,如果想保证线程安全的前提下操作LinkedList
,可以使用List list = Collections.synchronizedList(new LinkedList(...));
来生成一个线程安全的LinkedList
实现List,利用了其是两个单链表组成的,实现Deque,利用其是双向链表的性质,注意哈,Deque既可以当队列也可以当栈,所以队列和栈的方法LinkedList全有。
1 | public interface Deque<E> extends Queue<E> { |
Vector
简而言之,Vector 与 ArrayList 之间的关系,就是 HashMap 和 HashTable 之间的关系,Vector 的方法基本上都是 ArrayList 的方法 加个 synchronized 关键字组成的,这边讲下它与 ArrayList 的不同,就可以结束了!
不同点有:
- Vector 线程安全,是同步的;ArrayList 非线程安全,非同步的
- 扩容策略不同,ArrayList 扩容是变成原来的 1.5 倍,而 Vector 和 ArrayDeque 扩容一样,都是变为原来的两倍
- Vector 可以使用 Enumeration 和 Iterator 进行元素遍历,ArrayList 只提供了 Iterator 的方式
- 由于使用的线程同步,Vector 的效率比 ArrayList 低,但是 jdk8 之后,由于对 synchronized 的关键字的优化,引入了 偏向锁 等轻量级锁之后, Vector 和 ArrayList 的性能差异已经不大了
Queue
没啥好分析的,实现这个接口的,接触的比较多的也就是 PriorityQueue、ArrayDeque。
直接放链接:
注意点
就其实现而言,ArrayDeque采用了循环数组的方式来完成双端队列的功能。
- 无限的扩展,自动扩展队列大小的。(当然在不会内存溢出的情况下)
- 非线程安全的,不支持并发访问和修改。
- 支持fast-fail。
- 作为栈使用的话比比栈要快。
- 当队列使用比 LinkedList 要快。
- null元素被禁止使用,这个跟 ArrayList 和 LinkedList 也不一样
- 初始值跟 ArrayList 不一样,这里的初始值是 16,扩容也是直接扩容两倍,跟 ArrayList 的 1.5 倍不一样,扩容的时候利用了容量是2次幂这个性质,跟 HashMap 一样,直接与 length 相与即可。