目录
遍历集合的多种方法
(1)概述
迭代器的作用
迭代器模式:提供一个方法对一个容器对象中的各个元素进行访问,而又不暴露该对象容器的内部细节。
Java容器的种类有很多种,每种容器都有自己特有的数据结构。
因为容器的内部结构不同,很多时候可能不知道该怎样去遍历一个容器中的元素。所以为了使对容器内元素的操作更为简单,Java引入了迭代器模式,把访问逻辑从不同类型的集合类中抽取出来,从而避免向外部暴露集合的内部结构。
迭代器的局限
-
迭代器是针对单列集合使用的,即Collection家族。
-
只有实现了iterable接口,才能使用迭代器
-
Map集合不能使用迭代器
,但用Set集合获取它的键后,可以通过迭代器遍历Map的键,再获取对应的值,达到迭代Map的效果。 -
Iterator迭代器只能
从头到尾,单向遍历集合
。
(2)iterable接口
Java提供了一个Iterable接口。常用的实现了该接口的子接口有:Collection、List、Set等。
该接口的iterator()方法返回一个标准的Iterator实现。
(3)Iterator迭代器
遍历ArrayList
List<String> list = new ArrayList<String>();
list.add("张三1");
list.add("张三2");
list.add("张三3");
list.add("张三4");
//使用迭代器遍历ArrayList集合
Iterator<String> listIt = list.iterator();
while(listIt.hasNext()){
System.out.println(listIt.next());
}
遍历LinkedList
List<String> linkList = new LinkedList<String>();
linkList.add("link1");
linkList.add("link2");
linkList.add("link3");
linkList.add("link4");
//使用迭代器遍历LinkedList集合
Iterator<String> linkIt = linkList.iterator();
while(linkIt.hasNext()){
System.out.println(listIt.next());
}
遍历Set
Set<String> set = new HashSet<String>();
set.add("set1");
set.add("set2");
set.add("set3");
set.add("set4");
//使用迭代器遍历Set集合
Iterator<String> setIt = set.iterator();
while(setIt.hasNext()){
System.out.println(listIt.next());
}
使用迭代器遍历集合不需要考虑集合的种类及内部逻辑等,只需要控制迭代器本身,降低了耦合
迭代器遍历集合,就是利用集合对象获取迭代器,利用Iterator类型实例的hasNext()和next()方法。
- hasNext():判断集合中是否有下一个元素
- next():返回集合的下一个元素
迭代器注意事项
注意,在使用Iterator时,禁止对所遍历的容器进行改变其大小结构的操作
(add、remove)
否则会抛出ConcurrentModificationException(并发修改异常)。
由于foreach的本质也使用了迭代器,所以增强for同样有这个要求
。
错误代码示例
List<String> list = new ArrayList<String>();
list.add("张三1");
list.add("张三2");
list.add("张三3");
list.add("张三4");
//使用迭代器遍历ArrayList集合
Iterator<String> listIt = list.iterator();
while(listIt.hasNext()){
Object obj = listIt.next();
if(obj.equals("张三3")){
//改变集合大小
list.remove(obj);
}
}
在list.iterator()之后,迭代器就已经创建出来了。在这之后,不能对集合进行增删
。否则java会认为出现了线程不安全的问题
- 普通的迭代器,在迭代过程中,不能通过集合的方法,对集合本身作出增加元素和删除元素的修改。
- 但是
可以通过迭代器自身的方法Iterator.remove()来删除元素
通过源码,分析迭代器增删集合为什么抛出异常,以及Iterator.remove()为什么不会抛出异常
Iterator的源码
private class Itr implements Iterator<E> {
//用于遍历集合的索引
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
//检查版本号是否发生了改变
checkForComodification();
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];
}
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();
}
}
final void checkForComodification() {
//如果当前集合的版本号,与迭代器的版本号不符,就抛出并发修改异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
分析
-
ConcurrentModificationException异常,是由checkForComodification()方法抛出的。
迭代器每次next,都会先执行checkForComodification()
-
checkForComodification()方法的作用是比较modCount与expectedModCount是否相等,如果不想等,则抛出并发异常
-
modCount是当前集合的版本号,每次修改(增、删)集合都会加1
-
expectedModCount是当前迭代器的版本号,在迭代器实例化时初始化为modCount。
-
说明,迭代器创建后,迭代器的版本号与集合的版本号相等,之后集合做出修改,集合版本号会加1,与迭代器版本号不相等,迭代器就抛出了并发异常
-
在Iterator的remove()中同步了expectedModCount的值,所以下次再调用next()的时候,检查不会抛出异常。
Java设计迭代器并发异常的原因
使用该机制的主要目的是为了实现ArrayList中的快速失败机制
(fail-fast),在Java集合中较大一部分集合是存在快速失败机制的。
快速失败机制产生的条件:当多个线程对Collection进行操作时,若其中某一个线程通过Iterator遍历集合时,该集合的内容被其他线程所改变,则会抛出ConcurrentModificationException异常。
所以要保证在使用Iterator遍历集合的时候不出错误
,就应该保证在遍历集合的过程中不会对集合产生结构上的修改。
迭代器提供了删除数据的方法,既保证了功能的满足,又避免了可能出现的误操作现象(别的线程通过集合的方法删除元素,大概率是误操作)
(4)foreach遍历集合
foreach指的就是增强for循环。foreach是一种语法糖,并非方法或关键字,是Java5之后的特性。
实现Iterable接口的对象,才可以成为Foreach语句的目标。
Iterable接口包含一个能产生Iterator对象的方法,Iterable被foreach用来在序列中移动。
foreach要依赖于Iterable接口返回的Iterator对象,所以从本质上来讲,Foreach其实就是在使用迭代器
注意:如果要想使自己自定义的类可以采用foreach语法糖就必须实现Iterable接口。
Java中,数组也可以使用增强for的形式,但本质就不是调用迭代器了,而是转换为普通的遍历
示例 使用foreach遍历集合
List<String> list = new ArrayList<String>();
list.add("张三1");
list.add("张三2");
list.add("张三3");
list.add("张三4");
for (String string : list) {
System.out.println(string);
}
使用foreach的优势:代码简洁,不易出错,不用担心数组越界,会完完整整遍历一遍,省心
局限性:无法使用索引
,需要用到索引时还是需要for i循环
注:foreach通常与泛型结合使用
(5)Lambda表达式遍历集合
本质上也是Foreach,只是简化了写法。
示例
public static void main(String[] args) {
//创建list对象
List<String> list = new ArrayList<String>();
//添加数据
list.add("张三");
list.add("李四");
list.add("王二");
list.add("麻子");
//使用Lambda表达式遍历集合元素
list.forEach((String name)->System.out.println(name));
(6)选择合理的遍历方式
for循环与迭代器的对比
- ArrayList的随机访问比较快,而for循环中使用的get()方法,采用的即是随机访问的方法,因此在ArrayList里for循环快。
- LinkedList则是顺序访问比较快,Iterator中的next()方法采用的是顺序访问方法,因此在LinkedList里使用Iterator较快。
各有优势,主要根据集合的数据结构不同去选择
集合相关的工具类
1、Collections工具类
概述
针对集合操作的工具类
常用方法
方法名 | 说明 |
---|---|
public static void sort(List list) | 将指定列表按升序排序 |
public static int binarySearch(List<?> list,T key) | 二分查找 |
public static T max(Collection<?> coll) | 获取最大值 |
public static void reverse(List<?> list) | 反转指定列表中的元素顺序 |
public static void shuffle(List<?> list) | 随机排列指定的列表 |
2、comparable和comparator的区别
comparable出自java.lang包,只有一个方法:
comparator出自java.util包,有很多方法,其中一个和上面的很类似:
它们的用途是不一样的:
- compareTo:用于在实现了Comparable接口的类中,重写compareTo()方法,定义这个对象的排序规则(比较的是传入的对象和this对象)
- compare:在给集合排序时,传入Collections.sort()方法,重写作为参数,自定义比较方法。(比较的是传入的两个参数)
集合常见问题
1、快速失败机制
它的目的是,如果一个线程在遍历集合,而其他线程修改了集合,那么遍历集合的线程能够感知到。
1、快速失败机制的工作方式
迭代器保存了一个cursor,作为当前遍历的下标。还有一个lastRet,表示最后一个元素的索引值。
另外迭代器保存了一个expectedModCount,这个东西是在创建迭代器的时候,获取到的集合的版本号,在遍历过程中不会修改,除了remove()方法。
快速失败的原因就是,在每次调用next()方法的最开始,会调用这个checkForComodification()方法来检查集合是否发生过修改:
final void checkForComodification() {
if (ArrayList.this.modCount != this.expectedModCount) {
throw new ConcurrentModificationException();
}
}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
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];
}
迭代器遍历的过程
- hasNext()会判断是否存在下一个元素,具体方式就是判断当前游标是否等于集合容量。
- next()是获取下一个元素的值,在迭代过程中,每次都会返回目前cursor的值,然后cursor自增1。直到不存在下一个元素,则迭代结束。
迭代器遍历时删除的一个坑
平时在迭代器中调用集合的remove()方法删除元素,会抛出异常,原因就是:
- 本次操作之后,集合的modCount发生了变化
- 遍历时会调用迭代器的hasNext(),发现仍然有下一个元素,就会去调用迭代器的next()
- 迭代器的next()的最开头,会判断当前集合的modCount和迭代器维护的expectedModCount是否相同,此时不同,就会抛出异常。
而如果删除的是集合的倒数第二个元素,那就不会抛出异常,原因是:
- 比如集合为1,2,3,4,5,要删除4
- 遍历到4时,迭代器的游标cursor为4,执行了删除4的逻辑,集合的modCount同样会自增
- 但是,
此时集合的容量变为4,调用hasNext(),发现游标cursor等于了集合容量,此时就会退出循环,不会执行下一次next()
。
总结:抛出异常的原因并不是删除这个操作本身,而是删除后调用了next(),检查集合的版本号和迭代器的版本号不符,此时才会抛出异常。
2、安全失败机制
fail-safe,采用安全失败机制的集合容器,不是直接在集合上遍历的,而是先复制原先的集合内容,在拷贝的集合上进行遍历。
所以,在遍历过程中对原集合做的修改,不能被迭代器检测到
,所以不会抛出ConcurrentModificationException异常。
java.util.concurrent包下的集合都是安全失败的,所以支持并发修改
3、使用isEmpty()
对一个集合判空,有两种方式:isEmpty()、size() == 0
- isEmpty()的时间复杂度是O(1)
- 大多数集合,size()的时间复杂度也是O(1),但是java.util.concurrent下的某些集合不是O(1)
4、集合转数组
使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。
5、数组转集合
使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法
因为Arrays.asList() 方法返回的并不是ArrayList ,而是 java.util.Arrays 的一个内部类
这个内部类并没有实现集合的修改方法,这些方法默认是直接抛异常的。
如果想要获得一个ArrayList,就这样做:
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))