Java8 lambda 增强遍历接口 java.lang.Iterable.forEach 完全详解

在 Java 7 里,我们遍历集合的时候一般用的是 Iterable 接口 for each 的方式来操作,较之 fori 循环遍历确实已经提高了很大的效率和可读性。

不过应该有不少新手,在迭代使用不当的时候,遇到过 ConcurrentModificationException 异常。

很多人也会在遍历 Map 的时候一筹莫展,计较于哪种方式效率比较高,在哪种情况下使用哪一种迭代方式。或者有人根本就记不住如何迭代 HashMap,每次都要借助搜索引擎。

不过自从 Java 8 进入 lambdadefault 时代,这些烦恼将不会存在。

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);
    }
}

java_lang_Iterable_default_forEach.jpg

  • 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);
    }
}

java_util_Map_default_forEach.jpg

现在看一下 HashMapforEach 遍历是何等方便。

构造数据:

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
如果觉得这对你有用,请随意赞赏,给与作者支持
评论 0
最新评论