ArrayList
小结
- ArrayList底层实现是数组,而且允许元素为null值
- 增加和删除的操作效率低,因为有扩容和数组复制的开销存在(如果是根据下标去删除元素的会比较快)
- 查询和修改的效率较高,可以直接通过下标去访问元素,时间复杂度为O(1)
- ArrayList 插入元素需要复制的元素,所以 ArrayList 插入元素的位置越靠后效率越高
- 多线程操作同一个ArrayList是不安全的
PS:如果事先知道要存储的元素个数,那么最好使用带
int
参数的构造函数,因为这样能够减少数组扩容的开销
大致结构
ArrayList是使用数组实现的列表,具有顺序性。ArrayList内部使用了transient Object[] elementData;
来存储列表中的元素(即数组的缓存区);ArrayList的容量就是该缓存区的数组长度
private static final int DEFAULT_CAPACITY = 10
默认容量,只有在add的时候判断elementData是空数组的时候,就会初始化这个默认容量的数组赋值给elementDatamodCount
这个属性是用来记录List列表的表结构发生变化的次数(要对list的结构发生变化的操作,都会使得modCount的值进行加一;结构性操作指对列表长度进行修改的,即add/remove操作);这个属性比较重要
modCount其实是fail-fast 机制的实现,即当某一个线程A通过
Iterator
去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException
异常,产生fail-fast事件
构造方法
- 无参构造方法
1 | //无参构造方法 |
- 带int参数的构造方法
1 | public ArrayList(int initialCapacity) { |
- 参数大于0返回
int
大小的数组 - 参数为0返回空的数组
- 参数小于0则抛出异常
IllegalArgumentException
- 构造一个包含指定元素的列表
1 | public ArrayList(Collection<? extends E> c) { |
构造指定元素的列表,当指定的元素为空时返回的是一个空的数组;如果元素不为空,那么直接得到一个指定元素的数组
这里有个官方的注释,这里的作用是:如果得到的element数组类型不是Obejct类型的数组,那么就要将其转换成Object类型的数组,至于这样做的原因是:
如果没有这个Object类型转换的情况会是怎么样的,我们模拟一下
1
2
3 > List<Object> list = new ArrayList<Object>(Arrays.asList("yes", "no"));
> list.set(0, new Object());
>
注意:此处模拟指没有类型转换的情况执行上面的代码,会调用指定元素的构造方法(推荐debug看)
当执行
elementData = c.toArray();
之后,elementData会是 String[] 类型的数组,而不是 Object[] 类型的那么执行完该构造方法之后,上面代码就会等价于
1
2
3 > Object[] list = new String[]{"yes","no"};
> list[0]=new Object();
>
- 然后当你再执行
list.set(0, new Object());
的时候,就会抛出ArrayStoreException
异常了根本原因是
toArray()
方法中调用的是copyOf
方法,该方法实际使用的是泛型,虽然返回的是 Object[] 类型的数组,但是实际的类型已经声明成 String[] 了
增删改查方法
System.arraycopy、Arrays.copyOf
了解这两个方法对看下面几个方法很有帮助
1 | /** |
add
1 | //执行顺序1. |
addAll(Collection<? extends E> c)
方法和普通add方法类似,主要使用了System.arraycopy
方法来拷贝指定元素
Set
1 | //插入到index的下标的位置,然后返回旧的值 |
Get
1 | public E get(int index) { |
Remove
- 根据传入下标来移除列表中的元素
1 | public E remove(int index) { |
- 根据传入的对象来删除元素
1 | public boolean remove(Object o) { |
- 移除包含指定元素的列表
1 | public boolean removeAll(Collection<?> c) { |
ArrayList的迭代器
1 | public Iterator<E> iterator() { |
ArrayList使用的迭代器是其内部类Itr
其内部类还有
ListItr
也是迭代器,只是这个迭代器可以在任意方向遍历列表,即可以向前/向后遍历。可以通过public ListIterator<E> listIterator()
方法得到该迭代器
此处专注于迭代器Itr
,先来查看该类的属性
int cursor
:要返回的下一个元素的索引int lastRet = -1
:返回的最后一个元素的索引,如果没有返回-1int expectedModCount = modCount
:结构修改次数
- 遍历元素的方法
在遍历元素前列表都会比较结构修改次数,如果结构修改次数不对应,那么会抛出ConcurrentModificationException
异常;所以在多线程对同个列表进行迭代和增删操作的时候,很大可能会抛出异常
1 | //是否还有下一个可遍历的元素 |
- 移除元素
1 | public void remove() { |
ArrayList的遍历方式及问题
1 | /** |
注意:
for循环遍历元素的时候,如果需要在for循环中删除元素,此时需要注意:
- 使用foreach(即增强for循环)的话会有下标越界问题
- for循环如果长度length是每次判断都重新获取的话是没有问题的
- for循环如果是事先定义好长度length的话会发生下标越界问题
ArrayList元素去重
遍历列表去重
实现思路:
- 定义(new)一个新的List集合
- 遍历原集合中的每一个元素,然后判断新集合中是否包含了该元素
- 包含就不添加到新集合
- 不包含则加到新集合中
- 然后返回新的集合得到去重后的List
自定义对象去重
实现思路:跟遍历去重方式思路基本上一致,只是对于对象的遍历判断,是需要重写equals和hasCode方法来比较对象是否重复
JDK8的流方式去重
1 | list.stream().distinct().forEach(System.out::println); |
equals()
和hasCode()
方法来定义对象是否重复的规则### SubList方法的坑
SubList方法能够返回指定下标区间的父List的视图、视图、视图
1 | //不包含toIndex下标的元素,即左闭右开 |
要注意的地方就是:调用这个方法并不是返回一个全新的列表,而是返回一个视图。所以在操作这个方法得到的视图会影响原列表的内容。简单来说就是subList后得到的列表跟原列表是同一个对象
## LinkedList
### 小结
- LinkedList底层的实现是链表(双向链表),可以允许元素为null值,LinkedList是有序集合
- 增加和删除的时候只需要修改节点上的指针即可,效率比较高
- 查找和修改的时候就只能从链表的头出发一直往下遍历找到该目标元素的节点才能进行操作,效率很低
- LinkedList内部查询的时候使用的
size >> 1
的方式(即折半法查找)来查找index,从而提高查询的效率- LinkedList 集合插入元素的位置影响 LinkedList 集合插入的效率,插入位置越靠前或者越靠后,效率越高;位置越中间效率低
大致结构
transient Node<E> first
存放头结点transient Node<E> last
存放尾节点- modCount:fail-fast机制
- Node节点;LinkedList内部使用了Node类的存放元素的,新增元素的时候,会将元素包装成Node节点来进行操作
1 | private static class Node<E> { |
构造方法
- 无参构造(真无参。。)
1 | public LinkedList() { |
- 指定元素列表的构造器
1 | //调用addAll方法,这个下面详说 |
增删改查方法
add
LinkedList的增加方法的核心是linkBefore(E e, Node<E> succ)
、linkFirst(E e)
、linkLast(E e)
和node(int index)
1 | //添加元素到链表末尾 |
add(E e)
1 | public boolean add(E e) { |
add(int index, E element)
1 | public void add(int index, E element) { |
addAll
方法,带参构造函数中也调用此方法
1 | public boolean addAll(Collection<? extends E> c) { |
Get
1 | //返回头节点元素内容 |
Remove
LinkedList的删除方法的核心是unlink(Node<E> x)
、unlinkFirst(Node<E> f)
和unlinkLast(Node<E> l)
1 | //要求参数 x 不能为null |
remove()
移除第一个节点,即当前头节点remove(int index)
移除下标是index的节点,先检查下标是否越界,再利用node(int index)
方法得到节点,最后用unlink(Node<E> x)
移除removeFirst()
和removeLast()
方法。根据名称可知作用,如果头结点/尾节点为null的情况,即链表为空的情况会抛出NoSuchElementException
,然后各自调用unlinkFirst
和unlinkLast
删除节点remove(Object o)
1 | //删除指定元素的节点,利用for循环便利链表,找到第一个配对的元素删除 |
Set
1 | public E set(int index, E element) { |
pop、poll、peek、push、offer
- peek获取头结点;如果链表为空则返回null
- pop弹出头结点,头结点会被删除。如果链表为空,那么抛出
NoSuchElementException
- poll弹出头结点,头结点会被删除。如果链表为空则返回null
- push插入元素作为头结点
- offer插入节点到链表末尾,相当于入队操作
peek和pop方法的区别是:peek方法和pop方法都是返回头节点的元素,但是peek方法不会删除头结点只返回null,而pop方法会删除头节点(相当于出栈)
ArrayList和LinkedList的区别
底层实现和线程安全问题
- ArrayList
- 底层数据结构是数组。线程不安全
- LinkedList
- 底层数据结构是链表。线程不安全
- Vector
- 底层数据结构是数组。线程安全(方法都加上了
synchronized
同步锁)
- 底层数据结构是数组。线程安全(方法都加上了
增删改查方面
总体来说:
- ArrayList 查询和修改要比 LinkedList 快
- LinkedList 增加和删除要比 ArrayList 快
- 如果删除元素是删除末尾元素的情况下,ArrayList 要比 LinkedList 快,因为 LinkedList 需要操作前后指针
- 如果删除的是中间的位置的元素,还是 ArrayList 快,因为 LinkedList 的折半查找法分别是从 头/尾 往中间找元素的,所以索引越靠近中间,LinkedList 的性能就越差
- 如果增加的元素一直都是在列表的最后增加, 那么 ArrayList 会比 LinkedList 快;此时的扩容操作占整体时间是很少的,而 LinkedList 还需要新建对象并且操作链表的前驱和后继节点
CopyOnWriteArrayList
CopyOnWriteArrayList
是JUC包提供的线程安全的ArrayList,底层通过复制数组的方式来实现;CopyOnWriteArrayList
在迭代器遍历的使用不会抛出ConcurrentModificationException
异常(即没有快速失败机制),并且迭代器遍历的时候就不用额外加锁
copy-on-write是指写时复制;如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变
优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源
大致结构
final transient ReentrantLock lock = new ReentrantLock()
该列表通过 ReentrantLock 来实现加锁操作private transient volatile Object[] array
CowArrayList用来存储数据的数组
对数组的修改操作
add
1 | public boolean add(E e) { |
CowArrayList通过复制一个新数组,并且增加操作在新数组上完成,最后将array指向到新数组中;跟ArrayList的区别就是:
- CowArrayList有加锁操作
- CowArrayList新增的操作在复制好的新数组上完成,而ArrayList是在原数组上完成
CowArrayList的set()
、remove()
操作都类似add()
操作,都是在新数组上完成操作后将array指向新数组
剖析迭代器
因为CopyOnWriteArrayList
在迭代器遍历的使用不会抛出ConcurrentModificationException
异常,并且迭代器遍历的时候就不用额外加锁;这个原因就在于CopyOnWriteArrayList
在使用迭代器的时候,保存了一份原数组的副本
1 | static final class COWIterator<E> implements ListIterator<E> { |
显而易见:
在调用迭代器的方法时,迭代器会保存一份原数组的副本,即snapshot
。该迭代器的所有操作都是对原数组进行操作,因此也就没有线程间的读写问题了,也就不需要快速失败机制和加锁操作了,所以CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性
CopyOnWriteArrayList小结
- 如果 CopyOnWriteArrayList 需要经常增删改列表的数据,经常要执行
add()、set()、remove()
的话,那是比较耗费内存的(因为都需要复制一个新数组进行增删改) - 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性
- 相对于ArrayList和Vector来说,遍历数组时不会受其他线程对数组操作而造成影响(读的是原数组的副本)
Set 对应的是 CopyOnWriteArraySet