《Java8函数式编程》学习笔记 - 第三章 流
第三章 流
Java8中对核心类库的改进主要包括集合类的API和新引入的流(Stream)
3.1 从外部迭代到内部迭代
样板代码的理解:是为了完成了某种操作的固定需要写的代码,譬如写循环时的for,bean的getter和setter
外部迭代
for循环起始就是一个外部迭代的过程。
首先调用Iterator方法产生一个新的Iterator对象,进而控制整个迭代过程,这就是外部迭代。
int count = 0; Iterator<Artist> iterator = allArtists.iterator(); while(iterator.hasNext()) { Artist artist = iterator.next(); if (artist.isFrom("London")) { count++; } }
外部迭代的问题:
1. 很难受喜爱能够出不同的操作
2. 本质上是一种串行化操作。
3. 使用for循环会将行为和方法混为一谈。
内部迭代
long count = allArtists.stream() .filter(artist -> artist.isFrom("London")) .count();
Stream 是用函数式编程方式在集合类上进行复杂操作的工具。
分解为2步操作:
- 找出所有来自伦敦的艺术家
- 计算出他们的人数
每种操作都对应Stream接口的一个方法。
filter:这里是指“只保留通过某项测试的对象”,测试由一个函数完成。
由于StreamAPI的函数式编程风格,我们并没有改变集合,只是对Stream里的元素增加了一个描述。
count:count()方法计算给定的Stream里包含多少个对象。
3.2 实现机制
通常Java中调用一个方法,计算机会立即执行操作。
Stream里的一些方法略有不同,他们不是返回一个新的集合,而是创建新的集合的描述
long count = allArtists.stream() .filter(artist -> artist.isFrom("London"));
这行代码并不做什么实际工作,只是对本来的Stream增加了描述。
像filter这样只描述Stream,最终不产生新集合的方法,叫做惰性求值方法。
而像count这样最终会从Stream产生值的方法叫做及早求值方法。
判断是惰性求值方法还是及早求值方法很简单:只需要看返回值是不是Stream类型。
使用这些操作的理想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果,这正是它的合理之处。
整个过程和建造者模式有共通之处。建造者模式使用一系列操作设置属性和配置,
最后调用一个build 方法,这时,对象才被真正创建。
3.3 常用的流操作
下面流操作都是针对集合的操作,所以缺省可以看作是遍历了集合中的每个元素。
3.3.1 collect
collect(toList()) 方法由Stream 里的值生成一个列表,是一个及早求值操作。
List<String> collected = Stream.of("a", "b", "c") .collect(Collectors.toList()); assertEquals(Arrays.asList("a", "b", "c"), collected);
3.3.2 map
如果有一个函数可以将一种类型的值转换成另外一种类型,
map操作就可以使用该函数,将一个流中的值转换成一个新的流。
使用for 循环将字符串转成大写。
List<String> collected = new ArrayList<>(); for (String string : asList("a", "b", "hello")) { String uppercaseString = string.toUpperCase(); collected.add(uppercaseString); }
使用map 操作将字符串转换为大写形式
List<String> collected = Stream.of("a", "b", "hello") //Stream里的每个元素遍历,并转成大写。 //传给map的Lambda表达式只接受一个String类型的参数,返回一个新的String .map(string -> string.toUpperCase()) .collect(toList()); assertEquals(asList("A", "B", "HELLO"), collected);
给map的Lambda表达式的参数和返回值不必属于同一种类型,但是Lambda表达式必须是Function接口的一个实例
map的函数接口是Function
3.3.3 filter
遍历数据并检查其中的元素时,可尝试使用Stream 中提供的新方法filter
List<String> beginningWithNumbers = Stream.of("a", "1abc", "abc1") .filter(value -> isDigit(value.charAt(0))) .collect(toList());
跟map一样,接受一个返回值肯定是true或者false的函数作为参数,
经过过滤,Stream中符合条件的,即Lambda表达式值为true的元素被保留下来。
filter的函数接口是Predicate
3.3.4 flatMap
flatMap 方法可用Stream替换值,然后将多个Stream 连接成一个Stream
3.3.5 max和min
List<Track> tracks = asList(new Track("Bakai", 524), new Track("Violets for Your Furs", 378), new Track("Time Was", 451)); Track shortestTrack = tracks.stream() //Stream里的每个元素遍历,并比较length .min(Comparator.comparing(track -> track.getLength())) //取了最短的Track之后,把整个Track取出来 .get(); assertEquals(tracks.get(1), shortestTrack);
为了让Stream 对象按照曲目长度进行排序,需要传给它一个Comparator 对象。
Java 8 提供了一个新的静态方法comparing,使用它可以方便地实现一个比较器。
放在以前,我们需要比较两个对象的某项属性的值,现在只需要提供一个存取方法就够了。
本例中使用getLength方法。
3.3.6 通用模式
max,min,以及上述找最短曲目,都用得是通用模式。下面的伪代码表示的就是通用模式
//reduce模式 Object accumulator = initialValue; for(Object element : collection) { accumulator = combine(accumulator, element); }
3.3.7 reduce
reduce操作可以实现从一组值中生成一个值。 使用reduce方法的求和如下
int count = Stream.of(1, 2, 3) .reduce(0, (acc, element) -> acc + element); System.out.println(count);
但是感觉很繁琐,一般就是一个sum就可以解决了的事情。
BinaryOperator<Integer> accumulator = (acc, element) -> acc + element; int count = accumulator.apply( accumulator.apply(accumulator.apply(0, 1),2),3);
3.3.8整合操作
是否需要对外暴露一个List或者Set对象?可能一个Stream工厂才是最好的选择。
通过Stream暴露集合最大的优点在于,很好地封装了内部实现的数据结构。
仅暴露一个Stream接口,用户在实际操作中无论如何使用,都不会影响内部List或Set。
3.4 重构遗留代码
重构时可能遇到返回List或者Set类型,只要调用.stream()方法就可以转成Stream类型继续操作。
3.5 多次调用流操作
Stream的使用习惯,以及其长处,就是其链式调用。
3.6 高阶函数
高阶函数是指接受另外一个函数作为参数,或返回一个函数的函数。 高阶函数不难辨认:看函数签名就够了。如果函数的参数列表里包含函数接口,或该函数返回一个函数接口,那么该函数就是高阶函数。
3.7 正确使用Lambda表达式
明确了要达成什么转化,而不是说明如何转化。
这种方式写出来的代码,潜在的缺陷更少,更直接表达出了程序员的意图。
明确要达成什么转化,而不是说明如何转化的另外一层含义在于写出的函数没有副作用。
这点非常重要,这样只通过函数的返回值就能充分理解函数的全部作用。
3.8 要点回顾
- 内部迭代将更多控制权交给了集合类。
- 和Iterator类似,Stream是一种内部迭代方式。
- 将Lambda表达式和Stream上的方法结合起来,可以完成很多常见的集合操作。