6实战java高并发程序设计---Java 8/9/10与并发

2014年,Oracle发布了新版本Java 8。对于Java来说,这显然是一个具有里程碑意义的版本。它最主要的改进是增加了函数式编程的功能。就目前来说,Java最令人头痛的问题,也是受到最多质疑的地方,应该就是Java烦琐的语法。这样我们不得不花费大量的代码行数,来实现一些司空见惯的功能,以至于Java程序总是冗长的。但是,这一切将在Java 8的函数式编程中得到缓解。

严格来说,函数式编程与我们的主题并没有太大关系,我似乎不应该在这里提及它。但是,在Java 8中新增的一些与并行相关的API,却以函数式编程的范式出现,为了能让大家更好地理解这些功能,我会先简要地介绍一下Java 8中的函数式编程。


6.1 Java 8的函数式编程简介

6.1.1 函数作为一等公民

在理解函数作为一等公民这句话时,让我们先来看一种非常常用的互联网语言JavaScript,相信大家对它都不会陌生。JavaScript并不是严格意义上的函数式编程,不过,它也不是属于严格的面向对象编程。但是,如果你愿意,那么你既可以把它当作面向对象语言,也可以把它当作函数式语言,因此,把JavaScript称为多范式语言可能更加合适。6实战java高并发程序设计---Java 8/9/10与并发

注意这里each()函数的参数,这是一个匿名函数,在遍历所有的li节点时,会弹出li节点的文本内容。将函数作为参数传递给另外一个函数,这是函数式编程的特性之一。

6实战java高并发程序设计---Java 8/9/10与并发

这也是一段JavaScript代码,在这段代码中,注意函数f1的返回值,它返回了函数f2。在倒数第2行,返回f2函数并赋值给result,实际上,此时的result就是一个函数,并且指向f2。对result的调用,就会打印n的值。

一个函数可以作为另外一个函数的返回值,也是函数式编程的重要特点。

6.1.2 无副作用

函数的副作用指的是函数在调用过程中,除给出了返回值之外,还修改了函数外部的状态

比如,函数在调用过程中修改了某一个全局状态。函数式编程认为,函数的副用作应该被尽量避免

可以想象,如果一个函数肆意修改全局或者外部状态,当系统出现问题时,我们可能很难判断究竟是哪个函数引起的问题,这对于程序的调试和跟踪是没有好处的。如果函数都是显式函数,那么函数的执行显然不会受到外部或者全局信息的影响,因此,对于调试和排错是有益的。

注意:显式函数指函数与外界交换数据的唯一渠道就是参数和返回值,显式函数不会去读取或者修改函数的外部状态。与之相对的是隐式函数,隐式函数除参数和返回值外,还会读取外部信息,或者修改外部信息

6.1.3 声明式的(Declarative)

函数式编程是声明式的编程方式。命令式(Imperative)程序设计喜欢大量使用可变对象和指令。我们总是习惯于创建对象或者变量,并且修改它们的状态或值,或者喜欢提供一系列指令,要求程序执行。这种编程习惯在声明式的函数式编程中有所变化。对于声明式的编程范式,你不再需要提供明确的指令操作,所有的细节指令将会更好地被程序库封装,你要做的只是提出你的要求,声明你的用意即可

请看下面一段程序,这是一段传统的命令式编程,为了打印数组中的值,我们需要进行一个循环,并且每次需要判断循环是否结束。在循环体内,我们要明确地给出需要执行的语句和参数。

6实战java高并发程序设计---Java 8/9/10与并发

与之对应的声明式编程代码如下:

6实战java高并发程序设计---Java 8/9/10与并发

可以看到,变量数组的循环体居然消失了!println()函数似乎在这里也没有指定任何参数,在此,我们只是简单地声明了用意。有关循环及判断循环是否结束等操作都被简单地封装在程序库中。

6.1.4 不变的对象

在函数式编程中,几乎所有传递的对象都不会被轻易修改。请看以下代码:

6实战java高并发程序设计---Java 8/9/10与并发

6实战java高并发程序设计---Java 8/9/10与并发

代码第2行看似对每一个数组成员执行了加1的操作。但是在操作完成后,在最后一行打印arr数组所有的成员值时,你还是会发现,数组成员并没有变化!在使用函数式编程时,这种状态是一种常态,几乎所有的对象都拒绝被修改。这非常类似于不变模式。

6.1.5 易于并行

由于对象都处于不变的状态,因此函数式编程更加易于并行。实际上,你甚至完全不用担心线程安全的问题。我们之所以要关注线程安全,一个很重要的原因是当多个线程对同一个对象进行写操作时,容易将这个对象“写坏”。但是,由于对象是不变的,因此,在多线程环境下,也就没有必要进行任何同步操作了。这样有利于并行化,同时,在并行化后,由于没有同步和锁机制,其性能也会比较好。

6.1.6 更少的代码

通常情况下,函数式编程更加简明扼要

请看下面这个例子,对于数组中每一个成员,首先判断是否是奇数,如果是奇数,则执行加1,并最终打印数组内所有成员。

6实战java高并发程序设计---Java 8/9/10与并发

6实战java高并发程序设计---Java 8/9/10与并发


6.2 函数式编程基础

在正式进入函数式编程之前,有必要先了解一下Java 8为支持函数式编程所做的基础性的改进,这里将简要介绍一下FunctionalInterface注释、接口默认方法和方法句柄。

6.2.1 FunctionalInterface注释

Java 8提出了函数式接口的概念。所谓函数式接口,简单地说,就是只定义了单一抽象方法的接口。比如下面的定义:

6实战java高并发程序设计---Java 8/9/10与并发

注释FunctionalInterface用于表明IntHandler接口是一个函数式接口,该接口被定义为只包含一个抽象方法handle(),因此它符合函数式接口的定义。如果一个函数满足函数式接口的定义,那么即使不标注为@FunctionalInterface,编译器依然会把它看作函数式接口。这有点像@Override注释,如果你的函数符合重载的要求,无论你是否标注了@Override,编译器都会识别这个重载函数,但如果你进行了标注,而实际的代码不符合规范,那么就会得到一个编译错误的提示。

这里需要强调的是,函数式接口只能有一个抽象方法,而不是只能有一个方法

这里需要强调的是,函数式接口只能有一个抽象方法,而不是只能有一个方法。这分两点来说明:首先,在Java 8中,接口运行存在实例方法(参见下节的“接口默认方法”);其次,任何被java.lang.Object实现的方法,都不能视为抽象方法,因此NonFunc接口不是函数式接口,因为equals()方法在java.lang.Object中已经实现。

 

6实战java高并发程序设计---Java 8/9/10与并发

函数式接口的实例可以由方法引用或者lambda表达式进行构造,我们将在后面进一步举例说明。

6.2.2 接口默认方法(接口里面竟然可以定义的方法里面可以有方法体,实现类可以不重写接口里的默认方法 )

在Java 8之前的Java版本,接口只能包含抽象方法。但从Java 8开始,接口也可以包含若干个实例方法。这一改进使得Java 8拥有了类似于多继承的能力。一个对象实例,将拥有来自多个不同接口的实例方法。

6实战java高并发程序设计---Java 8/9/10与并发

6实战java高并发程序设计---Java 8/9/10与并发

注意上述代码中Mule实例同时拥有来自不同接口的实现方法,这在Java 8之前是做不到的。从某种程度上说,这种模式可以弥补Java单一继承的一些不便。但同时也要知道,它也将遇到和多继承相同的问题,如图6.2所示。如果IDonkey也存在一个默认的run()方法,那么同时实现它们的Mule就会不知所措,因为它不知道应该以哪个方法为准。

6实战java高并发程序设计---Java 8/9/10与并发

6实战java高并发程序设计---Java 8/9/10与并发

6实战java高并发程序设计---Java 8/9/10与并发

6.2.3 lambda表达式

lambda表达式可以说是函数式编程的核心。lambda表达式即匿名函数,它是一段没有函数名的函数体,可以作为参数直接传递给相关的调用者,lambda表达式极大地增强了Java语言的表达能力

6实战java高并发程序设计---Java 8/9/10与并发

和匿名对象一样,lambda表达式也可以访问外部的局部变量,如下所示:

6实战java高并发程序设计---Java 8/9/10与并发

上述代码可以编译通过,正常执行并输出6。与匿名内部对象一样,在这种情况下,外部的num变量必须声明为final,这样才能保证在lambda表达式中合法地访问它

奇妙的是,对于lambda表达式而言,即使去掉上述的final定义,程序依然可以编译通过!但千万不要以为这样你就可以修改num的值了。实际上,这只是Java 8做了一个小处理,它会自动地将在lambda表达式中使用的变量视为final

6.2.4 方法引用

方法引用是Java 8中提出的用来简化lambda表达式的一种手段。它通过类名和方法名来定位一个静态方法或者实例方法。

方法引用在Java 8中的使用非常灵活。总的来说,可以分为以下几种。

● 静态方法引用:ClassName::methodName。

● 实例上的实例方法引用:instanceReference::methodName。

● 超类上的实例方法引用:super::methodName。

● 类型上的实例方法引用:ClassName::methodName。

● 构造方法引用:Class::new。

● 数组构造方法引用:TypeName[]::new。

首先,方法引用使用“::”定义,“::”的前半部分表示类名或者实例名,后半部分表示方法名称。如果是构造函数,则使用new表示

6实战java高并发程序设计---Java 8/9/10与并发

对于第一个方法引用“User::getName”,表示User类的实例方法。在执行时,Java会自动识别流中的元素(这里指User实例)是作为调用目标还是调用方法的参数。在“User::getName”中,显然流内的元素都应该作为调用目标,因此实际上,在这里调用了每一个User对象实例的getName()方法,并将这些User的name作为一个新的流。同时,对于这里得到的所有name,使用方法引用System.out::println进行处理。这里的System.out为PrintStream对象实例,因此,这里表示System.out实例的println方法。系统也会自动判断,流内的元素此时应该作为方法的参数传入,而不是调用目标。

如果一个类中存在同名的实例方法和静态函数,那么编译器就会感到很困惑,因为此时,它不知道应该使用哪个方法进行调用。它既可以选择同名的实例方法,将流内元素作为调用目标,也可以使用静态方法,将流元素作为参数。

6实战java高并发程序设计---Java 8/9/10与并发


6.3 一步一步走入函数式编程