> 文档中心 > JAVA 新特性的入场券 - 函数式接口

JAVA 新特性的入场券 - 函数式接口


从 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)。

我们可以发现,范畴包含两个部分:

  • 是让不同实体互相转化的“态射”集合

  • 可以从某一实体“变形”的实体集合

我们不难发现,范畴中的“态射”便是我们所关心的“函数”概念。

范畴中的函数是为了表达数学运算方式,所以本质上是进行求解的数学方法。而函数式编程则是在计算机中通过函数接口的方式描述实体间的转化,从而获得范畴中的的另一个“变形”,显而易见:

函数式编程实际上是用编程的方式在对象间进行的一种数学运算。

那么我们就可以理解关于函数式接口的很多涉及,举几个例子的话:

  1. 函数式接口中之应有一个方法。因为是为了表示一个映射关系。

  2. 函数式接口方法中不有额外副作用。函数只进行实体的转换,而非其他其他应用逻辑。

函数式接口

由于函数是用来描述一次实体的转变的,所以 函数式接口中只有一个抽象方法 。但由于 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 我认为可以算是 Function 的一种特例变形,可以认为是 Function。而单独的进行封装是为了进行语义增强。其中源码上的说明也是如此:

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 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 consumer = System.out::println; consumer.accept("静态方法引用 - 我是一个消费者");    }    public static void function(){ System.out.println("-- Test for function --"); Function 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 supplier = FunctionDemo::new; System.out.println(supplier.get()); final Function show = FunctionDemo::show; System.out.println(show.apply(new FunctionDemo()));    }    @Override    public String toString() { return "构造函数方法引用 - 对象打印了自己";    }}

复制代码

这个例子的执行结果如下:

-- Test for predicate --predicate test 6 : truepredicate test -1 : falseintPredicate test 6 : trueintPredicate test -1 : false-- Test for consumer --静态方法引用 - 我是一个消费者-- Test for function --Function 新数字为:46实例方法引用 - IntUnaryOperator - 新数字为:46-- Test for supplier --构造函数方法引用 - 对象打印了自己类方法引用--提供了信息。

复制代码

这个例子分别展示了

  1. 几个基本函数式接口的使用方法

  2. int、long、double 的基础数据类型函数式接口的用法

  3. 函数式接口方法引用方法

其中如同日志中描述的一样,函数式接口方法引用方法有 4 种:

  • 静态方法引用

  • 非静态 实例方法引用

  • 非静态 类方法引用

  • 构造函数方法引用

最后

Java8 中的函数式编程是一种数学思想的程序化,而函数接口则是具体的执行单位。函数式接口以 Consumer、Supplier、Function 三个借口为核心进行功能扩展,以满足不同场景的便捷使用。

特别的,本文没有介绍函数式接口中的 andThen 、 compose 等方法,后续遇到咱们再续。