目录

为什么要关心Java8

本章内容

  • Java怎么又变了
  • 日新月异的计算应用背景:多核和处理大型数据集(大数据)
  • 改进的压力:函数式比命令式更适应新的体系架构
  • Java 8的核心新特性:Lambda(匿名函数)、流、默认方法

在Java 8里面,你可以编写更为简洁的代码,这些代码读起来更接近问题的描述:

inventory.sort(comparing(Apple::getWeight));


在Java 8之前,专家们可能会告诉你,必须利用线程才能使用多个内核。问题是,线程用起 来很难,也容易出现错误。从Java的演变路径来看,它一直致力于让并发编程更容易、出错更少。

Java 1.0里有线程和锁,甚至有一个内存模型——这是当时的最佳做法,但事实证明,不具备专 门知识的项目团队很难可靠地使用这些基本模型。Java 5添加了工业级的构建模块,如线程池和 并发集合。Java 7添加了分支/合并(fork/join)框架,使得并行变得更实用,但仍然很困难。而 Java 8对并行有了一个更简单的新思路,不过你仍要遵循一些规则,本书中会谈到。


Java8中的一些想法

  • Stream API

  • 向方法传递代码的技巧

  • 接口中的默认方法

Java 8提供了一个新的API(称为“流”,Stream),它支持许多处理数据的并行操作,其思路 和在数据库查询语言中的思路类似——用更高级的方式表达想要的东西,而由“实现”(在这里 是Streams库)来选择最佳低级执行机制。这样就可以避免用synchronized编写代码,这一代码 不仅容易出错,而且在多核CPU上执行所需的成本也比你想象的要高。

多核CPU的每个处理器内核都有独立的高速缓存。加锁需要这些高速缓存同步运行,然而这又需要在内核间进行 较慢的缓存一致性协议通信。

从有点修正主义的角度来看,在Java 8中加入Streams可以看作把另外两项扩充加入Java 8 的直接原因:把代码传递给方法的简洁方式(方法引用、Lambda)和接口中的默认方法

Java 8里面将代码传递给方法的功能(同时也能够返回代码并将其包含在数据结构中)还让 我们能够使用一整套新技巧,通常称为函数式编程。一言以蔽之,这种被函数式编程界称为函数的代码,可以被来回传递并加以组合,以产生强大的编程语汇。

Java怎么还在变

Java在编程语言生态系统中的位置

https://gitee.com/lienhui68/picStore/raw/master/null/20200806164804.png

流处理

第一个编程概念是流处理。介绍一下,流是一系列数据项,一次只生成一项。程序可以从输 入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。一个程序的输出流很可能是另一个程序的输入流。

用行为参数化把代码传递给方法

并行与共享的可变数据

你的行为必须能够同时对不同的输入安全地执行。一般情况下这就意味着,你写代码时不能访问共享的可变数据。这些函数有时被称为“纯 函数”或“无副作用函数”或“无状态函数”,这一点我们会在第7章和第13章详细讨论。前面说 的并行只有在假定你的代码的多个副本可以独立工作时才能进行。但如果要写入的是一个共享变 量或对象,这就行不通了。

Java 8的流实现并行比Java现有的线程API更容易,因此,尽管可以使用synchronized来打 破“不能有共享的可变数据”这一规则,但这相当于是在和整个体系作对,因为它使所有围绕这一规则做出的优化都失去意义了。在多个处理器内核之间使用synchronized,其代价往往比你 预期的要大得多,因为同步迫使代码按照顺序执行,而这与并行处理的宗旨相悖。

这两个要点(没有共享的可变数据,将方法和函数即代码传递给其他方法的能力)是我们平常所说的函数式编程范式的基石,我们在第13章和第14章会详细讨论。

Java需要演变

语言需要不断改进以跟进硬件的更新或满足程序员的期待(如果你还不够信服,想想COBOL还一度是商业上最重要的语言之一呢)。

Java中的函数

编程语言的整个目的就在于操作值,要是按照历史 上编程语言的传统,这些值因此被称为一等值(或一等公民,这个术语是从20世纪60年代美国民 权运动中借用来的)。编程语言中的其他结构也许有助于我们表示值的结构,但在程序执行期间 不能传递,因而是二等公民。前面所说的值是Java中的一等公民,但其他很多Java概念(如方法 和类等)则是二等公民。用方法来定义类很不错,类还可以实例化来产生值,但方法和类本身都 不是值。这又有什么关系呢?还真有,人们发现,在运行时传递方法能将方法变成一等公民。这在编程中非常有用,因此Java 8的设计者把这个功能加入到了Java中。顺便说一下,你可能会想, 让类等其他二等公民也变成一等公民可能也是个好主意。有很多语言,如Smalltalk和JavaScript, 都探索过这条路。

方法和Lambda作为一等公民

方法引用

1
2
3
4
5
6
7
public class Demo1 {
    public static void main(String[] args) {
        File[] hiddenFiles = new File("/Users/david").listFiles(File::isHidden);
        Arrays.asList(hiddenFiles).forEach(System.out::println);
    }

}

语法: 对象::方法引用 (即把这个方法作为值)

在Java 8里写下 File::isHidden的时候,你就创建了一个方法引用,你同样可以传递它。第3章会详细讨论这 一概念。只要方法中有代码(方法中的可执行部分),那么用方法引用就可以传递代码。

Lambda——匿名函数

除了允许(命名)函数成为一等值外,Java 8还体现了更广义的将函数作为值的思想,包括 ① Lambda (或匿名函数)。比如,你现在可以写(int x) -> x + 1,表示“调用时给定参数x, 就返回x + 1值的函数”。你可能会想这有什么必要呢?因为你可以在MyMathsUtils类里面定义 一个add1方法,然后写MyMathsUtils::add1嘛!确实是可以,但要是你没有方便的方法和类可用,新的Lambda语法更简洁。第3章会详细讨论Lambda。我们说使用这些概念的程序为函数式编程风格,这句话的意思是“编写把函数作为一等值来传递的程序

传递代码:一个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class InventoryDemo {
    public static void main(String[] args) {
        List<Apple> apples = Lists.newArrayList(
                new Apple(10, Color.RED),
                new Apple(18, Color.RED),
                new Apple(10, Color.GREEN)
        );
        List<Apple> rs = apples.parallelStream().filter(
                apple -> apple.getWeight() > 15
                        && apple.getColor() == Color.RED
        ).collect(Collectors.toList());
        rs.forEach(System.out::println);
    }


}

enum Color {
    GREEN, RED
}

class Apple {

    private int weight;
    private Color color;

    public Apple(int weight, Color color) {
        this.weight = weight;
        this.color = color;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    public Color getColor() {
        return color;
    }

    public void setColor(Color color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "Apple{" +
                "weight=" + weight +
                ", color=" + color +
                '}';
    }
}

从传递方法到Lambda

你甚至都不需要为只用一次的方法写定义;代码更干净、更清晰,因为你用不着去找自己到底传递了什么代码。但要是Lambda的长度多于几行(它的行为也不是一目了然)的话, 那你还是应该用方法引用来指向一个有描述性名称的方法,而不是使用匿名的Lambda。你应该以代码的清晰度为准绳

Java 8的设计师几乎可以就此打住了,要是没有多核CPU,可能他们真的就到此为止了。我 们迄今为止谈到的函数式编程竟然如此强大, 在后面你更会体会到这一点。 本来, Java加上 filter和几个相关的东西作为通用库方法就足以让人满意了,比如

static <T> Collection<T> filter(Collection<T> c, Predicate<T> p);

这样你甚至都不需要写filterApples了,因为比如先前的调用

filterApples(inventory, (Apple a) -> a.getWeight() > 150 );

就可以直接调用库方法filter:

filter(inventory, (Apple a) -> a.getWeight() > 150 );

但是, 为了更好地利用并行, Java的设计师没有这么做。 Java 8中有一整套新的类集合 API——Stream,它有一套函数式程序员熟悉的、类似于filter的操作,比如map、reduce,还 有我们接下来要讨论的在Collections和Streams之间做转换的方法。

用集合的话,你得自己去做迭代的过程。你得用for-each循环一个个去迭代元素,然后再处理元素。我们把这种数据迭代的方法称为外部迭代。相反,有了Stream API,你根本用不着操心循环的事情。数据处 理完全是在库内部进行的。我们把这种思想叫作内部迭代

多线程并非易事

通过多线程代码来利用并行(使用先前Java版本中的Thread API)并非易事。你 得换一种思路:线程可能会同时访问并更新共享变量。因此,如果没有协调好 ,数据可能会被 意外改变。

传统上是利用synchronized关键字,但是要是用错了地方,就可能出现很多难以察觉的错误。Java 8基于Stream的并行提倡很少使用synchronized的函数式编程风格,它关注数据分块而不是协调访问

Java 8也用Stream API(java.util.stream)解决了这两个问题:集合处理时的套路和晦涩,以及难以利用多核

Collection主要是为了存储和访问数据,而Stream则主要用 于描述对数据的计算。这里的关键点在于,Stream允许并提倡并行处理一个Stream中的元素。

Java中的并行与无共享可变状态

大家都说Java里面并行很难,而且和synchronized相关的玩意儿都容易出问题。那Java 8里面有什么“灵丹妙药”呢?事实上有两个。首先,库会负责分块,即把大的流分成几个小的流,以便并行处理。其次,流提供的这个几乎免费的并行,只有在传递给filter之类的库方法的方法不会互动(比方说有可变的共享对象)时才能工作。但是其实这个限制对于程序员来说挺自然的,举个例子,我们的Apple::isGreenApple就是这样。确实,虽然函数式编程中的函数的主要意思是“把函数作为一等值”,不过它也常常隐含着第二层意思,即“执行时在元素之间无互动”。

默认方法

Java 8中加入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。

接口如今可以包含实现类没有提供实现的方法签名 了!那谁来实现它呢?缺失的方法主体随接口提供了(因此就有了默认实现),而不是由实现类提供。

这就给接口设计者提供了一个扩充接口的方式,而不会破坏现有的代码。Java 8在接口声明 中使用新的default关键字来表示这一点。

eg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public interface Collection<E> extends Iterable<E> {
  default Stream<E> stream() {
          return StreamSupport.stream(spliterator(), false);
	}

  default Stream<E> parallelStream() {
          return StreamSupport.stream(spliterator(), true);
	}
	...
}

如果在好几个接口里有多个默认实现, 是否意味着Java中有了某种形式的多重继承?是的,在某种程度上是这样。我们在第9章中会谈 到,Java 8用一些限制来避免出现类似于C++中臭名昭著的菱形继承问题。

java 8中抽象类与接口的异同

相同点

  1. 都是抽象类型;
  2. 都可以有实现方法(以前接口不行);
  3. 都可以不需要实现类或者继承者去实现所有方法,(以前不行,现在接口中默认方法不需要实现者实现)

不同点

  • 抽象类不可以多重继承,接口可以(无论是多重类型继承还是多重行为继承);
  • 抽象类和接口所反映出的设计理念不同。其实抽象类表示的是"is-a"关系,接口表示的是"like-a"关系;
  • 接口中定义的变量默认是public static final 型,且必须给其初值,所以实现类中不能重新定义,也不能改变其值;抽象类中的变量默认是 friendly(不加修饰符) 型,其值可以在子类中重新定义,也可以重新赋值。

来自函数式编程的其他好思想

Optional<T>

常见的函数式语言,如SML、OCaml、Haskell,还提供了进一步的结构来帮助程序员。其中 之一就是通过使用更多的描述性数据类型来避免null。确实,计算机科学巨擘之一托尼·霍尔 (Tony Hoare)在2009年伦敦QCon上的一个演讲中说道:

我把它叫作我的“价值亿万美金的错误”。就是在1965年发明了空引用……我无法 抗拒放进一个空引用的诱惑,仅仅是因为它实现起来非常容易。

在Java 8里有一个Optional类,如果你能一致地使用它的话,就可以帮助你避免出现 NullPointer异常。它是一个容器对象,可以包含,也可以不包含一个值。Optional中有 方法来明确处理值不存在的情况,这样就可以避免NullPointer异常了。

(结构)模式匹配

这在数学中也有使用,例如: f(0) = 1 f(n) = n*f(n-1) otherwise

在Java中,你可以在这里写一个if-then-else语句或一个switch语句。其他语言表明,对 于更复杂的数据类型,模式匹配可以比if-then-else更简明地表达编程思想。

,Java 8对模式匹 配的支持并不完全,虽然我们会在第14章中介绍如何对其进行表达。与此同时,我们会用一个以 Scala语言(另一个使用JVM的类Java语言,启发了Java在一些方面的发展;请参阅第15章)表达 的例子加以描述。比方说,你要写一个程序对描述算术表达式的树做基本的简化。给定一个数据 类型Expr代表这样的表达式,在Scala里你可以写以下代码,把Expr分解给它的各个部分,然后 返回另一个Expr:

1
2
3
4
5
6
def simplifyExpression(expr: Expr): Expr = expr match {
	case BinOp("+", e, Number(0)) => e 
	case BinOp("*", e, Number(1)) => e 
	case BinOp("/", e, Number(1)) => e 
	case _ => expr 
}

这里,Scala的语法expr match就对应于Java中的switch (expr)。

模式匹配这个术语有两个意思, 这里我们指的是数学和函数式编程上所用的, 即函数是分情况定义的, 而不是使用 if-then-else。它的另一个意思类似于“在给定目录中找到所有类似于IMG*.JPG形式的文件”,和所谓的正则 表达式有关。

小结

以下是你应从本章中学到的关键概念。

  • 请记住语言生态系统的思想,以及语言面临的“要么改变,要么衰亡”的压力。虽然Java 可能现在非常有活力,但你可以回忆一下其他曾经也有活力但未能及时改进的语言的命 运,如COBOL。
  • Java 8中新增的核心内容提供了令人激动的新概念和功能,方便我们编写既有效又简洁的程序。
  • 现有的Java编程实践并不能很好地利用多核处理器。
  • 函数是一等值;记得方法如何作为函数式值来传递,还有Lambda是怎样写的。
  • Java 8中Streams的概念使得Collections的许多方面得以推广,让代码更为易读,并允许并行处理流元素。
  • 你可以在接口中使用默认方法,在实现类没有实现方法时提供方法内容。
  • 其他来自函数式编程的有趣思想,包括处理null和使用模式匹配。