最近在接触公司的新项目,在编写代码时发现了一些不仅好用而且非常方便的方法,例如本文将要介绍的 JDK1.8 特性 stream 的使用,下面将对其进行介绍和总结。

引入

JDK1.8 中的 stream 支持对元素流进行函数式操作,通过这种方式可以很方便的处理集合(如 List、Map),进而简化代码。下面通过一个例子进行说明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static void main(String[] args) {
    Person person1 = new Person(1, "ZhangSan", new Address("Beijing", "ChaoYang", "ChangYing Street"));
    Person person2 = new Person(2, "LiSi", new Address("Shandong", "Linyi", "Lanshan Street"));

    List<Person> list = new ArrayList<>();
    list.add(person1);
    list.add(person2);

    System.out.println(list);
}

/*
[Person(id=1, name=ZhangSan, address=Address(province=Beijing, city=ChaoYang, street=ChangYing Street)), Person(id=2, name=LiSi, address=Address(province=Shandong, city=Linyi, street=Lanshan Street))]
*/

在上述的代码中,首先创建了两个 Person 对象,在每个对象中又创建了 Address 对象,然后将这两个 Person 对象都添加到 List 中。

现在,我想获取每个 Person 中的 id 属性,并将这些 id 放到 List 中。在以前的方式中,可以通过遍历 list 中的 Person 对象逐个获取到对应 id 后,再将其逐个添加到新的 List 中。然而,这种方式相对较为繁琐。可以使用如下的方式,一行代码就可以搞定:

1
2
3
4
5
6
7
8
List<Integer> ids = list.stream()
                        .map(Person::getId)
                        .collect(Collectors.toList());
System.out.println(ids);

/*
[1, 2]
*/

可以看到,通过以上的方式,将每个 Person 对象中的 id 都存放到了一个 List 中,最终将其输出即可。

再看一个例子,例如我想得到一个关于 id 和其对应 Person 映射关系的 Map,可以使用如下方式:

1
2
3
4
5
6
7
8
9
Map<Integer, Person> idAndPersonMap = list.stream()
                                          .collect(Collectors.toMap(Person::getId, 
                                                                    Function.identity(), 
                                                                    (oldValue, newValue) -> newValue));
System.out.println(idAndPersonMap);

/*
{1=Person(id=1, name=ZhangSan, address=Address(province=Beijing, city=ChaoYang, street=ChangYing Street)), 2=Person(id=2, name=LiSi, address=Address(province=Shandong, city=Linyi, street=Lanshan Street))}
*/

通过以上的例子,在利用 JDK1.8 的特性的前提下,不仅可以简化代码,而且还可以使程序的整体结构看起来更加清晰、明了。下面将具体的介绍它们该如何使用。

介绍

以下面代码为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
List<String> list = Arrays.asList("a1", "a2", "b1", "c2", "c1");

list.stream()
    .filter(s -> s.startsWith("c"))
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);

/*
C1
C2
*/

对于 stream 的使用,可以将其分为「中间操作」和「终端操作」的概念。

「中间操作」是对数据进行处理的相关操作,例如上面的filter()map()sorted(),其会再次返回一个流,以供下个「中间操作」进行数据处理;

「终端操作」是结束当前流的一个动作,一般会返回非数据流或 void,例如上面的forEach()。对上面代码的解释如下:

  • stream:
    • 创建一个流。
  • 「中间操作」:
    • filter():返回一个由这个流中符合给定谓词(predicate)的元素组成的流。这里的谓词,指的是应用于每个元素,已确定它是否应该被包含,其实就相当于对元素的过滤。
      • 在 List 中找出(过滤)满足以前缀为c开始的字符串。这里面的s就是一个字符串变量,用于描述 List 中的每个字符串,也可以用其他字符表示,例如str等。
    • map():返回一个由对这个流的元素应用给定函数的结果组成的流。
      • 在上面的示例中,给定函数指的就是 Sting 的toUpperCase方法,将字符串转换成大写。
    • sorted():返回一个由当前流所组成的流,按照自然顺序进行排序。
  • 「终端操作」:
    • forEach():对当前流的每个元素执行一个动作。需要注意的是,该函数的返回值是void

使用

在创建流的时候,除了上述方法之外,还可以直接通过Stream.of()进行创建,如下所示:

1
2
3
4
5
6
7
Stream.of("a3", "a1", "a2")
        .findFirst() // 获取第一个元素
        .ifPresent(System.out::println); // 如果存在,则将其输出
    
/*
a3
*/

可以对流中元素进行计算,如下所示:

1
2
3
4
5
6
7
8
9
Arrays.stream(new int[]{1, 2, 3})
        .map(i -> 2 * i)
        .forEach(System.out::println);

/*
2
4
6
*/

求计算结果的平均值:

1
2
3
4
5
6
7
8
Arrays.stream(new int[]{1, 2, 3})
        .map(i -> 2 * i)
        .average()
        .ifPresent(System.out::println);

/*
4.0
*/

将流中的对象数据类型转换为基本数据类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Stream.of("a1", "b2", "c3")
        .map(s -> s.substring(1))
        .mapToInt(Integer::parseInt)
        .forEach(System.out::println);

/*
1
2
3
*/

将浮点类型转换成正数类型,然后再转换成对象流:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Stream.of(1.0, 2.0, 3.11)
        .mapToInt(Double::intValue)
        .mapToObj(i -> "a" + i)
        .forEach(System.out::println);
    
/*
a1
a2
a3
*/

可以使用collect将流中的元素作为集合转换成其他Collection的子类,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 List<Integer> list = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
list.stream().collect(Collectors.toSet()).forEach(System.out::println);

/*
1
2
3
4
5
*/

还有很多的其他方法,例如maxcountanyMatchallMatchnoneMatchfindAny等。

处理流程

前面说到,Stream 在处理的时候包括「中间操作」和「终端操作」两部分。如果不包含「终端操作」呢?看如下代码:

1
2
3
4
5
6
7
8
Stream.of("a1", "b2", "c3", "d4")
        .filter(s -> {
            System.out.println("filer: " + s);
            return true;
        });

/*
*/

其输出结果为空,并且 IDEA 会提示Result of 'Stream.filter()' is ignored。也就是说,只有「终端操作」存在时,「中间操作」才会执行。如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Stream.of("a1", "b2", "c3", "d4")
        .filter(s -> {
            System.out.println("filer: " + s);
            return true;
        })
        .forEach(str -> System.out.println("forEach: " + str));

/*
filer: a1
forEach: a1
filer: b2
forEach: b2
filer: c3
forEach: c3
filer: d4
forEach: d4
*/

通过结果可以看出,先执行一次filter(),其次再执行一次forEach(),然后依次循环执行。也就是在处理完第一元素之后,才会去处理第二个元素。

这么做的原因是出于效率方面的考虑,关于这点,通过下面的代码进行说明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Stream.of("a1", "b2", "c3", "d4")
        .map(s -> {
            System.out.println("map: " + s);
            return s.toUpperCase();
        })
        .anyMatch(s -> {
            System.out.println("anyMatch: " + s);
            return s.startsWith("B");
        });
    
/*
map: a1
anyMatch: A1
map: b2
anyMatch: B2
*/

通过输出结果可以看到,「终端操作」anyMatch()表示找到任何一个以B为前缀的字符串就停止,并返回true。首先在遍历到a1时不满足,则开始匹配b2,由于b2被转换成了B2,因此anyMatch()条件满足之后,便返回true于是循环就停止了

通过这种方式,可以减少map()的执行次数,进而提高了效率。

执行顺序

通过上面的例子可以看出,「中间操作」采用链式调用垂直执行的方式依次执行。再看如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Stream.of("d2", "a2", "b1", "b3", "c")
        .map(s -> {
            System.out.println("map: " + s);
            return s.toUpperCase();
        })
        .filter(s -> {
            System.out.println("filter: " + s);
            return s.startsWith("A");
        })
        .forEach(s -> System.out.println("forEach: " + s));

/*
map: d2
filter: D2
map: a2
filter: A2
forEach: A2
map: b1
filter: B1
map: b3
filter: B3
map: c
filter: C
*/

可以看到,执行结果很明确,只有元素a2执行了forEach()。但有一点糟糕的是,map()方法执行的次数与元素的个数相同,显然做了很多无意义的map()操作。如果将map()filter()方法的顺序调整一下呢?如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Stream.of("d2", "a2", "b1", "b3", "c")
        .filter(s -> {
            System.out.println("filter: " + s);
            return s.startsWith("a");
        })
        .map(s -> {
            System.out.println("map: " + s);
            return s.toUpperCase();
        })
        .forEach(s -> {
            System.out.println("forEach: " + s);
        });

/*
filter: d2
filter: a2
map: a2
forEach: A2
filter: b1
filter: b3
filter: c
*/

通故结果可以看到,仅有map()forEach执行了一次,性能得到了提升。

以上所提方法基本能够应用于日常的开发中,对于 Stream 的其他内容,可以根据下方的链接进行学习。

参考