Java集合框架中有三种类型结构:List集合、Set集合、Map映射,作为基础有必要巩固一下集合遍历的方式,如何选择合适的遍历,以及遍历过程中有哪些需要注意的地方。严格来说,Map并不属于Collection,是一种key-value型的数据类型,因此将List集合和Set集合放在一起。这里重点分析的是集合和映射的遍历方式及遍历问题,不在于比较三者有什么区别(关于它们的特性和区别,准备以后从源码分析的角度进行整理总结)。
目录
定义List/Set集合、Map映射:
public class Test {
public static void main(String[] args) {
String[] weathers = new String[]{"晴", "", "多云", "多云", "多云", "小雨", "阵雨", "暴雨", "雾霾", "多云转晴"};
// List
List<String> wList = new ArrayList<>(Arrays.asList(weathers));
// 转Set,默认HashSet
Set<String> wSet = wList.stream().collect(Collectors.toSet());
// Map
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < weathers.length; i++) {
map.put(i, weathers[i]);
}
System.out.println("List----------------》" + wList.size()); //10
System.out.println("Set----------------》" + wSet.size()); //8
System.out.println("Map----------------》" + map.size()); //10
}
}
一、List/Set集合遍历
这里以for的方式为例:
1、fori遍历:从前往后的顺序遍历
for (int i = 0; i < wList.size(); i++) {
if (wList.get(i).contains("转"))
wList.remove(i);
}
2、forr遍历:从后往前的顺序遍历
for (int i = wList.size() - 1; i >= 0; i--) {
if (wList.get(i).contains("转"))
wList.remove(i);
}
3、增强的for遍历:foreach
for (String weather : wList) {
if (weather.contains("转"))
wList.remove(weather); //java.util.ConcurrentModificationException
}
4、迭代器遍历:Iterator
for (Iterator<String> iterator = wList.iterator(); iterator.hasNext(); ) {
if (iterator.next().contains("转"))
iterator.remove();
}
5、Java8的forEach():
wList.forEach((str) -> {
if (str.contains("转"))
wList.remove(str); //java.util.ConcurrentModificationException
});
小结一下:
- 打开编译后的Test.class文件,不难看出:对于fori、forr、Java8的forEach()、迭代器等方式JVM编译源码时仍按照各自for的方式,对于foreach遍历JVM则按照迭代器Iterator方式(可以理解为一种Java语法糖);
- 为什么代码里要使用remove()实现删除集合元素的逻辑呢?这里就是为了引出java.util.ConcurrentModificationException问题。经测试fori、forr、迭代器Iterator均不存在这个异常问题,而foreach、java8的forEach()却会报异常,额?这个问题下面从源码的角度分析一番,来探究一下其中的原理。
二、Map映射遍历
1、foreach方式 -- keySet迭代key
for (Integer weather : map.keySet()) {
map.remove(weather); //java.util.ConcurrentModificationException
// System.out.println(weather + ":" + map.get(weather));
}
2、foreach方式 -- entrySet
for (Map.Entry<Integer, String> weather : map.entrySet()) {
map.remove(weather.getKey()); //java.util.ConcurrentModificationException
// System.out.println(weather.getKey() + ":" + weather.getValue());
}
3、foreach方式 -- 迭代value
// 缺点是只能遍历value
for (String value :map.values()){
if (value.contains("转"))
break;
// System.out.println(value);
}
4、迭代器:Iterator
for (Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator(); iterator.hasNext(); ) {
Map.Entry<Integer, String> entry = iterator.next();
iterator.remove();
// System.out.println(entry.getKey() + "" + entry.getValue());
}
5、Java8的forEach():
map.forEach((k, v) -> System.out.println(k + ":" + v));
map.forEach((k, v) -> map.remove(k)); //java.util.ConcurrentModificationException
小结一下:
- Map的foreach遍历方式,JVM编译时也是按照迭代器Iterator方式进行的(可以理解为一种Java语法糖);
- 通过foreach、Java8的forEach()等遍历方式,使用map.remove()删除会导致java.util.ConcurrentModificationException,换言之,Map使用迭代器方式删除是不存在ConcurrentModificationException的,因此关于遍历删除的问题有必要深入了解一下。
三、遍历删除的陷阱
从上面的测试结果可以看出,无论是Collection还是Map使用foreach方式遍历,在通过remove()删除元素时均存在java.util.ConcurrentModificationException,这个异常就是并发修改异常。以ArrayList集合为例,通过IDEA的debug模式找到问题在哪:
for (String weather : wList) { //foreach
if (weather.contains("转"))
wList.remove(weather);
}
1、从源代码点进去是找不到ConcurrentModificationException位置的,但能看到boolean remove(Object o) 中有个fastRemove()方法,里面有这个变量modCount++ ,先记住它,它的功能执行完一次remove()后变量modCount自增1。
protected transient int modCount = 0;
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++; //attention!!!!
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
}
2、既然foreach方式本质上是JVM编译时按照迭代器Iterator方式进行的,打开编译后的class文件:
// foreach遍历方式编译的结果
Iterator var5 = wList.iterator();
while(var5.hasNext()) {
String weather = (String)var5.next();
if (weather.contains("转")) {
wList.remove(weather);
}
}
while每一次循环都要调用一次迭代器Iterator的next()方法,这个方法的源码如下:
private class Itr implements Iterator<E> {
int expectedModCount = modCount; //默认0
...
public E next() {
checkForComodification(); //attention!!!!
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
...
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
注意到了attention标记的地方了吗,checkForComodification()是个检查异常的方法,之前程序抛出的ConcurrentModificationException异常就在此处,该方法判断modCount与expectedModCount不相等时抛异常。
找到问题之后,分析下foreach遍历过程:第一次执行while循环时,先调用next()方法,检查modCount与expectedModCount是否相等,modCount是为0,expectedModCount也是初始化的值0,所以一定相等,当执行到集合自身的remove()方法后modCount自增1,至此第一次循环是不会有问题的。当第二次执行while循环时,调用next()方法检查modCount与expectedModCount,此时modCount=1,而expectedModCount=0,不相等一定会抛出异常,因此这就是问题的根源所在!!!!!
既然集合自身的remove()方法存在问题,那使用迭代器Iterator的remove()方法怎么就可以了呢,还是看源码:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; //attention!!!!
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
迭代器的remove()在完成删除操作后,将expectedModCount = modCount;,这样保证下一次循环时next()方法检查不会抛异常了,而且自身的checkForComodification()检查也不会抛ConcurrentModificationException。
小结一下:
增强的for循环方式给我们使用带来了便利,但不熟悉Java这种语法糖本质时,使用它进行遍历删除会存在陷阱。既然知道了这个问题,所以开发中使用for循环删除元素尽量避免使用foreach遍历删除,而替换使用fori、forr、迭代器Iterator等这些方式。
ConcurrentModificationException异常是与modCount变量紧密相关的,而非安全集合中的add()、addAll()、remove()、clear()等方法均存在modCount++,因此多线程环境中去操作如ArrayList集合、LinkedHashMap等,势必会造成线程安全的问题,这就是为什么建议在并发编程中使用安全集合(如Vector、Hashtable等),以及J.U.T包下集合的原因了。
四、fail-fast与fail-safe机制
上面分析到非安全集合迭代器Iterator中的ConcurrentModificationException异常,不免会联想到Java集合中fail-fast(快速失败)机制。
从源码包展开rt.jar,里面的java.util包和java.util.concurrent包(J.U.T并发包)下均存在Java中能用到的所有集合。翻看一番后,发现java.util包下集合类的迭代器Iterator(随机看了Hashtable的Enumerator、WeakHashMap的HashIterator、LinkedList的LLSpliterator等)采用的是ConcurrentModificationException异常。
而J.U.T并发包下集合类的迭代器Iterator(随机看了CopyOnWriteArrayList 的COWIterator、ConcurrentHashMap的BaseIterator等)没有抛ConcurrentModificationException异常,而是通过集合备份的方式避免对源集合操作,这种机制就是fail-safe(安全失败)机制,通过两段源码感受下:
// CopyOnWriteArrayList 的COWIterator
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
...
static final class COWIterator<E> implements ListIterator<E> {
private final Object[] snapshot;
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements; //构造器内将源集合元素备份
}
...
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++]; //操作备份的集合
}
...
}
}
// ConcurrentHashMap的BaseIterator
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
...
static class BaseIterator<K,V> extends Traverser<K,V> {
final ConcurrentHashMap<K,V> map;
Node<K,V> lastReturned;
BaseIterator(Node<K,V>[] tab, int size, int index, int limit,
ConcurrentHashMap<K,V> map) {
super(tab, size, index, limit);
this.map = map; //构造器内将源Map备份
advance();
}
...
public final void remove() {
Node<K,V> p;
if ((p = lastReturned) == null)
throw new IllegalStateException();
lastReturned = null;
map.replaceNode(p.key, null, null); //操作备份的Map
}
}
}
因此采用fail-safe机制的集合,能保证多线程环境中操作相关方法时是安全的!总结下JUT包下哪些集合可以替代java.util包下的非安全集合:
-
CopyOnWriteArrayList:并发List集合,CopyOnWrite表达的含义是:原容器会复制出一个新的容器,修改等操作在新容器内完成后,将原容器的引用指向新容器即可。因为并发去读的原容器不会做任何修改动作,并发读是线程安全的,而写是在新容器内实现的,这样可能会存在数据的可见性和数据的一致性等问题。所以CopyOnWriteArrayList是一种读写分离的思想体现,适用于读多写少的场景,如加载系统级别的信息,查询黑白名单等业务;(高并发环境中,Vector提供的锁基本都是方法级别的锁,颗粒度较大,并发效率较低,这里CopyOnWriteArrayList特别适合替代Vector来实现并发操作安全)。
-
CopyOnWriteArraySet:并发Set集合,底层利用CopyOnWriteArrayList实现的;
-
ConcurrentHashMap: 并发HashMap,Java7时采用分段锁的思想实现(Segment数组+ReentrantLock锁):即将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。而Java8时抛弃了这种实现,采用了Node数组+单向链表+红黑树的数据结构, Node数组元素作为锁来实现每一行数据进行加锁来进一步减少并发冲突的概率(Java8对ConcurrentHashMap是重大改进的一个版本,新加入的红黑树及使用优化后的sychornized都值得好好学习!),它适用于读多写少的场景,如管理在线用户的登入/登出/超时; (高并发环境中,Hashtable和Collections.synchronizedMap提供的锁基本都是方法级别的锁,并发性能较低,适合使用ConcurrentHashMap替代)。
-
ConcurrentSkipListMap:并发有序的Map(可类比非安全有序的Map集合:TreeMap),SkipList表达的含义是:一种跳表,跳表可以分为很多层,每一层的数据都是有序排列的,上一层是下一层的子集,因此最底层包含了所有的有序数据,查询跳表时是按从上到下,从左往右进行的。ConcurrentSkipListMap适用于并发环境中对数据有排序要求的场景;
-
ConcurrentSkipListSet:并发有序的Set集合(可类比非安全有序的Set集合:TreeSet),底层利用ConcurrentSkipListMap实现的;(Java8后Set集合的底层都是利用对应的Map来实现的,如TreeSet ->TreeMap、LinkedHashSet->LinkedHashMap等)。
以上这些安全集合的特性,优缺点和一些细节等知识点,在以后的JDK源码分析中再进行深入分析和整理吧,作为基础的话知道它们表达的含义、使用的场景即可。
最近在看Java集合源码,当看到ArrayList集合时,发现Java8新增了一个迭代器Spliterator,它能保证多线程操作ArrayList集合是线程安全的,但它仍然是fail-fast的,实现的原理大致是将ArrayList集合分段处理来保证并发安全,这个新特性很重要,打算在源码分析中再详细分析。
小结
从Java集合遍历角度出发,亲测了List/Set/Map等集合容器的常见遍历方式,尤其是特别注意进行遍历删除时存在的陷阱,对ConcurrentModificationException异常发生的原因和过程做了深入分析,又由此联想到Java集合迭代器Iterator中的fail-fast与fail-safe机制,通过源码看到了二者最本质的区别——底层是否通过抛ConcurrentModificationException异常,fail-safe是JUT包下集合采用的快速安全机制,并总结了CopyOnWriteXxx、ConcurrentXxx和ConcurrentSkipListXxx等类型的安全集合所表达的含义,使用场景等。当然,Java集合的基础知识点还有很多,值得花时间去进一步巩固,知识关联性的总结也是一种好的学习方法,做到有浅有深的梳理才能有所收获。不积硅步无以至千里,点滴付出终将有所收获,共同进步 ~