1. ArrayList
- 基于数组实现,实现了 List 接口。(允许传入空值和重复元素)
- 当添加元素数量大于数组容量时,通过1.5倍的扩容机制进行扩容。
- 实现了 RandomAccess 接口,表明它具有随机访问的能力,RandomAccess 接口里其实什么内容都没有,它只是一个标识,ArrayList 能实现随机访问真正依靠的是基于数组实现的底层结构。
- 新增和删除操作会造成数组元素的移动,时间复杂度为 O(n)。由于支持随机访问,所以get和set操作的效率很高,时间复杂度为 O(1)。所以 ArrayList 适用于查询较多,而插入和删除较少的场景。
- 线程不安全。多线程环境下可以选择用 CopyOnWriteArrayList 。
- (实现了 java.io.Serializable 接口,意味着 ArrayList 支持序列化)
- (对ArrayList 进行遍历时,不推荐使用 foreach 循环遍历,因为 foreach 最终会被转换成迭代器遍历的形式,效率不如 for(int i=0;;)的遍历方式)
2. ArrayList如何扩容?负载因子是1
当new一个ArrayList时,如果不指定容量,那么初始容量为0,当对数组添加元素时,会默认分配10个容量。当容量用完后,会按照1.5倍扩容,扩容时会新建一个数组,容量是原本的1.5倍,将旧的数组中的元素添加到新的数组中,再将旧数组的引用指向新数组,就完成了扩容。
3. LinkedList
- 基于双向链表实现,不仅实现了 List 接口,还实现了 Deque 接口。
- 不支持随机访问,插入和删除操作的时间复杂度为 O(1),查找操作的时间复杂度为 O(n)。(优先使用 foreach 来遍历)
- ArrayList 需要在尾部预留空间,LinkedList 不用。
- LinkedList 用在查询较少、插入删除较多的场景。
- LinkedList 也是线程不安全的。
- (多线程环境下可以选择用 ConcurrentLinkedQueue,非阻塞线程安全的队列,基于 CAS 实现的) 。
- (实现了 java.io.Serializable 接口,所以支持序列化)
4. ArrayList和LinkedList的区别?实现原理
- ArrayList底层是动态数组;LinkedList底层是一个双向链表
- ArrayList可以随机访问元素;LinkedList不能随机访问元素,只能从头到尾依次访问。
- ArrayList在进行添加删除元素操作时,需要移动元素,效率很低;LinkedList在进行删除和添加元素时,只需要更改指针,效率较高。
5. ArrayList和LinkList是线程安全的吗,有线程安全的ArrayList吗?
都不是,Vector是线程安全的,内部的方法使用了synchronized关键字来同步。
concurrent 并发包下的CopyOnWriteArrayList是线程安全的,底层数组使用volatile修饰,修改数组时加ReentrantLock锁,但是只能保证最终数据一致性,不能保证数据的实时一致性。
6. CopyOnWriteArrayList是如何保证线程安全的?
-
原理:CopyOnWriteArrayList底层有一个array数组,是被volatile修饰的,可以在多个线程之间保持可见性,读操作没有加锁,array可以被多个线程同时读。写操作的话,在修改元素之前使用ReentrantLock进行加锁操作,然后将当前数组进行Copy,复制出一个新的数组,然后在新的数组里修改元素,修改完元素之后,再将原数组的引用指向新数组,最后释放锁。
-
过程:CopyOnWriteArrayList刚创建时,默认的大小为0,当向其插入一个元素时,将原数组复制到一个比原数组大1的新数组中,然后直接将插入的元素放置到新数组末尾,再修改array引用到新数组就可以,原来的数组就会被垃圾收集器回收。
-
为什么修改元素时要复制原数组去操作?
- 复制原数组再去操作,写操作在一个复制的数组上进行,读操作还是在原始数组上进行,就可以实现读写分离,互不影响。
- array属性是被volatile修饰的,将array指向一个新数组,其他线程就可以意识到这个修改,如果直接在原数组上进行修改的话,其他线程意识不到这个修改,读取的可能还是线程缓存中的数据。
-
初始化为什么要设置数组大小为0呢?
这是因为CopyOnWriteArrayList每次进行写操作时,都会复制原数组到新的数组中,实际占用的空间是原来的两倍,这样的空间开销,导致了CopyOnWriteArrayList不能像ArrayList那样初始化大小为10,不然太浪费空间了,而且CopyOnWriteArrayList主要用于读多写少的地方。 -
缺点:写操作时内存占用变为两倍;进行读操作时,写操作的数据还未同步,会导致不能读取实时性的数据,只能保证数据的最终一致性,所以不适合实时性要求很高的场景。
扩充知识点
- ArrayList 扩充知识点: 如果准备添加非常多的数据,那么通过for循环一步步添加时,每当碰到容量不足的情况就会进行一次1.5倍扩容,然后重新分配增量。为了重新分配的次数,可以先调用 ensureCapacity(要添加的数据大小) 方法,来让 ArrayList 进行对应的扩容,然后在添加数据,这样可以减少增量重新分配的次数。
- Java 提供的线程安全的队列(Queue) 有:
- 非阻塞型的 ConcurrentLinkedQueue :采用 CAS 实现。
- 阻塞型的 BlockingQueue 。阻塞队列