从面向过程出发到函数式编程(上篇)
小编
专注分享Java技术,坚持原创。你的关注和转发就是我们最好的动力。
前言
java8为我们引入的Lambda表达式、Stream ApI以及方法引用,它们为了java提供函数式编程的支持,虽然目前JDK已经出现到14的版本了,但是小编在工作中遇到一些员工中,对java的函数式编程并不有所了解,或者是不晓其原理。
今天我们就以一个对0~10间的累计为例子,从面向过程的编程方式开始,层层递进,演进到我们现在的函数式编程。
面向过程的累计
private static int sumInJava5(int size) {
int sum = 0;
// range方法是自定义的,获取1到10的集合
List<Integer> values = range(1,size);
for (Integer i : values) {
sum += i;
}
return sum;
}
这个例子使用到了jdk1.5的foreach语法,但其本身也是面向过程的,我们可以看出,加的操作是一步步的按照流程走,有先后之分。并不是通过调用对象的方法去进行累计。
说到这里,讲解一个疑惑,很多人只会用foreach语法,但是不知道集合和数组可以去使用foreach。其实是实现了Iterable的接口,提供了遍历的功能。
面向对象的累计
private static int sumInJava5Oop(int size, Sum sum) {
int result = 0;
// range方法是自定义的,获取1到10的集合
List<Integer> values = range(1,size);
for (Integer value : values) {
result = sum.apply(result, value);
}
return result;
}
private interface Sum {
/**
* 加法运算
* @param a 左值
* @param b 右值
* @return 返回值
*/
int apply(int a, int b);
}
private static class SumImpl implements Sum{
@Override
public int apply(int a, int b) {
return a + b;
}
}
这个累加是通过一个Sum类型的对象。调用它的apply方法实现的,是一个面向对象的过程。
在使用这个计算方法过程中,无论是通过传入实现类还是匿名内部类,代码是很繁琐的,jdk的开发者们为了解决这个问题,引入了Lambda表达式、Stream ApI以及方法引用。
使用传统的方式与方法引用调用我们的方法
// 这是传统的
sum = sumInJava5Oop(10, new SumImpl());
// 这是方法引用
sum = sumInJava5Oop(10, SumImpl::apply);
我们的第二行代码就是我们方法引用形式,使用到的操作符“::”,这个操作符把方法引用分成两边,左边是类名或者某个对象的引用,右边是方法名。
值得注意的是:由于类名引用需要方法是静态的,因此,java8提供了接口可以有静态方法的特性,在此之前的版本是没有的。
此外使用方法引用有一个重要的特点,虽然它作为我们方法的参数,但是并不需要是相同的类型和相同的方法名,只要保证相同的签名就行(方法返回值和方法的参数),如下面几种方式:
// 可以是实例对象的引用
sum = sumInJava5Oop(10, new SumImpl()::apply);
// 可以是完全不同类型对象方法名字
sum = sumInJava5Oop(10, OopDemo::calculate);
方法引用的格式
方法引用有以下四种类型
引用静态方法 ContainingClass::staticMethodName 。
引用某个对象的实例方法 containingObject::instanceMethodName
引用某个类型的任意对象的实例方法 ContainingType::methodName
引用构造方法 ClassName::new
这些都是java8提供新的编程语法支持的,它只要让编译器知道我们是方法参数、返回值以及在方法里面需要做什么就行。看到这里相信有不少人引发了另一个特性,没错就是我们的lambda表达式。
lambda表达式调用我们的累计方法
因为方法引用仅是一个方法实现的应用,在没有lambda表达式表达式的时候,我们选要在其他的地方编写我们的方法,而有了lambda表达式表达式以后,只需要在调用的位置写表达式就行:
sumInJava5Oop(10, (a,b) -> a+b);
注意:lambda表达式的主体仅包含一个表达式,且该表达式仅调用了一个已经存在的方法。方法引用的通用特性,方法引用所使用方法的入参和返回值与lambda表达式实现的函数式接口的入参和返回值一致。lambda可以看作是方法的实现。
函数式接口
我们可以发现,如果我们的Sum接口存在多个抽象方法,那么就不能用过lambda和方法引用进行传参操作。这是新语法提供高灵敏度的同时,也提供与之相应的保障。显然这种接口比较特殊,又有一个新的概念——函数式接口。
函数式接口的定义:
一个接口有且只有一个抽象方法。
函数式接口的实例可以通过 lambda 表达式、方法引用或者构造方法引用来创建。
创建函数式接口注意事项:
如果我们在某个接口上声明了 @FunctionalInterface 注解,那么编译器就会按照函数式接口的定义来要求该接口。
如果某个接口只有一个抽象方法,但我们并没有给该接口声明 @FunctionalInterface 注解,那么编译器依旧会将该接口看作是函数式接口。
重写 Object 类里的方法不会导致函数式接口失效。
函数式接口可以有多个方法
虽然函数式接口的定义要求了我们一个接口有且只有一个抽象方法。但是java有提供接口的默认方法实现(default-method)来解决这个问题。
@FunctionalInterface
private interface Sum {
// 抽象方法
int apply(int a, int b);
// default-method
default int xx(int a, int b){
return a*b;
};
// Object的方法
String toString();
}
尽管Sum接口有3个方法,但是方法引用和lambda表达式走的还是我们的抽象方法。
使用default-method注意事项
类实现接口方法,即如果接口声明了 default 方法,并且某类实现了该接口,那么 default 方法将会被继承。
如果有一个类继承了两个不同接口的同名 default 方法,jvm 编译器是无法识别到底该使用哪个方法的,必须重写 default 方法。
如果子类继承父类并实现接口,实现类的优先级比接口高。
专注分享Java技术,跟我一起学习吧
长按识别二维码关注