读书笔记——

Java 8 Lambdas: Functional Programming for the Masses

作者:[英] Richard Warburton(王群锋 译)

第1章 简介

1.1 为什么需要再次修改Java

面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。

1.2 什么是函数式编程

核心:在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值。

1.3 示例

第2章 Lambda表达式

Java 8的最大变化是引入了Lambda表达式——一种紧凑的、传递行为的方式。

2.1 第一个Lambda表达式

button.addActionListener(event->System.out.println("button clicked"))

尽管与之前相比,Lambda表达式中的参数需要的样板代码很少,但是Java 8仍然是一种静态类型语言。为了增加可读性并迁就我们的习惯,,声明参数时也可以包括类型信息,而且有时编译器不一定能根据上下文推断出参数的类型!

2.2 如何辨别Lambda表达式

Lambda表达式的集中变体:

Runnable noArguments = () -> System.out.println("Hello World");

上述所示的Lambda表达式不包含参数,使用空括号()表示没有参数。该Lambda表达式实现了Runnable接口,该接口也只有一个run方法,没有参数,且返回类型为void。

ActionListener oneArgument = event -> System.out.println("button clicked");

上述所示的Lambda表达式包含且只包含一个参数,可省略参数的括号。

Runnable multiStatement = () -> {
    System.out.print("Hello");
    System.out.println(" World");
}

如上,表达式的主体不仅可以是一个表达式,而且也可以是一段代码块。

BinaryOperator<Long> add = (x, y) -> x + y;

如上,Lambda表达式也可以表示包含多个参数的方法,这时就有必要思考怎样去阅读该Lambda表达式。这行代码并不是将两个数字相加,而是创建了一个函数,用来计算两个数字相加的结果。变量add的类型是BinaryOperator,它不是两个数字的和,而是将两个数字相加的那行代码。

BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

如上,有时最好可以显式声明参数类型。

目标类型是指Lambda表达式所在上下文环境的类型。

Lambda表达式的类型依赖于上下文环境,是由编译器推断出来的。

2.3 引用值,而不是变量

虽然无需将变量声明为final,但在Lambda表达式中,也无法用作非终态变量,如果坚持用作非终态变量,编译器就会报错。

2.4 函数接口

函数接口是只有一个抽象方法的接口,用作Lambda表达式的类型。

接口 参数 返回类型 示例
Predicate<T> T boolean 这张唱片已经发行了吗
Consumer<T> T void 输出一个值
Function<T, R> T R 获得Artist对象的名字
Supplier<T> None T 工厂方法
UnaryOperator<T> T T 逻辑非(!)
BinaryOperator<T> (T, T) T 求两个数的乘积(*)

2.5 类型推断

Predicate<Integer> alLeast5 = x -> x > 5;
BinaryOperator<Long> addLongs = (x, y) -> x + y;

没有泛型,代码则通不过编译

BinaryOperator add = (x, y) -> x + y;

2.6 要点回顾

  • Lambda表达式是一个匿名方法,将行为像数据一样进行传递。
  • Lambda表达式的常见结构:BinaryOperator add = (x, y) -> x + y。
  • 函数接口指仅具有单个抽象方法的接口,用来表示Lambda表达式的类型。

2.7 练习

第3章 流

流使程序员得以站在更高的抽象层次上对集合进行操作。

3.1 从外部迭代到内部迭代

long count = allArtists.stream()
        .filter(artist -> artist.isFrom("Londom"))
        .count();

stream是用函数式编程方式在集合类上进行复杂操作的工具

3.2 实现机制

  • 像filter这样只描述Stream,最终不产生新集合的方法叫作惰性求值方法,惰性求值的返回值是Stream;
  • 像count这样最终会从Stream产生值的方法叫作及早求值方法,及早求值返回值是另一个值或为空。

3.3 常用的流操作

3.3.1 collect(toList())

collect(toList())方法由Stream里的值生成一个列表,是一个及早求值操作。

List<String> collected = Stream.of("a", "b", "c")
        .collect(Collectors.toList());

3.3.2 map

如果有一个函数可以将一种类型的值转换成另外一种类型,map操作就可以使用该函数,将一个流中的值转换成一个新的流。

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

3.3.3 filter

遍历数据并检查其中的元素时,可尝试使用Stream中提供的新方法filter

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

3.3.4 flatMap

flatMap方法可用Stream替换值,然后将多个Stream连接成一个Stream。

List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
        .flatMap(numbers -> numbers.stream())
        .collect(toList());

3.3.5 max和min

Track shotestTrack = tracks.stream()
        .min(Comparator.comparing(track -> track.getLength()))
        .get();

Stream的min 和 max方法,返回Optional对象。通过调用get方法可以取出Optional对象中的值。

3.3.6 通用模式

3.3.7 reduce

reduce操作可以实现从一组值中生成一个值。count、min和max方法,因为常用而被纳入标准库中。这些方法都是reduce操作。

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

reducer的类型是BinaryOperator。

展开reduce操作:

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

3.3.8 整合操作

Set<String> origins = album.getMusicians()
        .filter(artist -> artist.getName().startsWith("The"))
        .map(artist -> artist.getNationality())
        .collect(toSet());

通过Stream暴露集合的最大优点在于,它很好地封装了内部实现的数据结构。仅暴露一个Stream接口,用户在实际操作中无论如何使用,都不会影响内部的list或Set。

3.4 重构遗留代码

albums.stream()
        .flatMap(album -> album.getTracks())
        .filter(track -> track.getLength() > 60)
        .map(track -> track.getName())
        .collect(toSet());

3.5 多次调用流操作

避免每一步操作都强制对函数求值

3.6 高阶函数

如果函数的参数列表里包含函数接口,或该函数返回一个函数接口,那么该函数就是高阶函数。

3.7 正确使用Lambda表达式

无论何时,将Lambda表达式传给Stream上的高阶函数,都应该尽量避免副作用。唯一的例外是forEach方法,它是一个终结方法。

3.8 要点回顾

  • 内部迭代将更多控制权交给了集合类。
  • 和Iterator类似,Stream是一种内部迭代方式。
  • 将Lambda表达式和Stream上的方法结合起来,可以完成很多常见的集合操作。

3.9 练习

3.10 进阶练习

第4章 类库

Java 8中的另一个变化是引入了默认方法和接口的静态方法,它改变了人们认识类库的方式,接口中的方法也可以包含代码体了。

4.1 在代码中使用Lambda表达式

使用Lambda表达式简化日志代码

Logger logger = new Logger();
logger.debug(() -> "Look at this!");

启用Lambda表达式实现日志记录器

public void debug(Supplier<String> message) {
    if (isDebugEnabled()) {
        debug(message.get());
    }
}

4.2 基本类型

IntSummaryStatistics trackLengthStats = album.getTracks()
        .mapToInt(track -> track.getLength))
        .sumaryStatistics();
System.out.println("Max: %d, Min: %d, Avg: %f, Sum: %d",
        trackLengthStats.getMax(),
        trackLengthStats.getMin(),
        trackLengthStats.getAverage(),
        trackLengthStats.getSum());

这些统计值在所有特殊处理的Stream,如DoubleStream、LongStream中都可以得出。如无需全部的统计值,也可分别调用min、max、average或sum方法获得单个的统计值,同样,三种基本类型对应的特殊Stream也都包含这些方法。

4.3 重载解析

Lambda表达式作为参数时,其类型由它的目标类型推导得出,推导过程遵循如下规则:

  • 如果只有一个可能的目标类型,由相应函数接口里的参数类型推导得出
  • 如果有多个可能的目标类型,由最具体的类型推导得出
  • 如果有多个可能的目标类型且最具体的类型不明确,则需人为指定类型

4.4 @FunctionalInterface

为了提高Stream对象可操作性而引入的各种新接口,都需要有Lambda表达式可以实现它。它们存在的意义在于将代码块作为数据打包起来。因此,它们都添加了@FunctionalInterface注释。

该注释会强制javac检查一个接口是否符合函数接口的标准。如果不符合,javac就会报错。重构代码时,使用它能很容易发现问题。

4.5 二进制接口的兼容性

在JDK之外实现Colleciton接口的类,需要实现新增的stream方法。为了避免这个糟糕情况,则需要在Java 8中添加新的语言特性:默认方法

4.6 默认方法

default void forEach(Consumer<? super T> action) {
    for (T t : this) {
        action.accept(t);
    }
}

默认方法和子类

类中重写的方法胜出

4.7 多重继承

接口允许多重继承,因此有可能碰到两个接口包含签名相同的默认方法的情况。如果子类不重写该方法,则javac并不明确应该继承哪个接口中的方法,因此编译器会报错。

三定律

  1. 类胜于接口。如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义的方法。
  2. 子类胜于父类。如果一个接口继承了另一个接口,且两个接口都定义了一个默认方法,那么子类中定义的方法胜出。
  3. 没有规则三。如果上面两条规则不适用,子类要么需要实现该方法,要么将该方法声明为抽象方法。

其中第一条规则是为了让代码向后兼容。

4.8 权衡

接口和抽象类之间还是存在明显的区别。接口允许多重继承,却没有成员变量;抽象类可以继承成员变量,却不能多重继承。在对问题域建模时,需要根据具体情况进行权衡,而在以前的Java中可能并不需要这样。

4.9 接口的静态方法

Stream是个接口,Stream.of是接口的静态方法。

Stream和其他几个子类还包含另外几个静态方法,特别是range和iterate方法提供了产生Stream的其他方式。

4.10 Optional

Optinal是为核心类库新设计的一个数据类型,用来替换null值。

使用Optional对象有两个目的:

  • 首先,Optional对象鼓励程序员适时检查变量是否为空,以避免代码缺陷;
  • 其次,它将一个类的API中可能为空的值文档化,这笔阅读实现代码要简单的多。

    Optional a = Optional.of("a"); Optional emptyOptional = Optional.empty(); Optional alsoEmpty = Optional.ofNullable(null); emptyOptinal.isPresent(); emptyOptional.orElse("b"); emptyOptional.oeElseGet(()->"c"));

4.11 要点回顾

  • 使用为基本类型定制的Lambda表达式和Stream,如IntStream可以显著提升系统性能。
  • 默认方法是指接口中定义的包含方法体的方法,方法名有default关键字做前缀。
  • 在一个值可能为空的建模情况下,使用Optional对象能替代使用null值。

4.12 练习

4.13 开放练习

第5章 高级集合类和收集器

5.1 方法引用

artiset -> artist.getName()

方法引用重写上面的Lambda表达式:

Artist::getName

标准语法为Classname::methodName。

(name, nationality) -> new Artist(name, nationality)

使用方法引用,上述代码可写为:

Artist::new

创建数组:

String[]::new

5.2 元素顺序

List<Integer> sameOrder = numbers.stream()
        .sorted()
        .collect(toList()); 

一些操作在有序的流上开销更大,调用unordered方法消除这种顺序就能解决该问题。大多数操作都是在有序流上效率更高,比如filter、map和reduce等。

forEach方法不能保证元素是按顺序处理的。如果需要保证按顺序处理,应该使用forEachOrdered方法。

5.3 使用收集器

5.3.1 转换成其他集合

在调用toList或者toSet方法时,不需要指定具体的类型。Stream类库在背后自动为你挑选出了合适的类型。比如,你可能希望使用TreeSet,而不是由框架在背后自动为你指定一种类型的Set,此时就可以使用toCollection,它接受一个函数作为参数,来创建集合。

stream.collect(toCollection(TreeSet::new));

5.3.2 转换成值

Function<Artist, Long> getCount = artist -> artist.getMembers().count();
Optional<Artist> biggestGroup = artists.collect(maxBy(Comparing(getCount)));

albums.stream().collect(averagingInt(album -> album.getTrackList().size()));

5.3.3 数据分块

另外一个常用的流操作是将其分解成两个集合。

有这样一个收集器partitioningBy,它接受一个流,并将其分成两部分。它使用Predicate对象判断一个元素应该属于哪个部分,并根据布尔值返回一个Map列表。

Map<Boolean, List<Artist>> bandsAndSolo = artists.collect(partitioningBy(artist -> artist.isSolo()));

Map<Boolean, List<Artist>> bandsAndSolo = artists.collect(partitioningBy(Artist::isSolo));

5.3.4 数据分组

Map<Artist, List<Album>> albumsByArtist = albums.collect(groupingBy(album -> album.getMainMusician()));

5.3.5 字符串

String result = artists.stream()
        .map(Artist::getName)
        .collect(Collectors.joining(",", "[", "]"));

Collectors.joining方法可以方便地从一个流得到一个字符串,允许用户提供分隔符、前缀和后缀

5.3.6 组合收集器

Map<Artist, Long> numberOfAlbums = albums.collect(groupingBy(album -> album.getMainMuscian(), counting()));

Map<Artist, List<String>> nameOfAlbums = albums.collect(groupingBy(Album::getMainMusician, mapping(Album::getName, toList())));

5.3.7 重构和定制收集器

String result = artists.stream()
        .map(Artist::getName)
        .collect(new StringCollector(",", "[", "]"));

定义字符串收集器:

public class StringCollector implements Collector<String, StringCombiner, String> {
    ...
}

一个收集器由四部分组成。首先是一个Supplier,这是一个工厂方法,用来创建容器。

public Supplier<StringCombiner> supplier() {
    return () -> new StringCombiner(delim, prefix, suffix);
} 

收集器的accumulator结合之前操作的结果和当前值,生成并返回新的值。

public BiConsumer<StringCombiner, String> accumulator() {
    return StringCombiner::add;
}

combine方法用于合并两个容器

public BinaryOperator<StringCombiner> combiner() {
    return StringCombiner::merge;
}

在收集阶段,容器被combiner方法成对合并进一个容器,直到最后只剩下一个容器为止。

finisher方法,转换成最终想要的结果

public Function<StringCombiner, String> finisher() {
    return StringCombiner::toString;
}

5.3.8 对收集器的归一化处理

reducing收集器,低效,建议定制收集器

5.4 一些细节

用Map实现缓存的读取

artistCache.computeIfAbsent(name, this::readArtistFromDB);

使用内部迭代遍历Map

albumsByArtist.forEach((artist, albums) -> {
    ......
});

5.5 要点回顾

  • 方法引用是一种引用方法的轻量级语法,形如:ClassName::methodName。
  • 收集器可用来计算流的最终值,是reduce方法的模拟。
  • Java 8 提供了收集多种容器类型的方式,同时允许用户自定义收集器。

5.6 练习

第6章 数据并行化

6.1 并行和并发

并发是两个任务共享时间段,并行则是两个任务在同一时间发生,比如运行在多核CPU上。如果一个程序要运行两个任务,并且只有一个CPU给它们分配了不同的时间片,那么这就是并发,而不是并行。

数据并行化:将数据分成块,为每块数据分配单独的处理单元。

当需要在大量数据上执行同样的操作时,数据并行化很管用。它将问题分解为可在多块数据上求解的形式,然后对每块数据执行运算,最后将各数据块上得到的结果汇总,从而获得最终答案。

任务并行化:线程不同,工作各异。

6.2 为什么并行化如此重要

6.3 并行化流操作

并行化操作流只需改变一个方法调用。如果已经有一个Stream对象,调用它的parallel方法就能让其拥有并行操作的能力。如果想从一个集合类创建一个流,调用parrelStream就能立即获得一个拥有并行能力的流。

int parallelArraySum = albums.parallelStream()
        .flatMap(Album::getTracks)
        .mapToInt(Track::getLength)
        .sum();

6.4 模拟系统

使用蒙特卡洛模拟法并行化模拟掷骰子事件

double fraction = 1.0 / N;
Map<Integer, Double> parallelDiceRolls = IntStream.range(0, N)
        .parallel()
        .mapToObj(twoDiceThrows())
        .collect(groupingBy(side -> side,
            sumingDouble(n -> fraction)));

6.5 限制

  • reduce方法初值必须为组合函数的恒等值,拿恒等值和其他值做reduce操作时,其他值保持不变。比如求和操作,其初值必须为0。
  • reduce操作的另一个限制是组合操作必须符合结合律。这意味着只要序列的值不变,组合操作的顺序不重要。

使用parallel方法能轻易将流转换为并行流。还有一个叫sequential的方法。在要对流求值时,不能同时处于两种模式,要么是并行的,要么是串行的。如果同时调用了parallel和sequential方法,最后调用的那个方法起效。

6.6 性能

影响并行流性能的主要因素:

  • 数据大小:输入数据的大小会影响并行化处理对性能的提升。
  • 源数据结构
  • 装箱:处理基本类型比处理装箱类型要快
  • 核的数量
  • 单元处理开销:花在流中每个元素身上的时间越长,并行操作带来的性能提升越明显。

    int addIntegers = values.parallelStream() .mapToInt(i -> i) .sum();

根据性能好坏,将核心类库提供的通用数据结构分成3组:

  • 性能好:ArrayList,数组或IntStream.range,这些数据结构支持随机读取,能轻易地被任意分解。
  • 性能一般:HashSet、TreeSet,这些数据结构不易公平地被分解,但是大多数时候分解是可能的。
  • 性能差:有些数据结构难于分解,如LinkedList,对半分解太难了,还有Streams.iterate和BufferedReader.lines,它们长度未知,因此很难预测该在哪里分解。

在讨论流中单独操作每一块的种类时,可以分成两种不同的操作:无状态的和有状态的。无状态操作整个过程中不必维护状态,有状态操作则有维护状态所需的开销和限制。

如果能避开有状态,选用无状态操作,就能获得更好的并行性能。无状态操作包括map、filter和flatMap,有状态操作包括sorted、distinct和limit。

6.7 并行化数组操作

Java 8还引入了一些针对数组的并行操作,脱离流框架也可以使用Lambda表达式。

  • parallelPrefix:更新一个数组,将每一个元素替换为当前元素和前驱元素的和,这里的”和”是一个宽泛的概念,它不必是加法,可以是任意一个BinaryOperator。
  • parallelSetAll:使用Lambda表达式更新数组元素
  • parallelSort:并行化对数组元素排序

使用并行化数组操作初始化数组

Arrays.parallelSetAll(values, i -> i);

计算简单滑动平均数

Arrays.parallelPrefix(sums, Double::sum);
int start = n - 1;
double[] simpleMovingAverage = IntStream.range(start, sums.length)
        .mapToDouble(i -> {
            double prefix = i == start ? 0 : sums[i - n];
            return (sums[i] - prefix) / n;
        })
        .toArray();

6.8 要点回顾

  • 数据并行化是把工作拆分,同时在多核CPU上执行的方式。
  • 如果使用流编写代码,可通过调用parallel或者parallelStream方法实现数据并行化操作。
  • 影响性能的五要素是:数据大小、源数据结构、值是否装箱、可用的CPU核数量,以及处理每个元素所花的时间。

6.9 练习

第7章 测试、调试和重构

7.1 重构候选项

7.1.1 进进出出、摇摇晃晃

logger.debug(() -> "Hello Wrold!");

7.1.2 孤独的覆盖

7.1.3 同样的东西写两遍

领域方法

public long countFeature(ToLongFunction<Album> function) {
    return albums.stream()
            .mapToLong(function)
            .sum();
}

public long countTracks() {
    return countFeature(album -> album.getTracks().count());
}

public long countRunningTime() {
    return countFeature(album -> album.getTracks()
            .mapToLong(track -> track.getLength())
            .sum());
}

7.2 Lambda表达式的单元测试

将Lambda表达式重构为一个方法,然后在主程序中使用

public static List<String> elementFirstToUppercase(List<String> words) {
    return words.stream()
            .map(Testing::firstToUppercase)
            .collect(Collectors.<String>toList());
}

public static String firstToUppercase(String value) {
    char firstChar = Character.toUpperCase(value.charAt(0));
    return firstChar + value.substring(1);
}

7.3 在测试替身时使用Lambda表达式

List<String> list = mock(List.class);
when(list.size()).thenAnswer(inv -> otherList.size());
assertEquals(3, list.size());

7.4 惰性求值和调试

7.5 日志和打印消息

forEach方法缺点:无法再继续操作流了,流只能使用一次,如果我们还想继续,必须重新创建流。

7.6 解决方案:peek

peek方法:能查看每个值,并且继续操作流。

使用peek方法还能以同样的方式,将输出定向到现有日志系统中。

7.7 在流中间设置断点

可在peek方法中加入断点

7.8 要点回顾

  • 重构遗留代码时考虑如何使用Lambda表达式,有一些通用的模式。
  • 如果想要对复杂一点的Lambda表达式编写单元测试,将其抽取成一个常规的方法。
  • peek方法能记录中间值,在调试时非常有用。

第8章 设计和架构的原则

软件开发最重要的设计工具不是什么技术,而是一颗在设计原则方面训练有素的头脑。

8.1 Lambda表达式改变了设计模式

8.1.1 命令者模式

命令者是一个对象,它封装了调用另一个方法的所有细节,命令者模式使用该对象,可以编写出根据运行期条件,顺序调用方法的一般化代码。

命令接收者:执行实际任务

public interface Editor {

    void save();

    void open();

    void close();
}

像open、save这样的操作称为命令。 命令者、具体命令者:封装了所有调用命令执行者的信息。

public interface Action {

    void perform();
}

public class Save implements Action {

    private final Editor editor;

    public Save(Editor editor) {
        this.editor = editor;
    }

    @Override
    public void perform() {
        editor.save();
    }
}

发起者:控制一个或多个命令的顺序和执行。

public class Macro {

    private final List<Action> actions;

    public Micro() {
        actions = new ArrayList<>();
    }

    public void record(Action action) {
        actions.add(action);
    }

    public void run() {
        actions.forEach(Action::perform);
    }
}

客户端:创建具体的命令者实例。

Macro macro = new Macro();
macro.record(new Open(editor));
macro.record(new Save(editor));
macro.record(new Close(editor));
macro.run();

事实上,所有的命令类都是Lambda表达式: 使用Lambda表达式构建宏:

Macro macro = new Macro();
macro.record(() -> editor.open());
macro.record(() -> editor.save());
macro.record(() -> editor.close());
macro.run();

使用方法引用构建宏

Macro macro = new Macro();
macro.record(editor::open);
macro.record(editor::save);
macro.record(editor::close);
macro.run();

8.1.2 策略模式

使用Lambda表达式可以去掉具体的策略实现,使用一个方法实现算法。

8.1.3 观察者模式

使用Lambda表达式可以去掉具体观察者的实现。

将大量代码塞进一个方法会让可读性变差是决定如何使用Lambda表达式的黄金法则。

8.1.4 模板方法模式

public class LoanApplication {

    private final Criteria identity;
    private final Criteria creditHistory;
    private final Criteria incomeHistory;

    public LoanApplication(Criteria identity, Criteria creditHistory, Criteria incomeHistory) {

        this.identity = identity;
        this.creditHistory = creditHistory;
        this.incomeHistory = incomeHistory;
    }

    public void checkLoanApplication() throws ApplicationDenied {

        identity.check();
        creditHistory.check();
        incomeHistory.check();
        reportFindings();
    }

    private void reportFindings() {

        ...
    }
}

public interface Criteria {

    public void check() throws ApplicationDenied;
}

public class CompanyLoanApplication extends LoanApplication {

    public CompanyApplication(Company company) {

        super(company::checkIdentity, company::checkHistoricalDebt, company::checkProfitAndLoss);
    }
}

8.2 使用Lambda表达式的领域专用语言

领域专用语言(DSL)是针对软件系统中某特定部分的编程语言。DSL高度专用:不求绵绵俱到,但求有所专长。

两类DSL:

  • 外部DSL:脱离程序源码编写,然后单独解析和实现。比如级联样式表(CSS)和正则表达式。
  • 内部DSL:内部DSL嵌入编写它们的编程语言中。

概念:

  • 每一个规则描述了程序的一种行为;
  • 期望是描述应用行为的一种方式,在规则中定义;
  • 多个规则合在一起,形成一个套件。

与JUnit对照,规则对应一个测试方法,期望对应断言,套件对应一个测试类。

8.2.1 使用Java编写DSL

8.2.2 实现

8.2.3 评估

8.3 使用Lambda表达式的SOLID原则

SOLID原则:

  • Single responsibility:单一功能原则
  • Open/closed:开闭原则
  • Liskov substitution:里氏替换原则
  • Interface segregation:接口分离原则
  • Dependency inversion:依赖反转原则

8.3.1 单一功能原则

程序中的类或方法只能有一个改变的理由。

public long countPrimes(int upTo) {

    return IntStream.range(1, upTo)
            .filter(this::isPrime)
            .count();
}

private boolean isPrime(int number) {

    return IntStream.range(2, number)
            .allMatch(x -> (number % x) != 0);
}

8.3.2 开闭原则

在Java 8中,任何传入高阶函数的Lambda表达式都由一个函数接口表示,高阶函数负责调用其唯一的方法,根据传入Lambda表达式的不同,行为也不同。这其实也是在用多态来实现开闭原则。

8.3.3 依赖反转原则

抽象不应依赖细节,细节应该依赖抽象。

public List<String> findHeadings(Reader input) {

    return withLinesOf(input, 
        lines -> lines.filter(line -> line.endsWith(":")))
                .map(line -> line.substring(0, line.length() - 1))
                .collect(toList()),
        HeadingLookupException::new);
}

private <T> T withLinesOf(Reader input, Function<Stream<String>, T> handler, Function<IOException, RuntimeException> error) {

    try (BufferedReader reader = new BufferedReader(input)) {
        return handler.apply(reader.lines());
    } catch (IOException e) {
        throw error.apply(e);
    }
}

8.4 进阶阅读

8.5 要点回顾

  • Lambda表达式能让很多现有设计模式更简单,可读性更强,尤其是命令者模式。
  • 在Java8中,创建领域专用语言有更多的灵活性。
  • 在Java8中,有应用SOLID原则的新机会。

第9章 使用Lambda表达式编写并发程序

9.1 为什么要使用非阻塞式I/O

非阻塞式I/O,有时也叫异步I/O,可以处理大量并发网络连接,而且一个线程可以为多个连接服务。如对聊天程序客户端的读写调用立即返回,真正的读写操作则在另一个独立的线程执行,这样就可以同时执行其他任务了。

9.2 回调

Java8为Pattern类新增了一个splitAsStream方法,该方法使用正则表达式将字符串分割好后,生成一个包含分割结果的流对象。

newline.splitAsStream(buffer.toString())
        .forEach(line -> {
            ...
        });

9.3 消息传递架构

9.4 末日金字塔

with方法提高可读性,但一个功能分散在了多个方法里,代码还是难于阅读。

9.5 Future

9.6 CompletableFuture

9.7 响应式编程

9.8 何时何地使用新技术

9.9 要点回顾

  • 使用基于Lambda表达式的回调,很容易实现事件驱动架构
  • CompletableFuture代表了IOU,使用Lambda表达式能方便地组合、合并
  • Observable继承了CompletableFuture的概念,用来处理数据流。

9.10 练习

第10章 下一步怎么办