如果确定插入、删除的元素是在前半段,那么就使用LinkedList;如果确定你插入、删除的元素在比较靠后的位置,那么可以考虑使用ArrayList,当然1.8LinkedList有头尾指针速度是一样的。
如果你不能确定你要做的插入、删除是在哪儿呢?建议使用LinkedList,因为一来LinkedList整体插入、删除的执行效率比较稳定,没有ArrayList这种越往后越快的情况因为需要移动元素;二来插入元素的时候,元素多了ArrayList就要进行一次扩容,ArrayList数组扩容是一个既消耗时间又消耗空间的操作。
- ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
- 对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针,除非位置离头或者尾部很近
- 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据,除非操作发送在ArrayList尾部
- LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
时间复杂度:
ArrayList线性表
get(index) 直接读取下标索引,类似数组内存地址可以通过下标计算得出 O(1)
add(E) 加在末尾 O(1)
add(index,E) 加在指定位置后,后面元素需要向后移动 O(n)
remove(index) 直接读取下标索引查找元素O(1),然后移动后面的元素O(n) , O(n)
LinkedList 链表
get(E) 获取指定元素,遍历,需要一个for循环 O(n)
add(E) 加到末尾 O(1)
add(index,E) 需要查找第几个元素,指针操作 O(n)
remove(index) 从头向后遍历,直到找到第index-1个结点,这需要O(n)时间;找到以后,改变指针的指向,这需要O(1)的时间。所以这种情况下,时间复杂度为O(n)
remove(E) 删除指定节点,直接指针操作E节点 O(1)
有序性:
ArrayList、LinkedList、Vector都是有序的。 Map是无序的,LinkedHashMap是有序的,通过链表链接顺序。Set是无序的,并且set中的元素不能重复,set的底层实现其实是Map,有HashSet、TreeSet,LinkedHashSet是有序的。
代码基于JDK1.8
ArrayList
- 线性表,底层是object数组存数据
- 扩容机制:默认大小是10,扩容是扩容到之前的1.5倍大小,每次扩容是将原数组中的数据复制到新的数组中
- 添加:如果是添加到数组的指定位置,那么可能会挪动大量的数组元素,并且可能会触发扩容机制;如果是添加到末尾的话,那么只可能触发扩容机制.
- 删除:如果是删除数组指定位置的元素,那么可能会挪动大量的数组元素;如果是删除末尾元素的话,那么代价是最小的. ArrayList里面的删除元素,其实是将该元素置为null.
- 查询和改某个位置的元素是非常快的 O(1)
ArrayList Class
1 | public class ArrayList<E> extends AbstractList<E> |
AbstractList
在ArrayList的所有涉及结构变化的方法中都增加modCount的值,包括:add()、remove()、addAll()、removeRange()及clear()方法。这些方法每调用一次,modCount的值就加1。
add()及addAll()方法的modCount的值是在其中调用的ensureCapacity()方法中增加的。
Itr实现了Iterator()接口,其中也定义了一个int型的属性:expectedModCount,这个属性在Itr类初始化时被赋予ArrayList对象的modCount属性的值。
在next()方法中调用了checkForComodification()方法,进行对修改的同步检查
1 | final void checkForComodification() { |
在对一个集合对象进行跌代操作的同时,并不限制对集合对象的元素进行操作,这些操作包括一些可能引起跌代错误的add()或remove()等危险操作。在AbstractList中,使用了一个简单的机制来规避这些风险。 这就是modCount和expectedModCount的作用所在
1 | public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> { |
从srcPos到srcPos + length – 1的元素拷贝到目标数组的destPos到destPos + length – 1的位置处
1 | public final class System { |
Vector
线程安全的容器
1 | public class Vector<E> |
实现原理很简单,根据要删除的对象找到它的下标i,然后根据这个下标删除第i个元素即可。除了加上了锁,以及一些代码风格的,与ArrayList没有区别。
循环删除问题
根据ArrayList循环的方式和删除元素的方式,可能会出现各种各样的问题:
1 | public static void main(String[] args) { |
ArrayList的内部迭代器
1 | private class Itr implements Iterator<E> { |
这个迭代器在里面定义了一个 int expectedModCount = modCount;
记录当前ArrayList对象修改的次数。而从前面remove方法的分析我们看到,在每次remove方法的调用过程中,modCount这个成员变量是会自增长的,但是remove方法里面并不会更新expectedModCount。而我们可以看待迭代器里的next方法的第一步就是调用方法checkForComodification, 去判断这两个count是否相等,不相等就抛出异常。而在remove元素之后此处的modCount已经比expectedModCount大1了,因此会抛出一个ConcurrentModificationException。
因此在使用Foreach快速遍历时,ArrayList内部创建了一个内部迭代器iterator,使用的是hasNext和next()方法来判断和取下一个元素,就可能会出现ConcurrentModificationException。如何避免这个问题呢?最好的办法就使用内部的这个迭代器进行remove操作。如果多线程操作还是会引发ConcurrentModificationException,解决Vector ConcurrentModificationException 问题,当需要使用迭代器的时候,添加锁,遍历完成后,再释放锁
1 | public static void main(String[] args) { |
ArrayList和LinkedList有什么区别,它们是怎么扩容的
事实上就是数组和链表作为两种最核心的数据结构的区别
- ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构
- 对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针
- 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据
LinkedList不存在扩容的问题,其元素节点定义如下:
1 | private static class Node<E> { |
对于ArrayList来说,存在扩容的问题。在添加元素时我们从第一节的代码可以知道它会先确保容量是否足够,如果不够则会扩容,实现方法如下:
1 | /** |
LinkedList
双向链表
JDK1.6使用的是一个带有头结点的双向循环链表
JDK1.7开始 LinkedList没有头节点,使用头、尾指针。在链头/尾进行插入/删除操作,first /last方式更加快捷
插入/删除操作按照位置,分为两种情况:中间 和 两头
- 在中间插入/删除,两者都是一样,先遍历找到index,然后修改链表index处两头的指针。
- 在两头,对于循环链表来说,由于首尾相连,还是需要处理两头的指针。而非循环链表只需要处理一边first.prev/last.next,所以理论上非循环链表更高效。恰恰两头(链头/链尾) 操作是最普遍的
LinkedList也是是未同步的,多线程并发读写时需要同步,那么可以使用Collections.synchronizedList方法对LinkedList的实例进行一次封装,也可以改用村concurrent包下面包装类
1 | /** |
Arrays.sort的排序方法是什么
JDK1.6以下:调用sort方法时,默认是使用mergeSort算法
JDK1.7后,使用TimSort算法
排序规则实现Comparator接口
Array和ArrayList有何区别?什么时候更适合用Array?
Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。尽管ArrayList明显是更好的选择,但也有些时候Array比较好用:
- 如果列表的大小已经指定,大部分情况下是存储和遍历它们
- 对于遍历基本数据类型,尽管Collections使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢
- 如果你要使用多维数组,使用数组[][]比List方便
超时剔除功能
另启一个线程,判断元素是否超时,如果超时就移除。或者使用有界限循环队列。
BlockingQueue有超时机制