java8函数式编程读书笔记——Stream流篇

935 阅读7分钟

在正文开始前,我想先问一个问题,在初学Stream流的时候,你是如何理解Stream流的?

我们都知道用Stream流处理集合更加简洁高效,而且易于并行化。那么一开始你认为Stream流是如何做到这点的呢?

在刚开始接触到Stream流这个概念时,我第一个感觉是好洋气,好高大上。他名字里有个流,是不是像字节流一样,把集合转换成二进制编码,然后直接操作编码。牛逼!

我理解的更高效是代码执行速度更快,那肯定是开了线程,stream流难道能把集合中的元素取出来,然后开线程去处理这些元素。牛逼!

这个错误的认知一直保持到我实习,工作的时候组长让我重构下查询接口,因为现在数据只有几万条,查询速度业务还能接受,等后面数据量再增加需要等待的时间可能达到一两分钟,早晚得重构。反正我现在闲着没事,就先研究下,至少能给后面重构提供思路。

我一听要加快查询速度,就问:是要分析SQL语句,看看有没有用到索引吗?组长说:肯定不存在慢SQL,你想想还有什么会影响响应速度。

俺一寻思,莫非是中间有个特别慢的算法?"停停停!组长直接打断我,你第一天来的时候我告诉过你什么?"我:"啊?",组长:"别在循环体里进行SQL查询,不管这个SQL执行的速度是快还是慢"哦哦,网络IO会消耗cpu和时间所以是这个原因导致的查询接口在数据量大的时候变慢"。我赶紧补充。看到组长露出还算有救的神色,我稍稍安心。

经过两天的奋斗,我拿出了第一版的成果,笑死!根本没加快,查询一千条还慢了2秒。组长看了长叹一口气,带着我过了一遍代码,指出我这两天重构的部分代码,根本不需要变动,我根本没找到需要优化的那部分代码。然后又指出,我操作集合的方式很外行,让我用stream流。

我脱口而出:"用stream流的话遇到线程安全问题怎么办?",这下周边几个同事也一起回过头,用一种孩子你没事吧的眼神看着我。

组长赶忙打圆场,你说的是并行流吗?一般直接用就行了,不用考虑这个。

我心虚的嗷了一声,回去后就开始爆肝java8函数式编程这本书。

现在我终于明白,stream流并不是一个新的数据结构,也没有改变数据的存储方式,它是通过接收一个该集合对象的迭代器来对集合数据进行处理的,而且处理完并不会改变这个集合,只会产生一个新的集合。

关于stream流是如何创建,以及节点的中间操作和结束操作。这本书没有详细介绍。但这本书通过外部迭代的代码,帮助我们了解了我们在调用哪几个常用方法时,到底做了什么。这里我将节选几个经典案例,以备日后复习。

map操作

map函数可以将一种类型的值转换为另一种类型

首先我们看一下map方法接收的参数

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Function就是一个函数式接口,它接收一个R类型的子类和T类型的超类作为参数接口中唯一的抽象方法是

R apply(T t);

接收一个T类型的参数,返回一个R类型的值。

那么为什么stream流中我们传入一个lambda()表达式他就能把整个集合给映射一遍呢?

这里用书中的例子来进行说明首先我们先用外部迭代来把一个小写字符串列表转换为大写字符串列表。

		List<String> collected = new ArrayList<>();
		for (String string : asList("a", "b", "hello")) {
			String uppercaseString = string.toUpperCase();
			collected.add(uppercaseString);
		}

然后我们用stream流中的map操作来实现一遍

	List<String> collected = Stream.of("a", "b", "hello")
                                    .map(string -> string.toUpperCase()) ?
                                    .collect(toList());

对比这两段代码,不难发现结合中单个元素变量名都为string,而且处理映射的代码也是相同的。

另外还有一个细节就是,外部迭代时,并没有直接改变集合内部元素,而是通过一个新的变量来接收。

由此我们可以推断出两个结论

1.Stream流操作实质上,就是在内部迭代时调用了我们传入的lambda表达式。

2.为什么Stream有了终止操作,才能真正开始计算,那是因为只有终止操作如collect,才会产生一个可以存储或计算中间操作返回值的对象。

filter操作

filter函数的作用是对集合元素进行过滤

我们先看filter函数接口

    Stream<T> filter(Predicate<? super T> predicate);

Predicate是一个函数式接口,接收参数为T或者T的超类、

接口中唯一的抽象方法是

boolean test(T t);

返回值为boolean类型

这里依旧使用书中的例子:找出一组字符串中以数字开头的字符串

我们先看外部迭代是如何做到的

    	List<String> beginningWithNumbers = new ArrayList<>();
            for(String value : asList("a", "1abc", "abc1")) {
                if (isDigit(value.charAt(0))) {
                    beginningWithNumbers.add(value);
                }
            }

再看Stream流是如何操作的

	List<String> beginningWithNumbers
                        = Stream.of("a", "1abc", "abc1")
                                   .filter(value -> isDigit(value.charAt(0)))
                                    .collect(toList());

我们可以理解为内部迭代时,通过调用我们传入的lambda表达式进行元素筛选。

reduce操作

reduce模式可以实现从一组值中生成一个值。如count、min、max都属于reduce操作

我们先看reduce接口的定义

	Optional<T> reduce(BinaryOperator<T> accumulator);
	
	T reduce(T identity, BinaryOperator<T> accumulator);
	
	<U> U reduce(U identity,
           BiFunction<U, ? super T, U> accumulator,
           BinaryOperator<U> combiner);
reduce方法有三个重载方法,其中BinaryOperator<T>是一个函数式接口
这个接口继承自BiFunction<T,T,T>接口,包含了minBy和maxBy两个静态方法

BiFunction<T, U, R>也是一个函数式接口,包含了一个抽象方法    
R apply(T t, U u);

我们先从第一个接口分析,这个接口只含有一个BinaryOperator<T>类型的参数
BinaryOperator<T>的抽象方法来自于它的父类BiFunction<T,T,T>
可以理解为在BinaryOperator中 T apply(T t,U u);三个参数类型都为T
它接受两个相同类型的参数并返回一个相同类型的结果这个操作符定义了如何将流中的两个元素组合成一个结果

第二个构造函数多了个参数T identity,这个identity是作为计算时的初始值

第三个构造函数接收三个参数
U identity - 初始值,用于归约操作的开始
BiFunction<U, ? super T, U> accumulator,- 累加器函数,它接受两个参数:归约操作的当前结果(类型为 U)和流中的下一个元素(类型为 T 的超类),并返回一个新的归约结果(类型为 U)
BinaryOperator<U> combiner - 组合器函数,它只在并行流中使用,用于合并两个归约结果

归约操作的外部迭代方式与之前不同,这里依旧引用书中的例子来讲解

使用 reduce 求和

外部迭代
   BinaryOperator<Integer> accumulator = (acc, element) -> acc + element;
int count = accumulator.apply(
                accumulator.apply(
                      accumulator.apply(0, 1),
                          2),
                                3);

Stream流内部迭代

 int count = Stream.of(1, 2, 3)
                    .reduce(0, (acc, element) -> acc + element);

可以理解为,reduce方法其实是一个递归方法,递归结束条件就是递归次数==集合长度。 前一次计算的结果作为后一次计算的参数。

结尾

这本书后续的内容还有很多,不仅通俗易懂,而且对知识点的讲解鞭辟入里。看完后我感觉我强的可怕!一定要找个机会 在那群老登面前装一个。不过笔记就先到此为止,累了摆烂。