一.List
java 中的 List 是一种类似于列表的集合,Java集合—-Map说过 Map 接口的实现类是不保证添加顺序的(LinkedHashMap 除外) ,但是对于 List 的接口实现类,基本上都是保证了添加的顺序。
1 | public interface List<E> extends Collection<E> { |
二.ArrayList
1.继承关系
首先看一下 ArrayList 的继承关系
1 | public class ArrayList<E> extends AbstractList<E> |
与 Map 相似,ArrayList 也会继承一个抽象类 AbstractList 而这个抽象类有继承自 AbstractCollection ,这两个抽象类的主要作用就是将一些通用的方法进行复用,对于具体的实现可能就会进行重写从而实现自己的特性。
除此之外,还看到 RandomAccess 这个接口
1 | * for typical instances of the class, this loop: |
这是一个 标记接口,在这个接口中的说明中有一段话 this loop: … runs faster than this loop: (如上),这段话的意思就是说对于 实现这个接口的类来说,使用 for 循环进行遍历要比使用迭代器快得多,比如在 Collections 的查巡操作中会根据书数组还是迭代器进行选择
1 | @SuppressWarnings("unchecked") |
至于 ArrayList 为什么要实现这个接口? 实际上就是因为 ArrayList 的内部实现是通过数组实现的。
标记接口就是标记实现这个接口的类具有某种特性。
2.数组
1 | transient Object[] elementData; // non-private to simplify nested class access |
ArrayList 从名字就可以猜出这是一个基于数组的集合,内部有一个用于存储元素的集合 elementData,这里使用 transient 进行修饰,就说明这个数组不进行序列化,但是 ArrayList 又支持序列化,这似乎有点矛盾,我们接着看。
1 |
|
ArrayList 并没有去序列化整个数组,而是对数组中的元素逐个进行序列化,这样做就减少空元素的序列化,加快序列化的速度。
3.add
1 | public boolean add(E e) { |
对于 ArrayList 的添加元素操作,首先会判断是否达到数组的长度,如果是就进行扩容,如果还没有就直接对数组进行赋值,最后将将集合元素加一。
这里的关键就是数组的动态扩容。
1 | /** |
最后的扩容就是调用 Arrays.copyOf 进行数组的复制。
3.remove
对于 ArrayList 的各种移除操作,最后都会调用下面两种方法之一。
1 | private void fastRemove(Object[] es, int i) { |
简单地说还是对数组进行复制,然后再将空出的位置置为 null ,等待 GC 回收。
4.subList
1 | public List<E> subList(int fromIndex, int toIndex) { |
subList 返回的是 ArrayList 的一个子视图,也就是子集合,实际的是 ArrayList 的一个内部类 SubList,虽然 SubList 也实现了 AbstractList 接口但是 SubList 并不是 ArrayList 的子类,== 和 instanceOf 会返回 false 。
1 | private static class SubList<E> extends AbstractList<E> implements RandomAccess { |
SubList 里并没有数组变量但是有一个root b变量 root 变量就是原本的集合,而且从两个构造器可以看出 SubList 也可以产生它 的 SubList 。而且 对于 SubList 的各种修改操作也都是调用 root 却修改原生的 集合,换句话说也就是对 SubList 的修改也会影响到原声的 ArrayList .
删除指定范围的元素就可以使用 list.subList(from,to).clear();
特别注意的是这进行这些方法的时候都回调用这个 checkForComodification 方法进行检查 .
1 | private void checkForComodification() { |
这个方法表示的是 如果 ArrayList 的 modCount 和 SubList 的 modCount 不一样的时候就会抛出异常,也就是说,在生成了 SubList 后,如果再对 ArrayList 进行修改则其 modCount 就会发生变化,这个时候如果又对 SubList 进行操作就会异常。
简单地说就是再生成 SubList 后,如果对 ArrayList 操作了,则不能对已生成的 SubList进行操作。
Array.asList
1 | public static <T> List<T> asList(T... a) { |
Arrays 有一个 asList 的返回一个 ArrayList ,从上面的源码可以看出,这个实际上也是 Arrays 的一个内部类,而且这个不是现在通常说的 ArrayList ,因此两个类也没 有继承关系,但是这个内部类可以转为 ArrayList 因为 ArrayList 的 构造器参数是 Collection<? extends E> c 而且这个内部类并没有包含 ArrayList 的所有方法,比如就没有 add 方法,不具备扩展和缩小。
对于数组 Array和 ArrayList 的区别 可以包含基本类型和对象类型,ArrayList 只能包含对象类型;Array 的大小是固定的,ArrayList 的大小是动态变化的;ArrayList 提供了更多的方法和特性, addAll()、removeAll()、iterator() 等。
5.equals
1 | public boolean equals(Object o) { |
对于 ArrayList 的equals 比较,最后还是比较其中的元素数量和每个元素对应时候符合 equals 。显然对于 ArrayList 和 SubList (这里取全部元素),就符合 equals 返回 true 。
6.线程安全
ArrayList 是不是线程安全的,一个方法是用 Collections.synchronizedList 方法把 ArrayList 变成一个线程安全的,另一个方法就是 Vector,它是ArrayList的线程安全版本,区别在于:Vector 可以指定增长因子,如果该增长因子指定了,那么扩容的时候会每次新的数组大小会在原数组的大小基础上加上增长因子;如果不指定增长因子,那么就给原数组大小*2。
ArrayList适用于查找但是不适用于增加和删除,因为内部的操作都是调用 System.arrayCopy 这种效率很低的方法进行处理,所以如果遇到了数据量略大且需要频繁插入或删除的操作效率就比较低了。
三.LinkedList
LinkedList 是以双向链表实现,链表无容量限制。
1 | public class LinkedList<E> |
LinkedList 的继承关系也很简单 AbstractSequentialList 用于保持有序性,除此之外还实现了 Deque ,表明这个一个 双向的链表,支持 Deque 的一些操作。对于每一个 Node 节点,包含了对上一个节点和下一个节点的引用。对于整个 LinkedList 持有头节点和尾节点的引用 。
1.基本操作
由于 LinkedList 是以链表形式存在,因此内部定义了许多对节点的基本操作
1 | /** |
基本上所有的增加删除都是基于上面这几个操作完成。
2.查询
LinkedList 不是以数组的为基础的,因此不能直接定位到指定的元素,需要进行遍历
1 | public E get(int index) { |
对于 LinkedList 的查巡遍历并不是从前往后进行的,而是进行判断,如果下标小于长度的一半就从头节点开始,否则就从尾节点开始。
访问的复杂度为 O(N/2))只有在链表两头的操作(比如 add()、addFirst()、removeLast() 或用在 iterator() 上的 remove() 操作)才不需要进行遍历寻找定位。
四.对比
- 当插入的元素比较靠后的时候,ArrayList 效率低,因为 ArrayList 将批量 copy 大量的元素。
- ArrayList 使用最普通的 for 循环遍历,数组元素之间没有关联,而迭代器强制将 RandomAccess 的 ArrayList 建立了前后遍历关系,且在每次遍历过程中进行了一堆判断,所以比较慢。 LinkedList 使用 foreach 循环比较快,前后元素是通过链表索引建立关联的,无法直接取到对应的下标,因此在使用普通的 index 索引下标遍历时就需要计算对应的元素在哪,二分法决定头部还是尾部遍历,然后一步步的遍历找到元素,所以在遍历中每次都要从头查找元素位置,十分低效率。而迭代器的实现就是指向下一个元素,迭代器直接通过 LinkedList 的指针进行遍历,一次遍历就能找到每个合适的元素。
- ArrayList 是动态数组顺序表,顺序表的存储地址是连续的,所以查找比较快,但是插入和删除时由于需要把其它的元素顺序移动,所以比较耗时。LinkedList 是双向链表的数据结构,同时实现了双端队列 Deque 接口,链表节点的存储地址是不连续的,每个存储地址通过指针关联,在查找时需要进行指针遍历节点,所以查找比较慢,而在插入和删除时比较快。
五.CopyOnWriteArrayList
CopyOnWriteArrayList 是 java 并发包中 concurrent 下的一个基于读写分离思想的容器,其主要作用是应用在多线程并发的情况读多写少的情况。
1 | //锁对象 |
从上面的实现中可以看出,CopyOnWriteArrayList 在添加元素的时候,通过加锁的方式来实现保证线程的安全,对每次添加一个元素,就是通过复制一个新的数组,然后将旧的引用指向新的数组。这就是一种写时复制的思想,因为写的时候是通过复制实现的,那么这个时候读也是没有问题的。
1 | /** |
对于读,这里并没有加锁,而是直接读取。因此对 CopyOnWriteArrayList 的并发读写可以分为三种情况:
- 1·如果写操作未完成,那么直接读取原数组的数据;
- 2·如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
- 3·如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
这种情况下,CopyOnWriteArrayList 不能保证写一个数据后马上对读可见,但是它保证了 CopyOnWriteArrayList 最后的数据都是一致的,因为添加是加锁的。由于 CopyOnWriteArrayList 的复制机制,因此性能在数据多或者数据较大的时候都会比较低,通过这种空间的解决了读写分离。