《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步操作:

  1. 找出所有来自伦敦的艺术家
  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上的方法结合起来,可以完成很多常见的集合操作。