Java集合框架主要分为两种类型容器
一种是集合Collection(有3种子类型:List、Set、Queue);一种是图Map
List | Set | Map |
---|---|---|
有序不唯一 | 无序唯一 | 无序key唯一非空 |
实现类ArrayList、LinkedList、Vector | 实现类HashSet、TreeSet | 实现类HashMap、 |
检索效率高,删除插入效率低会影响元素位置 | 检索效率低,删除插入效率高且不会改变位置 | |
SortedSet(TreeSet)有序唯一 | SortedMap(TreeMap)保持key有序 |
ArrayList
底层结构
ArrayList继承了AbstractList
底层结构是一个动态数组,对ArrayList的所有操作底层都是基于数组进行的
实现了RandomAccess接口(是一个标记接口),可以快速访问,具体表现为可以根据下标快速访问元素,因此读速度相对比较快。
线程安全性
ArrayList的写操作分为两个步骤,插入的时候首先在Object[size]的位置上放入新的元素,然后将size+1
就此而言,在多线程环境下已不具备原子性,所以是线程不安全的,更甚者如果是在指定位置插入元素,会涉及更多的步骤,要先移动待插入位置之后的元素,再将元素插入,然后size+1,所以写速度相对来说比较慢。
A线程进行add操作,假设size=0,将元素写入尚未修改size之前,cpu调度A暂停,B线程执行add操作,因为size尚未修改,B将元素也插入到0位置覆盖了A 的元素,之后AB都执行完修改size得到size=2,但实际集合中只有一个元素,造成线程不安全。
既然线程不安全,如何在多线程情况下保证线程安全呢?
1.Vector:使用同步方法实现线程安全,扩容扩充为员数组的2倍
2.Collections.synchronizedList:使用同步代码块实现,然后调用传入的list的自己的方法,使用迭代器遍历的时候需要手动进行同步处理;
3.CopyOnWriteArrayList :Juc包下的写时复制集合
List<String> arrayList=new Vector<>();
List<String> arrayList= Collections.synchronizedList(new ArrayList<>());
//由于Collections.synchronizedList中未对迭代器遍历进行同步处理,所以当时有迭代器遍历或增强for时需要手动进行同步处理
synchronized(this.list){
for (String s:this.list){
System.err.println(Thread.currentThread().getName()+":"+s);
}
}
//查询的时候,不加锁,随便访问,只有在写入/删除的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据
CopyOnWriteArrayList list=new CopyOnWriteArrayList();
Vector与synchronizedList的区别
Vector是将整个方法进行了同步,而synchronizedList是将ArrayList的方法用同步代码块包了起来,而彼此的方法又相差无几,所以在锁定范围上两者并无区别,Vector锁定对象是当前实例对象this,而synchronizedList有一个构造函数可以传入一个对象,如果没有指定就是当前实例对象this
CopyOnWriteArrayList : 采用ReentrantLock,lock.lock(); lock.unlock();
优点:适合一些读多写少的数据,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。缺点:这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。如果对象比较大,频繁地进行替换会消耗内存,从而引发Java的GC问题,这个时候,我们应该考虑其他的容器,例如ConcurrentHashMap。
动态扩容
官放介绍初始容量为10,提供了三个构造方法
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//参数为集合
public ArrayList(Collection<? extends E> c) {
//集合参数转过来的数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {
//转换的数组类型不是Object[],拷贝为Object[],这里Arrays.copyof方法实际是调用System.arraycopy
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//返回一个空的数组,EMPTY_ELEMENTDATA是一个空的数组,并未指定默认长度10
this.elementData = EMPTY_ELEMENTDATA;
}
}
//参数为初始长度
public ArrayList(int initialCapacity) {
//校验长度参数的可用性,>0初始化指定长度,=0初始化空数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}
//无参
public ArrayList() {
//DEFAULTCAPACITY_EMPTY_ELEMENTDATA也是一个空数组,并未指定默认长度10
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
看了构造方法,并未找到初始化长度为10的影子,我们不妨先来看看初始化之后,是如何添加元素的
private static final int DEFAULT_CAPACITY = 10;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//如果为空,取初始值和最小值中的较大值,可以看到DEFAULT_CAPACITY =10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;//修改次数+1,这个参数主要是用在集合的Fail-Fast机制(即快速失败机制)的判断中使用的
//如果最小长度大于当前长度,进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
//当前容量
int oldCapacity = elementData.length;
//新容量扩充为当前1.5倍,>>1为位运算的右移,即二进制去掉末尾一位相当于除以2
//比如10的2进制为1010,10>>1 即1010除掉末尾1位变为二进制的101即十进制的5
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新容量小于最小容量,扩充为最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//然后再一步判断,扩充后的容量大于最大长度(Integer的最大长度-8)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//如果最小长度大于最大长度,扩容为Integer的最大长度,否则扩充为Integer的最大长度-8
return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
我们可以看到在add的时候进行了初始容量的设置,并且进行了扩容,复制到新的数组,将新元素插入新的数组;
遍历踩坑
Iterator被创建之后,进行每一步操作都会比对当前list的操作次数modCount,如果遍历途中,对元素进行了写入或删除操作,Iterator会立即抛出ConcurrentModificationException异常。所以就有了几种骚操作:如何在遍历过程中删除元素
Iterator iterator=arrayList.iterator();
while (iterator.hasNext()){
System.err.println(iterator.next());
if (iterator.next().toString().endsWith("499")){
//arrayList.remove(iterator.next());//会报错 ConcurrentModificationException
iterator.remove();//如此甚好
}
}
为什么会这样呢?查看源码ArrayList.remove()会将modCount++,而Iterator创建之后modCount不再改变,每次遍历都会比对这个值,发现不同立即抛出异常,而反观Iterator自己的remove(),在删除当前元素的同时保证了modCount的一致性。另外我们还可以从末尾元素开始遍历元素并移除
//ArrayList
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
//Iterator
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
LinkedList
底层结构
双向链表
未完待续