本文不讲解JDK8函数式编程的具体API,而是解释函数式编程的概念。但是弄懂原理是学习API的前提,纲举目张才能事半功倍。
众所周知,JDK8引入了函数式编程。什么是函数式编程呢?为何需要函数式编程呢?
认知决定高度。首先函数式编程是与面向对象编程一个层级的概念。
任何Java程序员都不可能不知道面向对象编程OOP。OOP的口号是“万物皆对象”。什么是对象呢?就是现实中一个东西在编程领域的投射。对象有属性,有方法。属性表示数据,方法表示行为。对象可以用来表示任何事物,非常强大。既然如此,为何又需要函数式编程呢?
什么是函数
函数式编程来自数学。有点(gaozhong)数学功底的你一定不会忘记这个东东:
数学中的函数表示的是什么呢?其实就是一个计算过程。编程领域也是一样的, 函数表示了一个计算过程,比如,加减乘除,取余等等。就是我们java的方法的作用。
你肯定要问,那java中有方法了啊,为何又弄个什么函数式编程出来呢?问题在于,java中的方法是“二等公民”,它只能依附于对象存在,而不能独立存在。
JS中的函数与Java中的方法比较
这方面我们可以把java与js放在一起比较。在js中,函数是一等公民,你可以直接声明函数,使用函数。如下例所示。
//将函数定义为变量
var add = function(a,b){
return a+b;
}
var minus = function(a,b){
return a-b;
}
//定义一个计算函数,第一个参数是个函数,注意我们已经开始传递函数了
function calc(fn,a,b){
return fn(a,b);//调用函数fn,将a,b传入
}
//调用calc
//传入add函数作为第一个参数
var r1 = calc(add,10,5);
console.log("r1:"+r1)
//传入minus函数作为第一个参数
var r2 = calc(minus,10,5);
console.log("r2:"+r2)
在这里我们也看到,函数的好处是可以封装一段算法,一个计算过程,一个行为。这是对象无法做到的。但是好处却很显然,calc的第一个参数是个函数,在不改变calc方法的前提下,可以非常容易的扩展出各种算法。只需要提供不同函数实现即可。因为函数封装了算法。
这在java中是无法直接实现的。必须绕个弯。我们需要将函数放在一个接口中。代码如下:
package com.woniuxy.test;
/**
* 表示一个计算接口,这个接口存在的唯一用处就是存放calc方法
*/
public interface Fn {
//计算函数的抽象方法
double calc(double a,double b);
}
//实现加法
package com.woniuxy.test;
public class Add implements Fn {
@Override
public double calc(double a, double b) {
return a+b;
}
}
//实现减法
package com.woniuxy.test;
public class Minus implements Fn {
@Override
public double calc(double a, double b) {
return a-b;
}
}
//计算器类
package com.woniuxy.test;
public class Calculator {
/**
* 接受Fn接口作为参数,根据多态性,Fn可以传入Add和Minus
*/
public double calc(Fn fn, double a, double b){
return fn.calc(a,b);
}
}
最后我们来组装一下,代码与上面的js代码就非常接近了。
//调用测试
package com.woniuxy.test;
public class App {
public static void main(String[] args) {
//声明函数实例
Fn add = new Add();
Fn minus = new Minus();
Calculator c = new Calculator();
//调用
double r1 = c.calc(add,10,5);
System.out.println("r1:"+r1);
double r2 = c.calc(minus,10,5);
System.out.println("r2:"+r2);
}
}
我们来看,同样的计算逻辑,为何js为java要简单呢?因为java是oop的,基于对象。所以必须现有Fn接口才能有calc方法、必须现有Add类才能有calc方法的实现。方法必须依赖于对象。而JavaScript中函数是“一等公民”,可以直接创建。
引入函数式编程
为了简化这类代码,JDK8引入了函数式编程,终于Java也可以直接使用函数啦。
这里我们先给出BiFunction 这个接口,这个接口表示一个接受两个参数的函数,有三个泛型分别表示第一个参数的类型和第二个参数的类型以及返回值类型。所以可以直接用BiFunction来表示我们的这里的加减乘除的函数。
//修改Calculator.java,接受BiFunction作为第一个参数
public double calc2(BiFunction<Double,Double,Double> fn,double a,double b){
return fn.apply(a,b);
}
//不再创建Fn、Add和Minus了,直接用BiFunction表示操作
public class App {
public static void main(String[] args) {
//直接用BiFunction封装加法运算
BiFunction<Double,Double,Double> add =new BiFunction<Double, Double, Double>() {
@Override
public Double apply(Double a, Double b) {
return a+b;
}
};
//直接用BiFunction封装减法运算
BiFunction<Double,Double,Double> minus =new BiFunction<Double, Double, Double>() {
@Override
public Double apply(Double a, Double b) {
return a-b;
}
};
}
}
显然一下子少了3个类,比刚才简单了一些,但是 BiFunction<Double,Double,Double>
仍然看着头大呀。有简化的办法吗?
引入lambda表达式
是了,就是jdk8引入的lambda表达式。先看效果
//直接用BiFunction封装加法运算
BiFunction<Double,Double,Double> add = (a, b) -> a+b;
//直接用BiFunction封装减法运算
BiFunction<Double,Double,Double> minus = (a, b) -> a-b;
Calculator c = new Calculator();
//传入函数
double r1 = c.calc2(add,10,5);
System.out.println("r1:"+r1);
double r2 = c.calc2(minus,10,5);
System.out.println("r2:"+r2);
No Magic
代码是清晰了,但是你可能没弄明白是怎么回事。
BiFunction<Double,Double,Double> add =new BiFunction<Double, Double, Double>() {
@Override
public Double apply(Double a, Double b) {
return a+b;
}
};
//被转换成了
BiFunction<Double,Double,Double> add = (a, b) -> a+b;
这是怎么做到的呢?
道理其实很简单。BiFunction接口只有一个方法apply,而BiFunction存在的意义就是为这个方法提供载体。换言之,我们使用BiFunction接口就是奔着apply方法去的。既然如此,为何不直接把那个方法表示出来呢?jdk就提供了一种简洁的表示法,称为lambda表达式,直接表示出了接口里的方法。
反过来想一下,因为接口只有一个方法,所以非常明确的定位到这个方法。这里就是apply方法。并不会混淆。但是为何a,b参数都没有类型了呢?当然是为了简化代码,但其实是参数类型可以进行推断出来的。我们通过反射能够得到BiFunction的泛型参数,根据约定就可以知道a和b的类型了。有了这些约定,jdk可以获取到足够的信息,自动将lambda表达式转换为匿名内部类。
到这里,我们了解到JDK函数式编程的优势了。同样的封装计算过程(Add、Minus),传统的方式和函数式编程的差别是非常大的。
一些实例
事已至此,我们不如来看几个例子,感受下函数式编程的惊人魅力。请留意,lambda表达式都是在封装算法。
package com.woniuxy.examples;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Sorting {
private static List<Person> personList = Arrays.asList(new Person(20,"Dido")
,new Person(15,"Guava")
,new Person(30,"Alina")
,new Person(28,"Crack")
);
public static void main(String[] args) {
//0)封装sqrt,虽然有点多此一举
Function<Double,Double> sqrt = (number)->Math.sqrt(number);
System.out.println(sqrt.apply(9.0));;
//1)封装排序算法
//按年龄排序
Collections.sort(personList,(p1,p2)-> p1.getAge()-p2.getAge());
System.out.println(personList);
//按姓名排序
Collections.sort(personList,(p1,p2)-> p1.getName().compareTo(p2.getName()));
System.out.println(personList);
//2)封装选择算法
//选出18岁及以上的人
List<Person> adultList = personList.stream()//这里用到了List的StreamAPI
.filter((person -> person.getAge()>=18))//lambda表达式
.collect(Collectors.toList());
System.out.println(adultList);
//3)封装Runnable里的run方法,妈妈再也不用担心我写Runnable累死了
new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}).start();
}
}
class Person{
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
//...省略setter、getter
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
小结
JDK8函数式编程提供了一种直接封装函数的方式,即提供了一系列预定义的Function接口,提供了封装函数所需的功能。并通过lambda表达式简化了函数的编写方式。