最近在接触公司的新项目,在编写代码时发现了一些不仅好用而且非常方便的方法,例如本文将要介绍的 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
*/
|
还有很多的其他方法,例如max
、count
、anyMatch
、allMatch
、noneMatch
、findAny
等。
处理流程
前面说到,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 的其他内容,可以根据下方的链接进行学习。
参考