从Java8的“新”特性说起
说到Java中的函数式编程,就不得不说到Java8中引入的lambda表达式、stream API等特性。它们与函数式接口一起支撑起了Java的函数式编程。
函数式编程有较高的可读性与更好扩展性。而也正因为此,在后续版本Java的各种API中,充斥着各种通过函数式接口进行能力扩展的功能。所以,可以说理解函数式接口(编程)是进行后续高级特性学习的第一步。
本文将从概念上引入函数式接口的意义,以及着重针对Java8新提供函数式接口进行功能汇总,并不对lambda表达式与stream API做过多的展开。
数学概念-范畴(Category Theory)
数据概念太过晦涩,但是可以帮助你理解一些问题出现的原因与原理。可以说函数式编程的数学起源就是范畴概念。
一句话总结的话:范畴就是使用“箭头”连接的“物体”。
物体表示的是可以转化的实体,而箭头是物体的互相转化“关系”。举个例子来说,如果A可以通过*3
变换转化为B,而B可以通过/2
变成C,如:
A —*3—> B —/2—>C
A、B、C与两个箭头共同构成了一个范畴,所以我们可以说1、3、1.5与2、6、3与4、12、6与…他们是一个范畴。而A、B、C间箭头的这种转化关系的学名被称为"态射"(morphism)。而在范畴的概念中,B与C因为都是可以从A通过一个或者多个态射转化而来的,所以认为他们是A的不同状态的"变形"(transformation)。
我们可以发现,范畴包含两个部分:
- 是让不同实体互相转化的“态射”集合
- 可以从某一实体“变形”的实体集合
我们不难发现,范畴中的“态射”便是我们所关心的“函数”概念。
范畴中的函数是为了表达数学运算方式,所以本质上是进行求解的数学方法。而函数式编程则是在计算机中通过函数接口的方式描述实体间的转化,从而获得范畴中的的另一个“变形”,显而易见:
函数式编程实际上是用编程的方式在对象间进行的一种数学运算。
那么我们就可以理解关于函数式接口的很多涉及,举几个例子的话:
- 函数式接口中之应有一个方法。因为是为了表示一个映射关系。
- 函数式接口方法中不有额外副作用。函数只进行实体的转换,而非其他其他应用逻辑。
函数式接口
由于函数是用来描述一次实体的转变的,所以函数式接口中只有一个抽象方法。但由于Java的继承关系,这个”只有一个“的概念实际是是排除了Object的相关方法的。
满足这个条件的就可以作为函数式接口进行使用,但为了后续的开发导致歧义,你可以用@FunctionInterface
注解标记到接口上,用于表明这个接口只应该有一个抽象方法,如果不满足这个条件,则这个问题会在编译的时候就暴露出来。
尽管Java的函数式编程是在Java8才支持的,但是之前的版本中就有很多函数接口,其中常用的有:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
这些接口本身就满足上述条件,同时在Java8中也为这些接口加上了@FunctionInterface
注解特别标示。
Java8新增的函数式接口都有啥
在Java8中为了支持Lambda表达式与函数式编程,特别新增了一批函数式接口,他们的包路径为:
java.util.function
该包路径下一共43个类,我将它划分为以下几类:
- 基础类型4种
- 入参扩展3种
- 出入类型相同省略2种
- 基础类型扩展34种
基础类型
基础类型定义有以下几种:
接口 | 入参 | 返回 | 说明 |
---|---|---|---|
Predicate | T | boolean | 断言 |
Consumer | T | - | 消费一个数据 |
Function<T,R> | T | R | 从T映射到R |
Supplier | - | T | 提供一个数据 |
之所以说是基本定义,是因为其他的定义都是围绕在这些概念的基础上进行扩展的。
其中 Predicate 我认为可以算是Function的一种特例变形,可以认为是Function<T,Boolean>。而单独的进行封装是为了进行语义增强。其中源码上的说明也是如此:
Represents a predicate (boolean-valued function) of one argument.
那么你会发现,剩下来的三种基础类型Supplier、Function、Consumer,所对应了一个范畴实体的开始、范畴实体与实体的态射、范畴实体的结束。
入参扩展
入参扩展就是将具有入参的基本类型的参数个数扩展为了两个:
-
BiConsumer
-
BiFunction
-
BiPredicate
原则上,多参数的扩展是可以利用“科尔化”来处理的,但是由于两个参数的使用场景实在是太多了,比如处理Map相关的内容,所以特别的将两个入参的封装为了单独的接口。
出入类型相同省略
出入类型相同省略是对,Function与BiFunction的一种特殊的省略。由于在数据处理的时候存在大量使用相同数据类型进行处理的情况,例如: reduce操作。所以特别地提供了入参与出参相同的接口:
-
UnaryOperator(单个入参)
-
BinaryOperator(两个入参)
出入参数类型相同,则可以简化泛型定义的过程。
##基础类型扩展
基础类型扩展主要是针对常用的基础类型int、long、double类型进行了接口定义,三种类型各11个,以Int为例子:
- IntBinaryOperator
- IntConsumer
- IntFunction
- IntPredicate
- IntSupplier
- IntUnaryOperator
- ObjIntConsumer
- IntToDoubleFunction
- IntToLongFunction
- ToIntBiFunction
- ToIntFunction
可以看到这11个接口又可以分为三种:
- 入参推定,对于一种入参类型的接口,提供类型为
int
的接口。是Int开头的接口(不包含IntTo)。特别的,ObjIntConsumer是一个入参为int
的BiConsumer。 - 类型转换的Function,为了向其他基础类型进行转换的Function。是IntTo开头的接口
- 出参推定,对于出参的接口,提供类型为
int
的接口。是ToInt开头的接口。
除此之外为了boolean类型单独提供了BooleanSupplier接口。
基础类型扩展主要是避免在处理常用类型的函数式编程或者流编程的时候产生频繁的包装类转换。所以单独提供了一组接口,用于提高性能。
小例子
我们在这里写一个包含主要基本类型的小例子:
public class FunctionDemo {
public int calculate(int num ){
return num*2;
}
public String show(){
return "类方法引用--提供了信息。";
}
public static void main(String[] args) {
predicate();
consumer();
function();
supplier();
}
public static void predicate() {
System.out.println("-- Test for predicate --");
Predicate<Integer> predicate = i -> i > 0;
System.out.println("predicate test 6 : " + predicate.test(6));
System.out.println("predicate test -1 : " + predicate.test(-1));
IntPredicate intPredicate = i -> i > 0;
System.out.println("intPredicate test 6 : " + intPredicate.test(6));
System.out.println("intPredicate test -1 : " + intPredicate.test(-1));
}
public static void consumer(){
System.out.println("-- Test for consumer --");
Consumer<String> consumer = System.out::println;
consumer.accept("静态方法引用 - 我是一个消费者");
}
public static void function(){
System.out.println("-- Test for function --");
Function<Integer,Integer> function = x -> x*2;
System.out.println("Function 新数字为:" + function.apply(23));
FunctionDemo demo = new FunctionDemo();
IntUnaryOperator intFunction = demo::calculate;
System.out.println("实例方法引用 - IntUnaryOperator - 新数字为:" + intFunction.applyAsInt(23));
}
public static void supplier(){
System.out.println("-- Test for supplier --");
Supplier<FunctionDemo> supplier = FunctionDemo::new;
System.out.println(supplier.get());
final Function<FunctionDemo, String> show = FunctionDemo::show;
System.out.println(show.apply(new FunctionDemo()));
}
@Override
public String toString() {
return "构造函数方法引用 - 对象打印了自己";
}
}
这个例子的执行结果如下:
-- Test for predicate --
predicate test 6 : true
predicate test -1 : false
intPredicate test 6 : true
intPredicate test -1 : false
-- Test for consumer --
静态方法引用 - 我是一个消费者
-- Test for function --
Function 新数字为:46
实例方法引用 - IntUnaryOperator - 新数字为:46
-- Test for supplier --
构造函数方法引用 - 对象打印了自己
类方法引用--提供了信息。
这个例子分别展示了
- 几个基本函数式接口的使用方法
- int、long、double的基础数据类型函数式接口的用法
- 函数式接口方法引用方法
其中如同日志中描述的一样,函数式接口方法引用方法有4种:
- 静态方法引用
- 非静态 实例方法引用
- 非静态 类方法引用
- 构造函数方法引用
最后
Java8中的函数式编程是一种数学思想的程序化,而函数接口则是具体的执行单位。函数式接口以Consumer、Supplier、Function三个借口为核心进行功能扩展,以满足不同场景的便捷使用。
特别的,本文没有介绍函数式接口中的andThen
、compose
等方法,后续遇到咱们再续。