Java8 lambda 增强遍历接口 java.lang.Iterable.forEach 完全详解
在 Java 7 里,我们遍历集合的时候一般用的是 Iterable
接口 for each 的方式来操作,较之 fori
循环遍历确实已经提高了很大的效率和可读性。
不过应该有不少新手,在迭代使用不当的时候,遇到过 ConcurrentModificationException
异常。
很多人也会在遍历 Map
的时候一筹莫展,计较于哪种方式效率比较高,在哪种情况下使用哪一种迭代方式。或者有人根本就记不住如何迭代 HashMap
,每次都要借助搜索引擎。
不过自从 Java 8 进入 lambda
和 default
时代,这些烦恼将不会存在。
java.lang.Iterable.forEach 和 default
在 Java 8 中,接口中的方法可以被实现。
接口中被实现的方法叫做 default
方法,用关键字 default
作为修饰符来标识。
当一个类实现一个接口的时候,它可以实现已经在接口中被实现过的方法,但这不是必须的。这个类会继承 default 方法。这就是为什么当接口发生改变的时候,实现类不需要做改动的原因。
forEach
方法是 Java 8 中在集合父接口 java.lang.Iterable
中新增的一个 default 实现方法:
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
- forEach方法接受一个在 Java 8 中新增的 java.util.function.Consumer 的消费行为或者称之为动作(Consumer action )类型;
- 然后将集合中的每个元素作为消费行为的 accept 方法的参数执行;
- 直到每个元素都处理完毕或者抛出异常即终止行为;
- 除非指定了消费行为 action 的实现,否则默认情况下是按迭代里面的元素顺序依次处理。
List 遍历对比
我们通过看一下 java.util.List 的遍历案例,来直观感受一下 Iterable 迭代
迭代和 forEach
遍历的对比。
首先创建一个 ArrayList
,存放几个姓名字符串。
List<String> names = new ArrayList<>();
names.add("刘德华");
names.add("张学友");
names.add("黎明");
names.add("郭富城");
Iterable 迭代
简单的把姓名字符串打印出来:
@Test
public void testList()
{
List<String> names = new ArrayList<>();
names.add("刘德华");
names.add("张学友");
names.add("黎明");
names.add("郭富城");
for (String name : names)
{
System.out.println(name);
}
}
执行结果:
刘德华
张学友
黎明
郭富城
forEach 遍历
仍然是打印出姓名字符,执行结果是相同的,看一下代码对比:
@Test
public void testList()
{
List<String> names = new ArrayList<>();
names.add("刘德华");
names.add("张学友");
names.add("黎明");
names.add("郭富城");
//names.forEach(name -> System.out.println(name));
// 或者使用 lambda 表达式的简化版本双冒号表达式
// 调用 out 对象的 println 方法
names.forEach(System.out::println);
}
遍历筛选
假如现在需要把名字字符串为三个汉字的打印出来,又是什么样子的呢?
Iterable 迭代方式:
for (String name : names)
{
if (null != name && name.length() == 3)
{
System.out.println("-- " + name);
}
}
forEach 遍历方式:
names.forEach(name -> {
if (null != name && name.length() == 3)
{
System.out.println("-- " + name);
}
});
或者这样:
// stream and filter
names.stream()
.filter(name -> name != null && name.length() == 3)
.forEach(System.out::println);
Map 遍历对比
Map
接口同样也定义了 forEach
的 default 方法。
基本上,只要是需要或者可以迭代的类,都定义了相似的 forEach
default 方法。
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
现在看一下 HashMap
的 forEach
遍历是何等方便。
构造数据:
Map<String, Integer> items = new HashMap<>();
items.put("A", 10);
items.put("B", 20);
items.put("C", 30);
items.put("D", 40);
items.put("E", 50);
items.put("F", 60);
需求是打印出哈希元素的键值对,如果键为 D
, 则多打印出一行 find it!
。
Map Iterable 迭代
代码:
@Test
public void testMap()
{
Map<String, Integer> items = new HashMap<>();
items.put("A", 10);
items.put("B", 20);
items.put("C", 30);
items.put("D", 40);
items.put("E", 50);
items.put("F", 60);
for (Map.Entry<String, Integer> entry : items.entrySet()) {
System.out.println("Item : " + entry.getKey() + " Count : " + entry.getValue());
if("D".equals(entry.getKey())){
System.out.println("find it!");
}
}
}
似曾相识,多么可恶的复杂性,我只是想遍历啊,太复杂了!
Map forEach 遍历
当年,每次在遍历 Map
的时候,都会想,为什么不能像 List
遍历那样,只提供 key value 这样的变量就可以迭代获取到元素呢?现在可以了!
@Test
public void testMap()
{
Map<String, Integer> items = new HashMap<>();
items.put("A", 10);
items.put("B", 20);
items.put("C", 30);
items.put("D", 40);
items.put("E", 50);
items.put("F", 60);
items.forEach((k, v) -> {
System.out.println("Item : " + k + " Count : " + v);
if("D".equals(k)){
System.out.println("find it!");
}
});
}
同样的输出,不一样的简洁。
Item : A Count : 10
Item : B Count : 20
Item : C Count : 30
Item : D Count : 40
find it!
Item : E Count : 50
Item : F Count : 60
数组遍历
数组遍历的情况虽然没有那么幸运,得到直接支持,但是我们可以通过转换为 List
的方式间接使用。
String[] data = { "刘德华", "张学友", "黎明", "郭富城", "代码饭" };
List<String> names = Arrays.asList(data);
names.forEach(System.out::println);
自定义消费行为 Consumer
- 首先,需要实现
java.util.function.Consumer
接口; - 再次,实现其
accept
方法
class MyConsumer implements Consumer<Object> {
@Override
public void accept(Object t) {
System.out.println("forEach Consumer println:" + t);
}
}
测试,
@Test
public void testForEachConsumer()
{
List<Integer> myList = new ArrayList<Integer>();
for(int i=0; i<3; i++) myList.add(i);
MyConsumer myConsumer = new MyConsumer();
myList.forEach(myConsumer);
}
输出:
forEach Consumer println:0
forEach Consumer println:1
forEach Consumer println:2