前言
2014年,Oracle发布了Java8新版本。这对java来说是一个里程碑式的版本。他最主要的改进就是增加了函数式编程的功能(为了解决java程序总是冗长的问题),或许会感到奇怪,函数式编程和并发似乎没什么关系,但是java中与并发相关的API的实现,却是以函数式编程的范式来实现的。所以为了更好的理解这些功能,需要先学习下函数式编程。
java8的函数式编程一些特点
函数作为一等公民
我理解的,一句话来总结就是函数可以作为另一个函数的参数或者返回值吧!。如下面一段Javascript代码:
function f1(){
var n=1;
function f2(){
alert(n);
}
return f2;
}
var result= f1();
result();//1
上述代码f1返回了f2并赋值给了result,此时result就是一个函数,指向f2,调用result就会打印n的值
无副作用
函数的副作用指的是函数除了返回值外,还修改了函数外部的状态,不如修改了一个全局变量。可以想象,这样当系统出现故障时,我们很难判断问题是由哪一个函数引起的,对调试和追踪是没有好处的。如果函数都是显式函数,那么函数的执行显然不会收到外部或者全局信息的干扰,有利于调试和追踪。所谓显式函数是指函数与外界交换数据的唯一渠道就是参数和返回值,显式函数不会读取或者修改函数的外部状态。而与之对应的隐式函数还会读取外部信息或者修改外部信息。
然而 实际上完全的无副作用是不可能是实现的,系统总是需要获取或者修改外部信息的,同时,模块之间的交互也极有可能是通过共享变量进行的。因此大多数的函数式编程都允许副作用的存在,如Clojure等,但是与面向对象相比,这种函数调用的副作用,在函数式编程里,需要进行一些有效的限制。
申明式的(Declarative)
函数式编程是申明式的的编程方式。相对与命令式而言,命令式的·程序者总是喜欢使用大量的 可变对象和指令。我们总是喜欢创建对象和变量,并且修改他们的状态或值,或者喜欢提供一系列指令要求程序执行。而在函数式编程中,那些细节的指令将会更好的被函数库所封装,我们只需要提出我们的要求,申明我们的用意即可。
下面,我们来看下两种方式的打印数组的代码:
//命令式
public static void imperative(){
int [] array={1,2,3,4};
for(int i=0;i<array.length;i++){
System.out.println("array[i]");
}
}
申明式
public static void declarative(){
int [] array={1,2,3,4};
Arrays.stream(array).foreach(System.out::println);
}
不变的对象
在函数式编程中几乎所有的对象都不会被轻易的i修改。下面我么看个例子来裂解:
static int [] ar={1,2,3};
Arrays.stream(ar).map((x)->x=x+1).foreach(System.out::println);
System.out.println();
Arrays.stream(ar).foreach(System.out::println);
代码第二行看起来对数组每个元素进行了加一操作,但通过最后一行打印元素时会发现,数组成员并没有发生变化。
在使用函数式编程的时候,这种状态几乎是一种常态,几乎所有的对象都拒绝被修改。这非常类似与不变模式。
易于并行
由于对象都处于不变状态,因此函数式编程更加易于并行。不需要同步,也没有锁机制,其性能也会比较好。
更少的代码
不难理解,函数式编程的范式更加紧凑而且简洁。
函数式编程的基础
FunctionalInterface注解
java8提出了函数式接口的定义。简单来说,函数式接口,就是之定义了单一抽象方法的接口。如下面的定义:
@FunctionalInterface
public static interface IntHandler{
void handle(int i);
}
注释@FunctionalInterface表示这是一个函数式接口,该注释与@Override注释类似,不管该方法是否标注了该注释,只要满足重载或者函数式接口的定义,编译器就会把它看做重载方法或者函数式接口。
需要注意的是,并不是函数式接口只能有一个方法,首先接口运行存在实例方法,其次,任何被java.lang.Object实现的方法,都不能视为抽象方法。
比如下面 也是一个标准的函数式接口:
@FunctionalInterface
public static interface IntHandler{
void handle(int i);
boolean equals(Object obj);
}
接口默认方法
在java8之前,接口只能包含抽象方法。java8之后,接口还可以包含若干个实例方法,是的java8有了类似多继承的能力。一个对象实例,将拥有来自不同接口的实例方法。
lambda表达式
lambda表达式可以说是函数式编程的核心。lambda表达式就是匿名函数,他是一段没有函数名的函数体,可以直接作为参数传递给调用者。
下面看一段lambda表达式的使用:
List<Integer> numbers=Arrays.aslist (1,2,3,4);
numbers.forEach((Integer value) -> System.out.println(value));
上述代码遍历的输出列表的元素。
和匿名对象一样,lambda表达式也可以访问外部的局部变量,如下:
final int num=2;
Function<Integer,Integer> stringConverter = (from) -> from*num;
//num++;
System.out.println(stringConverter.apply(3));
上述代码可以编译通过,输出结果6,与匿名内部对象一样,在这种情况外部的num变量必须申明为final,才能保证lambda表达式合法的访问它。
但是对lambda表达式而言,即使去掉final,程序依然会编译通过。但是我们就不能修改num的值了,java8会自动将lambda表达式中用到的变量视为final。
方法引用
方法引用是java8中提出的用来简化lambda表达式的一种手段。它通过类名和方法名来定位一个静态方法和实例方法。
方法引用在java8中使用的非常灵活。总的来说,分为以下几种:
- 静态方法引用:ClassName::methodName
- 实例上的实例方法引用:instanceReference::methodName
- 超类上的实例方法引用: super::methodName
- 类型上的实例方法引用: ClassName::methodName
- 构造方法引用: Class::new
- 数组构造方法引用: TypeName[]::new
总结来说 一般::前面表示类名或者实例名,后半部分表示方法名,构造函数用new表示。
应该容易理解,此处就不在用例子说明了。
java函数式编程的简单实践
下面我们以一个java8流的例子。来简单显示java函数式编程:
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.function.IntConsumer;
public class Funtionpr {
static int [] arr= {1,2,3,4};
/**
* @param args
*/
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
// for (int i:arr) {
// System.out.println(i);
// }
// Arrays.stream(arr).forEach(new IntConsumer() {
// @Override
// public void accept(int value) {
// System.out.println(value);
// }
// });
Arrays.stream(arr).forEach(( x)->System.out.println(x)
);
}
}