原创文章, 转载请私信. 订阅号 tastejava 学习加思考, 仔细品味java之美
Java开发者一般都习惯面向对象编程, 实际项目中函数式编程出现频率也不太高, 要理解函数式编程首先要理解一些前置概念, 我来总结一下Java中的函数式编程, 如果为其他人节约了时间, 减轻了学习成本, 那就太好了.
什么是函数式编程
函数式编程是一种编程范式, 允许将函数作为参数传递给目标方法, 目标方法也可以返回一个函数.(将方法实现延后到调用方法传递参数的时刻, 让参数或者返回结果包含逻辑)
Js中应用函数式编程
Javascript中函数式编程应用很广泛, 由于js中函数本身也是一种变量, 所以js中很轻松就能实现将方法当做参数传递到方法中, 配合箭头函数修复this指向问题, js中可以很轻松的应用函数式编程. Promise风格的Http库Axios中经常会传递方法res => {}到then()中去处理请求到的数据.
约定
为了方便描述, 我来定义几个名词
参数方法: 将方法当做参数传递给另一个方法
目标方法: 要调用的方法
结果方法: 将方法作为结果返回
JDK8以前匿名内部类实现函数式编程
从函数式编程的含义可以了解到, 一种开发语言要支持函数式编程最首要的问题就是让方法可以当做参数来传递. 即有能力做到将参数方法传递给目标方法, 目标方法内执行参数方法, 目标方法执行完毕后返回一个结果方法
在Js中方法可以赋值给变量, 自然方法可以当做实参传递, Java中虽然有Method来描述一个方法对象, 但是方法本身并不是一个Method类型的变量, 只不过Method对象包含着目标方法的有用信息.
由于方法在Java中不是一个变量对象. 所以我们在JDK8之前要把参数方法传递到目标方法中, 只能先定义一个包含参数方法的接口, 然后构造匿名内部类对象当做目标方法的参数.具体代码实现如下:
/**
* Author: GaoZl
* Date: 2019/11/11
* Time: 18:31
* Description: 先定义一个接口, 接口中包含要当做参数的方法
*/
public interface NormalInterface {
/**
* 将两个参数相加并返回
* @param num1 数字1
* @param num2 数字2
* @return 返回参数之和
*/
int add(int num1, int num2);
}
/**
1. Author: GaoZl
2. Date: 2019/11/11
3. Time: 18:33
4. Description: 利用匿名内部类实现函数式编程
*/
@Slf4j
public class TestNormalInterface {
/**
* 目标方法, 接收参数方法并执行
* @param normalInterface
* @param num1
* @param num2
*/
private void addAndPrint(NormalInterface normalInterface, int num1, int num2) {
int result = normalInterface.add(num1, num2);
log.info("两数之和为{}", result);
}
/**
* 利用匿名内部类实现函数式编程
*/
@Test
public void testFunctionalProgrammingWidthEnclosingClass() {
this.addAndPrint(new NormalInterface() {
@Override
public int add(int num1, int num2) {
return num1 + num2;
}
}, 1, 2);
}
}
JDK8及更高版本实现函数式编程
JDK8以前实现函数式编程有一些缺陷
- 匿名内部类参数方法比较繁琐冗余
- 已有的方法实现无法很方便的当做参数方法
- Lambda表达式方式也要预先定义函数式接口, 函数式接口参数相同可以复用, 是一种冗余
在JDK8中支持的Lambda表达式解决了第一个缺陷, 方法引用解决了第二个缺陷, JDK内置的通用的函数式接口弥补了第三个缺陷. 虽然在这样的条件下应用函数式编程还是没有Js中便捷(还是需要定义参数方法对应的接口, 静态类型语言甜蜜的包袱 😃 ), 但是已经很强大了.
Lambda表达式
Lambda表达式的Java实现如下:
// 一个接收两个字符串参数, 并在方法体中操作参数的Lambda表达式
(str1, str2) -> {
log.info("我有一个{}", str1);
log.info("你有一个{}", str2);
log.info("我们既有{}又有{}", str1, str2);
log.info("What a virtue to share!");
}
Lambda与函数式接口(FunctionalInterface)
Java中的Lambda表达式语法资料有很多, 不再赘述. 网络上大多数博文有一个缺点, 它们的Lambda表达式真的只是介绍Lambda表达式的语法在这里我来补充一下Lambda表达式的定义部分.
上方的Lambda表达式代码只是Lambda表达式变量值, 那么变量的类型或者变量的定义在哪呢, 在Java中, Lambda表达式的定义/类型是对应的函数式接口(FunctionalInterface)
函数式接口和注解@FunctionalInterface
函数式接口定义: 满足只有一个抽象方法的接口, 就是函数式接口
@FunctionalInterface注解表名一个接口是函数式接口, 为了兼容低版本JDK, 这个注解不是必须的, 也就是说没有标明此注解的接口在满足定义时也是函数式接口.
想详细了解这个注解可以查看源代码, 源代码中注释很全面和清晰, 此处总结几句关键的注释
① 函数式接口就是严格只有一个抽象方法, 可以有其他default方法或者static方法的接口
② 如果接口标记了此注解, 定义要满足类型是接口, 满足函数式定义, 否则编译不通过
③ 函数式接口实例可以通过Lambda表达式, 方法引用或者构造器引用的方式创建
④ 如果一个接口满足函数式接口定义(第①点), 那么编译器会将其视为函数式接口
下面我们定义一个函数式接口:
/**
* Author: GaoZl
* Date: 2019/11/11
* Time: 17:34
* Description: 函数式接口示例
*/
// 注解@FunctionalInterface显式说明此接口是函数式接口, 不满足函数式接口定义将会编译失败
@FunctionalInterface
public interface MyFunctionalInterface {
// Lambda表达式对应的方法定义, 用于当做参数方法
void shareItWithYou(String mine, String yours);
// 函数式接口中允许存在default方法
// 此方法用于组合多个函数式接口实例
default MyFunctionalInterface shareWidthAThirdPerson(MyFunctionalInterface after) {
// 此方法本身不操作参数, 返回一个结果方法 (shareWidthYou方法)
return (mine, yours) -> {
// 结果方法被调用时会先调用参数方法
shareItWithYou(mine, yours);
// 然后调用后置方法, 类似先执行 A, 再执行 B, 再执行 C 的效果
after.shareItWithYou(mine, yours);
};
}
// 默认逻辑, 相当于shareWidthYou的默认实现 例如框架中提供的函数式接口默认实现
// 通过方法引用此方法, 引用的类型也是MyFunctionalInterface, 类似接口中不但能实现自己的接口方法, 还能多实现
static void defaultMethod(String s, String s1) {
System.out.println("我来自默认实现defaultMethod方法, 我们所有人一起拥有" + s + "和" + s1);
}
}
从代码中可以看到, 注解@FunctionalInterface是为了避免编码错误, 明确提供编译器级别的定义约束, 虽然不是必须注解, 但是明确要创建一个函数式接口那就应该加上此注解.
函数式接口四种实例化方式与执行细节
从@FunctionalInterface注解源码注释中可以看到, 函数式接口可以通过Lambda表达式, 方法引用或者构造方法引用实例化, 除此之外还可以通过显式的匿名内部类实现.下面代码演示这四种方式实例化函数式接口, 先准备如下类, 用于演示引用构造方法实例化函数式接口.
/**
* Author: GaoZl
* Date: 2019/11/12
* Time: 14:10
* Description: 拥有两个字符串的构造方法, 与函数式接口MyFunctionalInterface抽象方法形参一致
*/
@Slf4j
public class TestConstructorReference {
public TestConstructorReference(String mine, String yours) {
log.info("来自一个普通类的构造方法, 与函数式接口参数恰好一致, 可以被引用成函数式接口实例");
log.info("接收到参数{}和{}", mine, yours);
log.info("调用时用相应函数式接口的方法签名, 实际执行的是引用普通方法或引用构造方法的逻辑");
}
}
具体演示和说明代码如下:
/**
* Author: GaoZl
* Date: 2019/11/11
* Time: 17:38
* Description: 函数式接口四种实例化方式以及执行细节
*/
@Slf4j
public class TestFunctionalInterface {
@Test
public void testFunctionalInterface() {
// Lambda表达式方式创建函数式接口实例
MyFunctionalInterface functionalOne = (str1, str2) -> {
log.info("我有一个{}", str1);
log.info("你有一个{}", str2);
log.info("我们既有{}又有{}", str1, str2);
log.info("What a virtue to share!");
};
// 匿名内部类方式创建函数式接口实例
MyFunctionalInterface functionalTwo = new MyFunctionalInterface() {
@Override
public void shareItWithYou(String mine, String yours) {
log.info("现在第三人也有{}和{}啦", mine, yours);
}
};
// 引用静态方法方式创建函数式接口实例
MyFunctionalInterface functionalThree = MyFunctionalInterface::defaultMethod;
// 引用构造犯法创建函数式接口实例
MyFunctionalInterface functionalFour = TestConstructorReference::new;
// 第一个实例发起调用, 利用shareWidthThirdPerson方法组合第二个第三个实例
// shareWidthThirdPerson组合方法主要逻辑是返回新的结果方法, 结果方法主要逻辑
// 是先调用第一个实例方法, 然后再把要组合的实例放在其后调用, 类似责任链式调用
// 执行顺序为functionalOne -> functionalTwo -> functionalThree各自的shareWidthYou方法
functionalOne.shareWidthAThirdPerson(functionalTwo)
.shareWidthAThirdPerson(functionalThree)
.shareWidthAThirdPerson(functionalFour)
.shareItWithYou("apple", "banana");
}
}
至此我们就理清了Java中Lambda表达式和函数式接口的关系.下面我们来看一下常见的函数式接口
常见的函数式接口与函数式编程
由于Java是强类型语言, 也就意味着虽然Lambda表达式的逻辑可以在传递参数时实现, 但是其定义也就是函数式接口, 必须提前定义. 各个函数式接口间最大的区别是唯一的抽象方法形参列表不同, Java为此提供了一批通用的函数式接口, 用于辅助开发者应用函数式编程.
JDK1.8之前的函数式接口
从前面函数式接口的定义可以知道, 即使没有@FunctionalInterface注解时, 接口满足函数式接口定义, 那么在JDK1.8以及更高版本的编译器下就会将其视为函数式接口, 在接收函数式接口实例作为参数的方法就可以应用Lambda表达式, JDK1.8之前就已经提供了几个常见的函数式接口.下面三个常见的JDK1.8以前的函数式接口在1.8版本中已经用@FunctionalInterface注解修饰.其中最早一个函数式接口Runnable从JDK1.0版本就已经提供了.
- Runnable (since 1.0)
- Callable (since 1.5)
- Comparator (since 1.2)
与Comparator同样是1.2版本提供的接口Comparable也符合函数式接口定义, 不过在1.8版本中并没有加入注解修饰, 从用法上来看Comparator可以自定义逻辑当做参数传递给排序方法, 而Comparable接口用于被对象实现, 表明对象可以被比较, 单独接收Comparable的Lambda表达式实例没有意义.所以虽然Comparable接口符合函数式接口定义, 从编译器角度也会被看作函数式接口, 但是并没有实用性
JDK1.8内置的通用函数式接口
JDK内置的函数式接口, 五大类
Consumer 消费者类型的函数式接口, 接收参数无返回值.
- Consumer<T> 接收一个参数输入, 无返回值
- BiConsumer<T,U> 接收两个参数, 无返回值
- DoubleConsumer 接收一个Double参数, 无返回值
- IntConsumer 接收一个Integer类型参数, 无返回值
- LongConsumer 接收一个Long类型参数, 无返回值
- ObjDoubleConsumer<T> 接收一个对象和一个Double参数, 无返回值
- ObjIntConsumer<T> 接收一个对象和一个Integer参数, 无返回值
- ObjLongConsumer<T> 接收一个对象和一个Long类型参数, 无返回值
Supplier 供应者类型的函数式接口, 无参数, 有返回值
- Supplier<T> 无参数, 返回一个结果
- BooleanSupplier 无参数, 返回一个Boolean值
- DoubleSupplier 无参数, 返回一个Double值
- IntSupplier 无参数, 返回一个Integer值
- LongSupplier 无参数, 返回一个Long类型值
Predicate 断言类型的函数式接口, 接收输入参数, 返回Boolean类型, 进行是否断言
- Predicate<T> 接收一个输入参数, 返回一个Boolean结果
- BiPredicate<T,U> 接收两个参数, 返回一个Boolean结果
- DoublePredicate 接收一个Double参数, 返回一个Boolean结果
- IntPredicate 接收一个Integer参数, 返回一个Boolean结果
- LongPredicate 接收一个Long参数, 返回一个Boolean结果
Function 描述方法类型的函数式接口
- Function<T,R> 接收一个参数T, 返回结果R
- BiFunction<T,U,R> 接收两个参数, 返回一个结果
- DoubleFunction<R> 接收一个Double参数, 返回一个结果
- DoubleToIntFunction 接收一个Double参数, 返回一个Integer结果
- DoubleToLongFunction 接收一个Double参数, 返回一个Long结果
- IntFunction<R> 接收一个Integer参数, 返回一个结果
- IntToDoubleFunction 接收一个Integer参数, 返回一个Double结果
- IntToLongFunction 接收一个Integer参数, 返回一个Long结果
- LongFunction<R> 接收一个Long参数, 返回一个结果
- LongToDoubleFunction 接收一个Long参数, 返回一个Double结果
- LongToIntFunction 接受一个Long参数返回一个Integer结果
- ToDoubleBiFunction<T,U> 接收两个参数, 返回一个Double结果
- ToDoubleFunction<T> 接收一个参数, 返回一个Double结果
- ToIntBiFunction<T,U> 接收两个参数, 返回一个Integer结果
- ToIntFunction<T> 接收一个参数, 返回一个Integer结果
- ToLongBiFunction<T,U> 接收两个参数, 返回一个Long结果
- ToLongFunction<T> 接收一个参数, 返回一个Long结果
Operator 操作符类型的函数式接口
- UnaryOperator<T> 一元操作符, 接收参数T, 返回结果T
- LongUnaryOperator 一元操作符, 接收一个Long, 返回一个Long
- IntUnaryOperator 一元操作符, 接收一个Integer, 返回一个Integer
- DoubleUnaryOperator 一元操作符, 接收一个Double, 返回一个Double
- BinaryOperator<T> 二元操作符, 接收两个同类型操作符, 返回类型也为同类型操作符
- DoubleBinaryOperator 二元操作符, 接收两个Double, 返回一个Double
- IntBinaryOperator 二元操作符, 接收两个Integer, 返回Integer
- LongBinaryOperator 二元操作符, 接收两个Long, 返回一个Long
总结
网络上大多数资料比较分散, 单纯的讲Lambda表达式语法, 想在Java开发中应用函数式编程, 重要的是理解函数式接口与Lambda表达式的关系. 要了解函数式编程概念, 函数式接口概念, Lambda表达式概念, 再了解一些JDK内置的函数式接口, 就能比较顺畅的使用函数式编程啦