java8 实战读书笔记:初识 Stream、流的基本操作,nginx 架构原理
版本 3:List<String> dishNames = menu.stream().map(Dish::getName).collect(Collectors.toList());
文章的后续部分尽量使用最简洁的 lambda 表达式。
我们来看一下 Stream 关于 map 方法的声明:
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
接受一个函数 Function,其函数声明为:T -> R,接收一个 T 类型的对象,返回一个 R 类型的对象。
当然,java 为了高效的处理基础数据类型(避免装箱、拆箱带来性能损耗)也定义了如下方法:
IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)
思考题:对于字符数值[“Hello”,“World”] ,输出字符序列,并且去重。
第一次尝试:
public static void test_flat_map() {
String[] strArr = new String[] {"hello", "world"};
List<String> strList = Arrays.asList(strArr);
strList.stream().map( s -> s.split(""))
.distinct().forEach(System.out::println);
}
输出结果:
为什么会返回两个 String[]元素呢?因为 map(s -> s.split()) 此时返回的流为 Stream<String[]>,那我们是不是可以继续对该 Steam[String[]],把 String[]转换为字符流,其代码如下:
public static void test_flat_map() {
String[] strArr = new String[] {"hello", "world"};
List<String> strList = Arrays.asList(strArr);
strList.stream().map( s -> s.split(""))
.map(Arrays::stream)
.distinct().forEach(System.out::println);
}
其返回结果:
还是不符合预期,其实原因也很好理解,再次经过 map(Arrays:stream)后,返回的结果为 Stream<Stream< String>>,即包含两个元素,每一个元素为一个字符流,可以通过如下代码验证:
public static void test_flat_map() {
String[] strArr = new String[] {"hello", "world"};
List<String> strList = Arrays.asList(strArr);
strList.stream().map( s -> s.split(""))
.map(Arrays::stream)
.forEach( (Stream<String> s) -> {
System.out.println("\n --start---");
s.forEach(a -> System.out.print(a + " "));
System.out.println("\n --end---");
} );
}
综合上述分析,之所以不符合预期,主要是原数组中的两个字符,经过 map 后返回的是两个独立的流,那有什么方法将这两个流合并成一个流,然后再进行 disinic 去重呢?
答案当然是可以的,flatMap 方法闪亮登场:先看代码和显示结果:
public static void test_flat_map() {
String[] strArr = new String[] {"hello", "world"};
List<String> strList = Arrays.asList(strArr);
strList.stream().map( s -> s.split(""))
.flatMap(Arrays::stream)
.distinct().forEach( a -> System.out.print(a +" "));
}
其输出结果:
符合预期。一言以蔽之,flatMap 可以把两个流合并成一个流进行操作。
2.3 查找和匹配
Stream API 提供了 allMatch、anyMatch、noneMatch、findFirst 和 findAny 方法来实现对流中数据的匹配与查找。
2.3.1 allMatch
我们先看一下该方法的声明:
boolean allMatch(Predicate<? super T> predicate);
接收一个谓词函数(T->boolean),返回一个 boolean 值,是一个终端操作,用于判断流中的所有元素是否与 Predicate 相匹配,只要其中一个元素不复合,该表达式将返回 false。
示例如下:例如存在这样一个 List a,其中元素为 1,2,4,6,8。判断流中的元素是否都是偶数。
boolean result = a.stream().allMatch( a -> a % 2 == 0 ); // 将返回 false。
2.3.2 anyMatch
该方法的函数声明如下:
boolean anyMatch(Predicate<? super T> predicate)
同样接收一个谓词 Predicate( T -> boolean ),表示只要流中的元素至少一个匹配谓词,即返回真。
示例如下:例如存在这样一个 List a,其中元素为 1,2,4,6,8。判断流中的元素是否包含偶数。
boolean result = a.stream().anyMatch( a -> a % 2 == 0 ); // 将返回 true。
2.3.3 noneMatch
该方法的函数声明如下:
boolean noneMatch(Predicate<? super T> predicate);
同样接收一个谓词 Predicate( T -> boolean ),表示只要流中的元素全部不匹配谓词表达式,则返回 true。
示例如下:例如存在这样一个 List a,其中元素为 2,4,6,8。判断流中的所有元素都不式奇数。
boolean result = a.stream().noneMatch( a -> a % 2 == 1 ); // 将返回 true。
2.3.4 findFirst
查找流中的一个元素,其函数声明如下:
Optional<T> findFirst();
返回流中的一个元素。其返回值为 Optional,这是 jdk8 中引入的一个类,俗称值容器类,其主要左右是用来避免值空指针,一种更加优雅的方式来处理 null。该类的具体使用将在下一篇详细介绍。
public static void test_find_first(List<Dish> menu) {
Optional<Dish> dish = menu.stream().findFirst();
// 这个方法表示,Optional 中包含 Dish 对象,则执行里面的代码,否则什么事不干,是不是比判断是否为 null 更友好
dish.ifPresent(a -> System.out.println(a.getName()));
}
2.3.5 findAny
返回流中任意一个元素,其函数声明如下:
Optional<T> findAny();
2.4 reduce
reduce 归约,看过大数据的人用过会非常敏感,目前的 java8 的流操作是不是有点 map-reduce 的味道,归约,就是对流中所有的元素进行统计分析,归约成一个数值。
首先我们看一下 reduce 的函数说明:
T reduce(T identity, BinaryOperator<T> accumulator)
T identity:累积器的初始值。
BinaryOperator< T> accumulator:累积函数。BinaryOperator< T> extend BiFunction<T, U, R>。BinaryOperator 的函数式表示,接受两个 T 类型的入参,返回 T 类型的返回值。
Optional<T> reduce(BinaryOperator<T> accumulator);
可以理解为没有初始值的归约,如果流为空,则会返回空,故其返回值使用了 Optional 类来优雅处理 null 值。
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
首先,最后的返回值类型为 U。
U identity:累积函数的初始值。
BiFunction<U, ? super T, U> accumulator:累积器函数,对流中的元素使用该累积器进行归约,在具体执行时 accumulator.apply( identity, 第二个参数的类型不做限制 ),只要最终返回 U 即可。
BinaryOperator< U> combiner:组合器。对累积器的结果进行组合,因为归约 reduce,java 流计算内部使用了 fork-join 框架,会对流的中的元素使用并行累积,每个线程处理流中一部分数据,最后对结果进行组合,得出最终的值。
温馨提示:对流 API 的学习,一个最最重点的就是要掌握这些函数式编程接口,然后掌握如何使用 Lambda 表达式进行行为参数化(lambda 表达当成参数传入到函数中)。
接下来我们举例来展示如何使用 reduce。
示例 1:对集合中的元素求和
List<Integer> goodsNumber = Arrays.asList( 3, 5, 8, 4, 2, 13 );
java7 之前的示例:
int sum = 0;
for(Integer i : goodsNumber) {
sum += i;// sum = sum + i;
}
System.out.println("sum:" + sum);
求和运算符: c = a + b,也就是接受 2 个参数,返回一个值,并且这三个值的类型一致。
故我们可以使用 T reduce(T identity, BinaryOperator< T> accumulator)来实现我们的需求:
public static void test_reduce() {
List<Integer> goodsNumber = Arrays.asList( 3, 5, 8, 4, 2, 13 );
int sum =
goodsNumber.stream().reduce(0, (a,b) -> a + b);
//这里也可以写成这样:
// int sum = goodsNumber.stream().reduce(0, Integer::sum);
System.out.println(sum);
}
不知大家是否只读(a,b)这两个参数的来源,其实第一个参数为初始值 T identity,第二个参数为流中的元素。
那三个参数的 reduce 函数主要用在什么场景下呢?接下来还是用求和的例子来展示其使用场景。在 java 多线程编程模型中,引入了 fork-join 框架,就是对一个大的任务进行先拆解,用多线程分别并行执行,最终再两两进行合并,得出最终的结果。reduce 函数的第三个函数,就是组合这个动作,下面给出并行执行的流式处理示例代码如下:
public static void test_reduce_combiner() {
/** 初始化待操作的流 */
List<Integer> nums = new ArrayList<>();
int s = 0;
for(int i = 0; i < 200; i ++) {
nums.add(i);
s = s + i;
}
// 对流进行归并,求和,这里使用了流的并行执行版本 parallelStream,内部使用 Fork-Join 框架多线程并行执行,
// 关于流的内部高级特性,后续再进行深入,目前先以掌握其用法为主。
int sum2 = nums.parallelStream().reduce(0,Integer::sum, Integer::sum);
System.out.println("和为:" + sum2);
// 下面给出上述版本的 debug 版本。
// 累积器执行的次数
AtomicInteger accumulatorCount = new AtomicInteger(0);
// 组合器执行的次数(其实就是内部并行度)
AtomicInteger combinerCount = new AtomicInteger(0);
int sum = nums.parallelStream().reduce(0,(a,b) -> {
accumulatorCount.incrementAndGet();
return a + b;
}, (c,d) -> {
combinerCount.incrementAndGet();
return c+d;
});
System.out.println("accumulatorCount:" + accumulatorCount.get());
System.out.println("combinerCountCount:" + combinerCount.get());
}
从结果上可以看出,执行了 100 次累积动作,但只进行了 15 次合并。
流的基本操作就介绍到这里,在此总结一下,目前接触到的流操作:
1、filter
函数功能:过滤
操作类型:中间操作
返回类型:Stream
函数式接口:Predicate
函数描述符:T -> boolean
2、distinct
函数功能:去重
操作类型:中间操作
返回类型:Stream
3、skip
函数功能:跳过 n 个元素
操作类型:中间操作
返回类型:Stream
接受参数:long
4、limit
函数功能:截断流,值返回前 n 个元素的流
操作类型:中间操作
返回类型:Stream
接受参数:long
5、map
函数功能:映射
操作类型:中间操作
返回类型:Stream
函数式接口:Function<T,R>
函数描述符:T -> R
6、flatMap
函数功能:扁平化流,将多个流合并成一个流
操作类型:中间操作
返回类型:Stream
函数式接口:Function<T, Stream>
函数描述符:T -> Stream
7、sorted
函数功能:排序
操作类型:中间操作
返回类型:Stream
函数式接口:Comparator
函数描述符:(T,T) -> int
8、anyMatch
函数功能:流中任意一个匹配则返回 true
操作类型:终端操作
返回类型:boolean
函数式接口:Predicate
函数描述符:T -> boolean
9、allMatch
函数功能:流中全部元素匹配则返回 true
操作类型:终端操作
返回类型:boolean
函数式接口:Predicate
函数描述符:T -> boolean
10、 noneMatch
函数功能:流中所有元素都不匹配则返回 true
操作类型:终端操作
返回类型:boolean
函数式接口:Predicate
函数描述符:T -> boolean
11、findAny
函数功能:从流中任意返回一个元素
操作类型:终端操作
返回类型:Optional
12、findFirst
函数功能:返回流中第一个元素
操作类型:终端操作
返回类型:Optional
13、forEach
函数功能:遍历流
操作类型:终端操作
返回类型:void
函数式接口:Consumer
函数描述符:T -> void
14、collect
函数功能:将流进行转换
操作类型:终端操作
返回类型:R
函数式接口:Collector<T,A,R>
15、reduce
函数功能:规约流
操作类型:终端操作
返回类型:Optional
函数式接口:BinaryOperator
函数描述符:(T,T) -> T
16、count
函数功能:返回流中总元素个数
操作类型:终端操作
返回类型:long
由于篇幅的原因,流的基本计算就介绍到这里了,下文还将重点介绍流的创建,数值流与 Optional 类的使用。
欢迎加笔者微信号(dingwpmz),加群探讨,笔者优质专栏目录:
评论