使用Lambda表达式、foreach循环、Predicate操作、Stream操作遍历Collection集合

Lambda表达式遍历Collection集合 

Java8 为 Iterable 接口新增了一个 forEach(Consumer action) 默认方法,该方法所需参数的类型是一个函数式接口,而 Iterable 接口是 Collection 接口的父接口,因此 Collection 集合也可直接调用该方法。

当程序调用 Iterable 的 forEach(Consumer action) 遍历集合元素时,程序会依次将集合元素传给 Consumer 的 accept(T t) 方法(该接口中唯一的抽象方法)。正因为 Consumer 是函数式接口,因此可以使用 Lambda 表达式来遍历集合元素。

如下程序示范了使用 Lambda 表达式来遍历集合元素。

    public class CollectionEach {
        public static void main(String[] args) {
            // 创建一个集合
            Collection objs = new HashSet();
            objs.add("C语言中文网Java教程");
            objs.add("C语言中文网C语言教程");
            objs.add("C语言中文网C++教程");
            // 调用forEach()方法遍历集合
            objs.forEach(obj -> System.out.println("迭代集合元素:" + obj));
        }
    }

输出结果为:

迭代集合元素:C语言中文网C++教程
迭代集合元素:C语言中文网C语言教程
迭代集合元素:C语言中文网Java教程

上面程序中粗体字代码调用了 Iterable 的 forEach() 默认方法来遍历集合元素,传给该方法的参数是一个 Lambda 表达式,该 Lambda 表达式的目标类型是 Comsumer。forEach() 方法会自动将集合元素逐个地传给 Lambda 表达式的形参,这样 Lambda 表达式的代码体即可遍历到集合元素了。

使用foreach循环遍历Collection集合

除了使用 Iterator 接口迭代访问 Collection 集合里的元素方法之外,我们还可以使用 Java5 提供的 foreach 循环迭代访问集合元素,而且更加便捷。如下程序示范了使用 foreach 循环来迭代访问集合元素。

public class ForeachTest {
    public static void main(String[] args) {
        // 创建一个集合
        Collection objs = new HashSet();
        objs.add("C语言中文网Java教程");
        objs.add("C语言中文网C语言教程");
        objs.add("C语言中文网C++教程");
        for (Object obj : objs) {
            // 此处的obj变量也不是集合元素本身
            String obj1 = (String) obj;
            System.out.println(obj1);
            if (obj1.equals("C语言中文网Java教程")) {
                // 下面代码会引发 ConcurrentModificationException 异常
                objs.remove(obj);
            }
        }
        System.out.println(objs);
    }
}

输出结果为:

C语言中文网C++教程
C语言中文网C语言教程
C语言中文网Java教程
[C语言中文网C++教程, C语言中文网C语言教程]

上面代码使用 foreach 循环来迭代访问 Collection 集合里的元素更加简洁,这正是 JDK 1.5 的 foreach 循环带来的优势。与使用 Iterator 接口迭代访问集合元素类似的是,foreach 循环中的迭代变量也不是集合元素本身,系统只是依次把集合元素的值赋给迭代变量,因此在 foreach 循环中修改迭代变量的值也没有任何实际意义。

同样,当使用 foreach 循环迭代访问集合元素时,该集合也不能被改变,否则将引发 ConcurrentModificationException 异常。所以上面程序中第 14 行代码处将引发该异常。

使用Java 8新增的Predicate操作Collection集合

Java 8 起为 Collection 集合新增了一个 removeIf(Predicate filter) 方法,该方法将会批量删除符合 filter 条件的所有元素。该方法需要一个 Predicate 对象作为参数,Predicate 也是函数式接口,因此可使用 Lambda 表达式作为参数。

如下程序示范了使用 Predicate 来过滤集合。

public class ForeachTest {
    public static void main(String[] args) {
        // 创建一个集合
        Collection objs = new HashSet();
        objs.add(new String("C语言中文网Java教程"));
        objs.add(new String("C语言中文网C++教程"));
        objs.add(new String("C语言中文网C语言教程"));
        objs.add(new String("C语言中文网Python教程"));
        objs.add(new String("C语言中文网Go教程"));
        // 使用Lambda表达式(目标类型是Predicate)过滤集合
        objs.removeIf(ele -> ((String) ele).length() < 12);
        System.out.println(objs);
    }
}

上面程序中第 11 行代码调用了 Collection 集合的 removeIf() 方法批量删除集合中符合条件的元素,程序传入一个 Lambda 表达式作为过滤条件。所有长度小于 12 的字符串元素都会被删除。编译、运行这段代码,可以看到如下输出:

[C语言中文网Java教程, C语言中文网Python教程]

使用 Predicate 可以充分简化集合的运算,假设依然有上面程序所示的 objs 集合,如果程序有如下三个统计需求:

  1. 统计集合中出现“C语言中文网”字符串的数量。
  2. 统计集合中出现“Java”字符串的数量。
  3. 统计集合中出现字符串长度大于 12 的数量。

此处只是一个假设,实际上还可能有更多的统计需求。如果采用传统的编程方式来完成这些需求,则需要执行三次循环,但采用 Predicate 只需要一个方法即可。下面代码示范了这种用法。

public class ForeachTest2 {
    public static void main(String[] args) {
        // 创建一个集合
        Collection objs = new HashSet();
        objs.add(new String("C语言中文网Java教程"));
        objs.add(new String("C语言中文网C++教程"));
        objs.add(new String("C语言中文网C语言教程"));
        objs.add(new String("C语言中文网Python教程"));
        objs.add(new String("C语言中文网Go教程"));
        // 统计集合中出现“C语言中文网”字符串的数量
        System.out.println(calAll(objs, ele -> ((String) ele).contains("C语言中文网")));
        // 统计集合中出现“Java”字符串的数量
        System.out.println(calAll(objs, ele -> ((String) ele).contains("Java")));
        // 统计集合中出现字符串长度大于 12 的数量
        System.out.println(calAll(objs, ele -> ((String) ele).length() > 12));
    }

    public static int calAll(Collection books, Predicate p) {
        int total = 0;
        for (Object obj : books) {
            // 使用Predicate的test()方法判断该对象是否满足Predicate指定的条件
            if (p.test(obj)) {
                total++;
            }
        }
        return total;
    }
}

输出结果为:

5 1 1

上面程序先定义了一个 calAll() 方法,它使用 Predicate 判断每个集合元素是否符合特定条件,条件将通过 Predicate 参数动态传入。从上面程序中第 11、13、15 行代码可以看到,程序传入了 3 个 Lambda 表达式,其目标类型都是 Predicate,这样 calAll() 方法就只会统计满足 Predicate 条件的图书。

 使用Java 8新增的Stream操作Collection集合

Java8 还新增了 Stream、IntStream、LongStream、DoubleStream 等流式 API,这些 API 代表多个支持串行和并行聚集操作的元素。上面 4 个接口中,Stream 是一个通用的流接口,而 IntStream、LongStream、 DoubleStream 则代表元素类型为 int、long、double 的流。

Java 8 还为上面每个流式 API 提供了对应的 Builder,例如 Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builder,开发者可以通过这些 Builder 来创建对应的流。

独立使用 Stream 的步骤如下:

  1. 使用 Stream 或 XxxStream 的 builder() 类方法创建该 Stream 对应的 Builder。
  2. 重复调用 Builder 的 add() 方法向该流中添加多个元素。
  3. 调用 Builder 的 build() 方法获取对应的 Stream。
  4. 调用 Stream 的聚集方法。

在上面 4 个步骤中,第 4 步可以根据具体需求来调用不同的方法,Stream 提供了大量的聚集方法供用户调用,具体可参考 Stream 或 XxxStream 的 API 文档。对于大部分聚集方法而言,每个 Stream 只能执行一次。例如如下程序。

public class IntStreamTest {
    public static void main(String[] args) {
        IntStream is = IntStream.builder().add(20).add(13).add(-2).add(18).build();
        // 下面调用聚集方法的代码每次只能执行一行
        System.out.println("is 所有元素的最大值:" + is.max().getAsInt());
        System.out.println("is 所有元素的最小值:" + is.min().getAsInt());
        System.out.println("is 所有元素的总和:" + is.sum());
        System.out.println("is 所有元素的总数:" + is.count());
        System.out.println("is 所有元素的平均值:" + is.average());
        System.out.println("is所有元素的平方是否都大于20: " + is.allMatch(ele -> ele * ele > 20));
        System.out.println("is是否包含任何元素的平方大于20 : " + is.anyMatch(ele -> ele * ele > 20));
        // 将is映射成一个新Stream,新Stream的每个元素是原Stream元素的2倍+1
        IntStream newIs = is.map(ele -> ele * 2 + 1);
        // 使用方法引用的方式来遍历集合元素
        newIs.forEach(System.out::println); // 输岀 41 27 -3 37
    }
}

上面程序先创建了一个 IntStream,接下来分别多次调用 IntStream 的聚集方法执行操作,这样即可获取该流的相关信息。注意:上面 5~13 行代码每次只能执行一行,因此需要把其他代码注释掉。

Stream 提供了大量的方法进行聚集操作,这些方法既可以是“中间的”(intermediate),也可以是 "末端的"(terminal)。

  • 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的 map() 方法就是中间方法。中间方法的返回值是另外一个流。
  • 末端方法:末端方法是对流的最终操作。当对某个 Stream 执行末端方法后,该流将会被“消耗”且不再可用。上面程序中的 sum()、count()、average() 等方法都是末端方法。

除此之外,关于流的方法还有如下两个特征。

  • 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
  • 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。

下面简单介绍一下 Stream 常用的中间方法。

方法说明
filter(Predicate predicate)过滤 Stream 中所有不符合 predicate 的元素
mapToXxx(ToXxxFunction mapper)使用 ToXxxFunction 对流中的元素执行一对一的转换,该方法返回的新流中包含了 ToXxxFunction 转换生成的所有元素。
peek(Consumer action)依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法主要用于调试。
distinct()该方法用于排序流中所有重复的元素(判断元素重复的标准是使用 equals() 比较返回 true)。这是一个有状态的方法。
sorted()该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。
limit(long maxSize)该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。

下面简单介绍一下 Stream 常用的末端方法。

方法说明
forEach(Consumer action)遍历流中所有元素,对每个元素执行action
toArray()将流中所有元素转换为一个数组
reduce()该方法有三个重载的版本,都用于通过某种操作来合并流中的元素
min()返回流中所有元素的最小值
max()返回流中所有元素的最大值
count()返回流中所有元素的数量
anyMatch(Predicate predicate)判断流中是否至少包含一个元素符合 Predicate 条件。
allMatch(Predicate predicate)判断流中是否每个元素都符合 Predicate 条件
noneMatch(Predicate predicate)判断流中是否所有元素都不符合 Predicate 条件
findFirst()返回流中的第一个元素
findAny()返回流中的任意一个元素

除此之外,Java 8 允许使用流式 API 来操作集合,Collection 接口提供了一个 stream() 默认方法,该方法可返回该集合对应的流,接下来即可通过流式 API 来操作集合元素。由于 Stream 可以对集合元素进行整体的聚集操作,因此 Stream 极大地丰富了集合的功能。

例如,对于Predicate操作collection集合一节的示例程序需要额外定义一个 calAll() 方法来遍历集合元素,然后依次对每个集合元素进行判断,这样做太麻烦了。如果使用 Stream 可以直接对集合中所有的元素进行批量操作。下面使用 Stream 来改写这个程序。

public class CollectionStream {
    public static void main(String[] args) {
        // 创建一个集合
        Collection objs = new HashSet();
        objs.add(new String("C语言中文网Java教程"));
        objs.add(new String("C语言中文网C++教程"));
        objs.add(new String("C语言中文网C语言教程"));
        objs.add(new String("C语言中文网Python教程"));
        objs.add(new String("C语言中文网Go教程"));
        // 统计集合中出现“C语言中文网”字符串的数量
        System.out.println(objs.stream().filter(ele -> ((String) ele).contains("C语言中文网")).count()); // 输出 5
        // 统计集合中出现“Java”字符串的数量
        System.out.println(objs.stream().filter(ele -> ((String) ele).contains("Java")).count()); // 输出 1
        // 统计集合中出现字符串长度大于 12 的数量
        System.out.println(objs.stream().filter(ele -> ((String) ele).length() > 12).count()); // 输出 1
        // 先调用Collection对象的stream ()方法将集合转换为Stream
        // 再调用Stream的mapToInt()方法获取原有的Stream对应的IntStream
        objs.stream().mapToInt(ele -> ((String) ele).length())
                // 调用forEach()方法遍历IntStream中每个元素
                .forEach(System.out::println);// 输出 11 11 12 10 14
    }
}

输出结果为:

5    1    1    11    11    12    10    14

从上面代码第 11~20 行可以看出,程序只要调用 Collection 的 stream() 方法即可返回该集合对应的 Stream,接下来就可通过 Stream 提供的方法对所有集合元素进行处理,这样大大地简化了集合编程的代码,这也是 Stream 编程带来的优势。

上面程序中第 18 行代码先调用 Collection 对象的 stream() 方法将集合转换为 Stream 对象,然后调用 Stream 对象的 mapToInt() 方法将其转换为 IntStream 这个 mapToInt。方法就是一个中间方法,因此程序可继续调用 IntStream 的 forEach() 方法来遍历流中的元素。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值