List
ArrayList
1、数据结构
基于数组结构,默认大小为10,支持快速随机访问 (实现了RandomAccess),但插入删除代价高
当我们装载的是基本类型的数据int,long,boolean,short,byte…的时候我们只能存储他们对应的包装类,它的主要底层实现是数组Object[] elementData。
2、主要使用的api
boolean add(E e)
E get(int index)
int size()
3、一般使用流程
//1、创建ArrayList
List list = new ArrayList();
//2、添加元素
list.add("1");
list.add("2");
list.add("3");
//3、从list中取出某个元素
String s = (String)list.get(1);
//4、遍历整个list
for (Object o : list) {
String str = o.toString();
}
//5、指定位置添加
list.add(2,"cf");
//6、指定位置删除
list.remove(2);
4、重要参数
//默认大小
private static final int DEFAULT_CAPACITY = 10;
//未初始化前的默认容量空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList底层存储的数组
transient Object[] elementData;
//ArrayList中的元素个数
private int size;
transient 动态序列化,只序列化有元素部分的数组
5、构造函数
public ArrayList() //无参构造
public ArrayList(Collection<? extends E> c) //将集合c复制到一个ArrayList中
public ArrayList(int initialCapacity) //创建指定容量大小的ArrayList
无参构造:创建的 ArrayList 未初始化(底层数组为空数组)直到首次执行 add() 初始化扩容为默认长度10的数组
指定容量构造: ArrayList 会创建长度为 initialCapacity
的数组,首次执行 add() 一般不再触发扩容
6、add过程(以下代码均来源于jdk 11)
//1、提供给外界调用的方法
public boolean add(E e) {
modCount++; //操作数,ArrayList实现了快速失败
add(e, elementData, size);
return true;
}
//2、每当size==数组长度时,扩容
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow(); //第一次进来触发初始化扩容
elementData[s] = e;
size = s + 1;
}
7、扩容grow()
条件:s == elementData.length,原数组满了
流程:
- 重新定义一个长度约为原数组1.5倍的新数组
- 将原数组中数据原封不动的复制到新数组中,再将地址指向新数组即可
//3、扩容
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
//创建一个新的数组,将原数组中的元素复制过去
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
private int newCapacity(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //对1.7的优化,效率更高
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//首次初始化为10
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
扩容:grow();每次扩容为原容量+原容量/2(1.8优化通过右移位运算实现),通过创建新容量数组并复制旧元素到新数组中,花销代价高
8、指定位置add流程:
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
//检查是否需要扩容
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
//复制从index到末尾的数组,将它放到index+1,就是往后挪一个位置出来,当list很大时效率极低
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}
9、删除指定位置:
指定位置add的逆过程,也是要用到arraycopy,复制后面的数组挪上来
ArrayList插入删除一定很慢吗?
其实不一定,如果将它作为一个堆栈来用,它的push和pop操作不涉及数据移动,效率还是不错的。
ArrayList用来做队列合适么?不合适,会涉及数据移动
那数组适合用来做队列么?
合适,比如ArrayBlockingQueue内部实现就是一个环形队列,它是一个定长队列,内部是用一个定长数组来实现的。
10、遍历性能
ArrayList的遍历和LinkedList遍历性能比较如何?
论遍历ArrayList要比LinkedList快得多,ArrayList遍历最大的优势在于内存的连续性,
CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。
11、fail-fast:快速失败
参数:modCount
- modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
- 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。
问题:ArrayList使用forEach遍历的时候删除元素容易出现ConcurrentModificationException
因此需要遍历删除某个元素时,应该使用Iterator的方法remove,具体请参考:ArraryList遍历的时候删除元素会报错的原因
12、线程不安全
ArrayList不是线程安全的集合,在并发编程下不安全。
- 可以使用Collections.synchronizedList(放入list),或者Vector,但他们都是将传统方法统统加上synchronized,效率不高。
- concurrent并发包下的CopyOnWriteArrayList
LinkedList
基于双向链表,不支持随机访问,插入删除代价低,非线程安全
Vector
Vector 和 ArrayList 类似,但属于强同步类。如果程序本身是线程安全的(thread-safe,没有在多个线程之间共享同一个集合/对象),那么使用ArrayList是更好的选择。
线程安全,加锁同步但开销大,性能低,已经弃用;
扩容默认是两倍
与ArrayList的区别:
- 扩容大小:Vector每次请求其大小的双倍空间,而ArrayList每次对
size
增长50%。 - 线程安全
- 初始化:Vector在构造方法中就直接初始化数组且指定容量,而ArrayList在
add
方法中才进行初始化数组。 - 序列化:
ArrayList
使用了transient
关键字进行存储优化
ArrayList 的初始化时机
构造函数 ArrayList(int initialCapacity)会不会初始化数组大小?
不会初始化数组大小!准确来说,集合大小不会初始化为指定容量。
不管指定容量多少,构造出来的集合 size == 0;
List list = new ArrayList(10);
System.out.println(list.size()); // 0
list.set(5,"init");
// IndexOutOfBoundsException: Index: 5, Size: 0
即使创建了定长数据,为什么 set/add()方法 指定index(大于size)依然报错?
因为虽然ArrayList的底层是定长数组,但是其任何操作都是围绕 size 大小来执行的,这就保证了数组非空元素连续且置顶存放:[0,1,2,3,4,5,6,7,null,null]
ArrayList 的序列化
ArrayList
使用了transient
关键字进行存储优化,为什么?
-
由于其扩容机制,容量每次增加为原来的1.5倍。对于其底层数组来说,会预留一些空间,这些空间可能没有实际存储元素。
-
ArrayList
重写了writeObject
和readObject
序列化方法,保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
SynchronizedList
SynchronizedList是java.util.Collections
中的一个静态内部类。它与 Vector 同样是线程安全的集合,通过Collections.synchronizedList(List list)
方法获得。
主要了解SynchronizedList 与 Vector 的区别:
- Vector使用同步方法实现,synchronizedList使用同步代码块实现。后者能选择对哪个对象加锁,前者只能给this 对象加锁
- SynchronizedList 有很好的扩展和兼容功能,因为它可以将 ArrayList、LinkedList等List的子类都包装成线程安全,而 Vector 只能是数组。
- 使用SynchronizedList的时候,进行遍历时要手动进行同步处理,因为SynchronizedList没有对 Iterator 做同步处理,而Vector却对该方法加了方法锁。
CopyOnWriteArrayList
CopyOnWriteArrayList 是 CopyOnWrite容器中的一员。
Copy-On-Write简称COW,是一种程序设计中的优化策略。基本思路是:读时共享,写时复制。
原理
CopyOnWriteArrayList 是一个读写分离容器。
- 对它进行并发读时,不需要加锁,因为当前容器不进行写操作;
- 当有线程执行add操作时,会先从原数组copy出一个新数组,然后在新数组中写操作,写完后将原数组引用指向新数组,在整个add过程(写)加锁。
优势
- CopyOnWriteArrayList 可以当成线程安全的 ArrayList
- 适用于读多写少的并发环境下