目录
实现类有ArrayList、LinkedList、Vector、Stack等
ArrayList读取速度快于LinkedList,而插入和删除速度又慢于LinkedList
System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
elementData设置成了transient,那ArrayList是怎么把元素序列化的呢?
(3)ArrayList插入、删除、查询元素的时间复杂度各是多少?
(10)LinkedList插入、删除、查询元素的时间复杂度各是多少?
(13)CopyOnWriteArrayList是怎么保证并发安全的?
(14)CopyOnWriteArrayList的实现采用了什么思想?
(15)CopyOnWriteArrayList是不是强一致性的?
(16)CopyOnWriteArrayList适用于什么样的场景?
(17)CopyOnWriteArrayList插入、删除、查询元素的时间复杂度各是多少?
(18)CopyOnWriteArrayList为什么没有size属性?
List常用实现类总结
List中的元素是有序的、可重复的,主要实现方式有动态数组和链表。
java中提供的List的实现主要有ArrayList、LinkedList、CopyOnWriteArrayList,另外还有两个古老的类Vector和Stack。
实现类有ArrayList、LinkedList、Vector、Stack等
○ArrayList是基于数组实现的,是一个数组队列。可以动态的增加容量!
○LinkedList是基于链表实现的,是一个双向循环列表。可以被当做堆栈使用!
○Vector是基于数组实现的,是一个矢量队列,是线程安全的!
○Stack是基于数组实现的,是栈,它继承与Vector,特性是FILO(先进后出)!
使用场景
- 当集合中对插入元素数据的速度要求不高,但是要求快速访问元素数据,则使用ArrayList。
- 当集合中对访问元素数据速度不做要求不高,但是对插入和删除元素数据速度要求高的情况,则使用LinkedList。
- 当集合中有多线程对集合元素进行操作时候,则使用Vector!但是现在BVector现在一般不再使用,如需在多线程下使用,可以用CopyOnWriteArrayList,在java.util.concurrent包下。
- 当集合中有需求是希望后保存的数据先读取出来,则使用Stack。
ArrayList读取速度快于LinkedList,而插入和删除速度又慢于LinkedList
原因
- ArrayList随机读取的时候采用的是get(index),根据指定位置读取元素,而LinkedList则采用size/2 ,二分法去加速一次读取元素。
- ArrayList插入时候要判断容量,删除之后的所有元素都要向数组的前端移动。 而LinkedList直接插入,不用判断容量,删除的时候很轻松,只需要更新被删除元素附近的链接。(链表将每个对象存放在独立的节点中,每个节点还存放着序列中下一个节点的引用)
ArrayList
常用方法:
System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
String[] arr = {"A","B","C","D","E","F"};
String[] arr2 = {"G","H","I","J","K","L"};
System.arraycopy(arr,3,arr2,2,2);
System.out.println(JSONObject.toJSONString(arr2));
结果: ["G","H","D","E","K","L"]
最后一个参数是需要复制的个数
arr的第3个index(包括index 3)开始的两个数据替换arr2的第2个index(包括index 2)开始的数据 替换两个数据
类方法等细节分析
(1)ArrayList内部使用数组存储元素,当数组长度不够时进行扩容,每次加一半的空间,ArrayList不会进行缩容;
(2)ArrayList支持随机访问,通过索引访问元素极快,时间复杂度为O(1);
(3)ArrayList添加元素到尾部极快,平均时间复杂度为O(1);
(4)ArrayList添加元素到中间比较慢,因为要搬移元素,平均时间复杂度为O(n);
(5)ArrayList从尾部删除元素极快,时间复杂度为O(1);
(6)ArrayList从中间删除元素比较慢,因为要搬移元素,平均时间复杂度为O(n);
(7)ArrayList支持求并集,调用addAll(Collection<? extends E> c)方法即可;
(8)ArrayList支持求交集,调用retainAll(Collection<? extends E> c)方法即可;
(7)ArrayList支持求单向差集,调用removeAll(Collection<? extends E> c)方法即可;
elementData设置成了transient,那ArrayList是怎么把元素序列化的呢?
查看writeObject()方法可知,先调用s.defaultWriteObject()方法,再把size写入到流中,再把元素一个一个的写入到流中。
一般地,只要实现了Serializable接口即可自动序列化,writeObject()和readObject()是为了自己控制序列化的方式,这两个方法必须声明为private,在java.io.ObjectStreamClass#getPrivateMethod()方法中通过反射获取到writeObject()这个方法。
在ArrayList的writeObject()方法中先调用了s.defaultWriteObject()方法,这个方法是写入非static非transient的属性,在ArrayList中也就是size属性。同样地,在readObject()方法中先调用了s.defaultReadObject()方法解析出了size属性。
elementData定义为transient的优势,自己根据size序列化真实的元素,而不是根据数组的长度序列化元素,减少了空间占用。
LinkedList
类方法等细节分析
(1)LinkedList是一个以双链表实现的List;
(2)LinkedList还是一个双端队列,具有队列、双端队列、栈的特性;
(3)LinkedList在队列首尾添加、删除元素非常高效,时间复杂度为O(1);
(4)LinkedList在中间添加、删除元素比较低效,时间复杂度为O(n);
(5)LinkedList不支持随机访问,所以访问非队列首尾的元素比较低效;
(6)LinkedList在功能上等于ArrayList + ArrayDeque;
CopyOnWriteArrayList
类方法等细节分析
【死磕 Java 集合】— CopyOnWriteArrayList源码分析
CopyOnWriteArrayList是ArrayList的线程安全版本,内部也是通过数组实现,每次对数组的修改都完全拷贝一份新的数组来修改,修改完了再替换掉老数组,这样保证了只阻塞写操作,不阻塞读操作,实现读写分离。
(1)CopyOnWriteArrayList使用ReentrantLock重入锁加锁,保证线程安全;
(2)CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下;
(3)CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1);
(4)CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于读多写少的场合;
(5)CopyOnWriteArrayList只保证最终一致性,不保证实时一致性,因为读写是在两个容器进行的,只有当写操作执行完毕引入指向新容器后,读才能感知到容器的变化。;
List相关面试问题整理
(1)ArrayList和LinkedList有什么区别?
1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。 (LinkedList是双向链表,有next也有 previous)
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
(2)ArrayList是怎么扩容的?
List.add 方法時 里面的 ensureCapacityInternal方法下的 grow方法
private void grow(int minCapacity) {
// 获取到ArrayList中elementData数组的内存空间长度
int oldCapacity = elementData.length;
// 扩容至原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组,
// 不够就将数组长度设置为需要的长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//若预设值大于默认的最大值检查是否溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间
// 并将elementData的数据复制到新的内存空间
elementData = Arrays.copyOf(elementData, newCapacity);
}
(3)ArrayList插入、删除、查询元素的时间复杂度各是多少?
ArrayList支持随机访问,通过索引访问元素极快,时间复杂度为O(1);
ArrayList添加元素到尾部极快,平均时间复杂度为O(1);
ArrayList添加元素到中间比较慢,因为要搬移元素,平均时间复杂度为O(n);
ArrayList从尾部删除元素极快,时间复杂度为O(1);
ArrayList从中间删除元素比较慢,因为要搬移元素,平均时间复杂度为O(n);
(4)怎么求两个集合的并集、交集、差集?
ArrayList支持求并集,调用addAll(Collection<? extends E> c)方法即可;
ArrayList支持求交集,调用retainAll(Collection<? extends E> c)方法即可;
ArrayList支持求单向差集,调用removeAll(Collection<? extends E> c)方法即可;
(5)ArrayList是怎么实现序列化和反序列化的?
查看writeObject()方法可知,先调用s.defaultWriteObject()方法,再把size写入到流中,再把元素一个一个的写入到流中。
一般地,只要实现了Serializable接口即可自动序列化,writeObject()和readObject()是为了自己控制序列化的方式,这两个方法必须声明为private,在java.io.ObjectStreamClass#getPrivateMethod()方法中通过反射获取到writeObject()这个方法。
在ArrayList的writeObject()方法中先调用了s.defaultWriteObject()方法,这个方法是写入非static非transient的属性,在ArrayList中也就是size属性。同样地,在readObject()方法中先调用了s.defaultReadObject()方法解析出了size属性。
elementData定义为transient的优势,自己根据size序列化真实的元素,而不是根据数组的长度序列化元素,减少了空间占用
(6)集合的方法toArray()有什么问题?
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
c.toArray之后返回的类型或许不是Object类型,貌似1.9已经修复了 ,不知道为什么会出现这种问题
(7)什么是fail-fast?
参考博客:https://www.cnblogs.com/myseries/p/10877362.html
fail-fast 机制是java集合(Collection)中的一种错误机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
这种“ 及时失败” 的迭代器井不是一种完备的处理机制,而只是“ 善意地” 捕获并发错误,因此只能作为并发问题的预警指示器。
public class FailFastTest {
static final List<Integer> list = new ArrayList<>();
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(() -> {
add((int)(Math.random() * 10));
print();
});
}
}
private static void add(int number) {
list.add(number);
}
private static void print() {
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
以上代码会抛出java.util.ConcurrentModificationException
异常。
(8)LinkedList是单链表还是双链表实现的?
双链表
(9)LinkedList除了作为List还有什么用处?
LinkedList还是一个双端队列,具有队列、双端队列、栈的特性
(10)LinkedList插入、删除、查询元素的时间复杂度各是多少?
LinkedList在队列首尾添加、删除元素非常高效,时间复杂度为O(1);
LinkedList在中间添加、删除元素比较低效,时间复杂度为O(n);
LinkedList不支持随机访问,所以访问非队列首尾的元素比较低效;
(11)什么是随机访问?
java集合类中元素的访问分为随机访问和顺序访问。随机访问一般是通过index下标访问,行为类似数组的访问。而顺序访问类似于链表的访问,通常为迭代器遍历。 以List接口及其实例为例。ArrayList是典型的随机访问型,而LinkedList则是顺序访问型。List接口既定义了下标访问方法又定义了迭代器方法。所以其实例既可使用下标随机访问也可以使用迭代器进行遍历。但这两种方式的性能差异很明显。
RandomAccess接口
JDK中的RandomAccess接口是一个标记接口,它并未定义方法。其目的是用于指示实现类具有随机访问特性,在遍历时使用下标访问较迭代器更快。如果:
for(int i = 0, n = list.size(); i < n; i++)
list.get(i);
的运行比
for(Interator i = list.iterator();i.hasNext();)
i.next();
快,则应实现RandomAccess接口。
原文链接:https://blog.csdn.net/yaogebeizhan/article/details/78180448
(12)哪些集合支持随机访问?他们都有哪些共性?
ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。
(13)CopyOnWriteArrayList是怎么保证并发安全的?
ReentrantLock, 以及 private transient volatile Object[] array 修饰 元素
(14)CopyOnWriteArrayList的实现采用了什么思想?
写入时复制(CopyOnWrite)思想
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
(15)CopyOnWriteArrayList是不是强一致性的?
不是,见(14),CopyOnWriteArrayList只保证最终一致性,不保证实时一致性,因为读写是在两个容器进行的,只有当写操作执行完毕引入指向新容器后,读才能感知到容器的变化。
(16)CopyOnWriteArrayList适用于什么样的场景?
适用于读多写少的场景
(17)CopyOnWriteArrayList插入、删除、查询元素的时间复杂度各是多少?
CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下;
CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1);
删除也是空间复杂度是O(n)
(18)CopyOnWriteArrayList为什么没有size属性?
因为每次修改都是拷贝一份正好可以存储目标个数元素的数组,所以不需要size属性了,数组的长度就是集合的大小,而不像ArrayList数组的长度实际是要大于集合的大小的。
比如,add(E e)操作,先拷贝一份n+1个元素的数组,再把新元素放到新数组的最后一位,这时新数组的长度为len+1了,也就是集合的size了。
参考博客: