一、List
1)ArrayList:
概括的说,ArrayList是一个动态数组,它是线程不安全的,允许元素为null。
其底层数据结构依然是数组,它实现了List
java.io.Serializable接口,其中RandomAccess代表了其拥有随机快速访问的能力,ArrayList可以以O(1)的时间复杂度去根据下标访问元素。
因其底层数据结构是数组,所以可想而知,它是占据一块连续的内存空间(容量就是数组的length,数组的默认大小为10),所以它也有数组的缺点,空间效率不高。由于数组的内存连续,可以根据下标以O1的时间读写(改查)元素,因此时间效率很高。
当集合中的元素超出这个容量,便会进行扩容操作。扩容操作也是ArrayList的一个性能消耗比较大的地方,所以若我们可以提前预知数据的规模,应该通过public ArrayList(int initialCapacity) {}构造方法,指定集合的大小,去构建ArrayList实例,以减少扩容次数,提高效率。
基本操作总结:
(1)增加元素的操作:
增加单个元素的方法是add(E e)和add(int index, E e)。这两个方法都是向容器中添加新元素,这可能会导致capacity不足,因此在添加元素之前,都需要进行剩余空间检查,如果需要则自动扩容。
addAll()方法能够一次添加多个元素,根据位置不同也有两个版本,一个是在末尾添加的addAll(Collection extends E> c)方法,一个是从指定位置开始插入的addAll(int
index, Collection extends E> c)方法。跟add()方法类似,在插入之前也需要进行空间检查,如果需要则自动扩容;如果从指定位置插入,也会存在移动元素的情况。 addAll()的时间复杂度不仅跟插入元素的多少有关,也跟插入的位置相关。
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。
扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
public booleanadd(Ee) {
ensureCapacityInternal(size+ 1); // Increments modCount!!
elementData[size++] =e;
return true;
}
private void ensureCapacityInternal(intminCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity=Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(intminCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(intminCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity=minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity=hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData=Arrays.copyOf(elementData, newCapacity);
}
小结:
先判断是否越界,是否需要扩容。
如果扩容, 就复制数组。
然后设置对应下标元素值。
值得注意的是:
1 如果需要扩容的话,默认扩容一半。如果扩容一半不够,就用目标的size作为扩容后的容量。
2 在扩容成功后,会修改modCount
(2)删除元素的操作:
会修改modCount。remove()方法也有两个版本,一个是remove(int index)删除指定位置的元素,另一个是remove(Object o)删除第一个满足o.equals(elementData[index])的元素。删除操作是add()操作的逆过程,需要将删除点之后的元素向前移动一个位置。需要注意的是为了让GC起作用,必须显式的为最后一个位置赋null值。removeAll(Collection> c)方法批量删除一个列表的元素,用removeAll可以实现两个集合的差集。
需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。
publicEremove(intindex) {
rangeCheck(index);
modCount++;
EoldValue=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
returnoldValue;
}
(3)修改元素的操作:
不会修改modCount,相对增删是高效的操作。
public E set(int index, E element) {
rangeCheck(index);//越界检查
E oldValue =elementData(index); //取出元素
elementData[index] =element;//覆盖元素
return oldValue;//返回元素
}
(4)查找元素的操作:
不会修改modCount,相对增删是高效的操作。
public E get(int index) {
rangeCheck(index);//越界检查
returnelementData(index); //下标取数据
}
E elementData(int index) {
return (E)elementData[index];
}
(5)清空,clear
会修改modCount。
public void clear() {
modCount++;//修改modCount
// clear to let GC do its work
for (int i = 0; i
elementData[i] =null;
size = 0; //修改size
}
(6)包含contain
//普通的for循环寻找值,只不过会根据目标对象是否为null分别循环查找。
public boolean contains(Object o) {
return indexOf(o) >=0;
}
//普通的for循环寻找值,只不过会根据目标对象是否为null分别循环查找。
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i< size; i++)
if(elementData[i]==null)
return i;
} else {
for (int i = 0; i< size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
增删改查中,增导致扩容,则会修改modCount,删一定会修改。改和查一定不会修改modCount。
扩容操作会导致数组复制,批量删除会导致 找出两个集合的交集,以及数组复制操作,因此,增、删都相对低效。而改、查都是很高效的操作。
Fail-fast:
fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出ConcurrentModificationException异常。fail-fast机制并不保证在不同步的修改下一定会抛出异常,它只是尽最大努力去抛出,所以这种机制一般仅用于检测bug。
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
出现的场景:
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。
出现的原理:
public static voidmain(String[] args) {
List list = new ArrayList<>();
for (int i = 0 ;i < 10 ; i++ ) {
list.add(i +"");
}
Iterator iterator = list.iterator();
int i = 0 ;
while(iterator.hasNext()) {
if (i == 3){
list.remove(3);
}
System.out.println(iterator.next());
i ++;
}
}
看看最关心的next()方法,看看为什么在迭代过程中,如果有线程对集合结构做出改变,就会发生fail-fast:
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >=size)
throw newNoSuchElementException();
Object[]elementData = ArrayList.this.elementData;
if (i >=elementData.length)
throw newConcurrentModificationException();
cursor = i + 1;
return (E)elementData[lastRet = i];
}
从源码知道,每次调用next()方法,在实际访问元素前,都会调用checkForComodification方法,该方法源码如下:
final voidcheckForComodification() {
if (modCount !=expectedModCount)
throw newConcurrentModificationException();
}
可以看出,该方法才是判断是否抛出ConcurrentModificationException异常的关键。在该段代码中,当modCount != expectedModCount
时,就会抛出该异常。但是在一开始的时候,expectedModCount初始值默认等于modCount,为什么会出现modCount != expectedModCount,很明显expectedModCount在整个迭代过程除了一开始赋予初始值modCount外,并没有再发生改变,所以可能发生改变的就只有modCount,在前面关于ArrayList扩容机制的分析中,可以知道在ArrayList进行add,remove,clear等涉及到修改集合中的元素个数的操作时,modCount就会发生改变(modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,调用相关方法使集合的个数发生改变,就会使modCount发生变化,这样在checkForComodification方法中就会抛出ConcurrentModificationException异常。
序列化时也可能发生fail-fast:
private voidwriteObject(java.io.ObjectOutputStreams)
throwsjava.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount =modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i s.writeObject(elementData[i]); } if (modCount !=expectedModCount) { throw newConcurrentModificationException(); } } 避免fail-fast: 方法1 在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。看看ArrayList中迭代器的remove方法的源码: public void remove(){ if (lastRet <0) throw newIllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor =lastRet; lastRet =-1; expectedModCount = modCount; } catch(IndexOutOfBoundsException ex) { throw newConcurrentModificationException(); } } 可以看到,该remove方法并不会修改modCount的值,并且不会对后面的遍历造成影响,因为该方法remove不能指定元素,只能remove当前遍历过的那个元素,所以调用该方法并不会发生fail-fast现象。该方法有局限性。 方法2 使用java并发包(java.util.concurrent)中的类来代替ArrayList 和hashMap。 比如使用 CopyOnWriterArrayList代替ArrayList,CopyOnWriterArrayList在是使用上跟ArrayList几乎一样,CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。 对于HashMap,可以使用ConcurrentHashMap,ConcurrentHashMap采用了锁机制,是线程安全的。在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。即迭代不会发生fail-fast,但不保证获取的是最新的数据。 替代方案: 可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。 List list = new ArrayList<>(); List synList = Collections.synchronizedList(list); 也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。 List list = new CopyOnWriteArrayList<>(); Vector * Vector与其的区别在于Vector在API上都加了synchronized所以它是线程安全的,Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制; * Vector扩容时,是翻倍size,而ArrayList是扩容到1.5倍。 2)LinkedList: 概括的说,LinkedList是线程不安全的,允许元素为null的双向链表。 其底层数据结构是双向链表,它实现List java.io.Serializable接口,它实现了Deque 因其底层数据结构是链表,所以可想而知,它的增删只需要移动指针即可,故时间效率较高。不需要批量扩容,也不需要预留空间,所以空间效率比ArrayList高。 缺点就是需要随机访问元素时,时间效率很低,虽然底层在根据下标查询Node的时候,会根据index判断目标Node在前半段还是后半段,然后决定是顺序还是逆序查询,以提升时间效率。不过随着n的增大,总体时间效率依然很低。 基本操作总结: 节点Node结构: private static class Node { E item;//元素值 Node next;//后置节点 Node prev;//前置节点 Node(Node prev,E element, Node next) { this.item = element; this.next = next; this.prev = prev; } } (1)增加元素的操作: 增加单个元素的add()方法有两个版本,一个是add(E e),该方法在LinkedList的末尾插入元素,因为有last指向链表末尾,在末尾插入元素的花费是常数时间。只需要简单修改几个相关引用即可;另一个是add(int index, E element),该方法是在指定下表处插入元素,需要先通过线性查找找到具体位置,然后修改相关引用完成插入操作。 批量增加元素的addAll()的方法: 链表批量增加,是靠for循环遍历待插入数组,依次执行插入节点操作。对比Array List是通过System.arraycopy完成批量增加的; 通过下标获取某个node的时候(增、查),会根据index处于前半段还是后半段进行一个折半,以提升查询效率。 (2)删除元素的操作: remove()方法也有两个版本,一个是删除跟指定元素相等的第一个元素remove (Object o),另一个是删除指定下标处的元素remove(int index)。 两个删除操作都要:1.先找到要删除元素的引用,2.修改相关引用,完成删除操作。 在寻找被删元素引用的时候remove(Object o)调用的是元素的equals方法,而remove(int index)使用的是下标计数,两种方式都是线性时间复杂度。在步骤2中,两个revome()方法都是通过unlink(Node 总结: 链表批量增加,是靠for循环遍历待插入数组,依次执行插入节点操作。对比ArrayList是通过System.arraycopy完成批量增加的。增加一定会修改modCount; 通过下标获取某个node的时候(增、查),会根据index处于前半段还是后半段进行一个折半,以提升查询效率; 删也一定会修改modCount。按下标删,也是先根据index找到Node,然后去链表上unlink掉这个Node。按元素删,会先去遍历链表寻找是否有该Node,如果有,去链表上unlink掉这个Node; 改也是先根据index找到Node,然后替换值。改不修改modCount; 查本身就是根据index找到Node。 为什么Java里的Arrays.asList生成的List不能用add和remove方法? 在平时的开发过程中,我们知道可以将一个Array的对象转化为List。这样的操作,我们只要采用Arrays.asList这个方法就行了。前段时间一直用这个方法,有一天,我发现通过Arrays.asList得到的List无法进行add和remove等操作。 下面是一段很简单的测试代码: public class MainFacade{ public static voidmain(String[] args) { List list = Arrays.asList(1,2,3); list.add(5); System.out.print(list.toString()); } } 不过上面的代码会throw出一个UnsupportedOperationException这样的异常: Exception in thread "main"java.lang.UnsupportedOperationException atjava.util.AbstractList.add(AbstractList.java:148) atjava.util.AbstractList.add(AbstractList.java:108) atorg.popkit.MainFacade.main(MainFacade.java:14) atsun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) atsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:606) atcom.intellij.rt.execution.application.AppMain.main(AppMain.java:134) 终其原因是Arrays.asList方法返回的ArrayList是继承自AbstractList同时实现了RandomAccess和Serializable接口,定义如下: private static classArrayList extends AbstractList implementsRandomAccess, java.io.Serializable 我们再来看看AbstractList这个类的定义: public abstract class AbstractList extendsAbstractCollection implements List 这时我们发现AbstractList这个类的set add remove方法定义如下: public void add(intindex, E element) { throw newUnsupportedOperationException(); } public E set(int index,E element) { throw newUnsupportedOperationException(); } public E remove(intindex) { throw newUnsupportedOperationException(); } 现在知道了它throw UnsupportedOperationException异常的原因了。 通过上面的分析,我们知道,其实通过asList方法得到的List是只读的,那么平时我们怎样避免这样的错误发生?我们可以采用如下方法: List list = newArrayList<>(Arrays.asList(1,2,3)); 为什么通过new生成的List可以正常执行add、remove等操作呢? public class ArrayList extends AbstractList implementsList, RandomAccess, Cloneable, java.io.Serializable 因为在ArrayList中覆盖和重新实现了父类AbstractList中的add和remove等方法。