Java 8 中的函数式编程
函数式编程的核心特点是,函数作为一段功能代码,可以像变量一样进行引用和传递,以便在有需要的时候进行调用。
函数类型与 @FunctionalInterface
Java 对函数式编程的支持,本质是通过接口机制来实现的。首先定义一个仅声明一个方法的接口,然后对接口冠以 @FunctionalInterface
注解,那么这个接口就可以作为「函数类型」,可以接收一段以 Lambda 表达式,或者方法引用予以承载的逻辑代码。
被 @FunctionalInterface
标注的接口仅声明一个方法,一旦这个函数定义好,它能执行的功能是确定的,就是调用和不调用的区别。显然,这个方法可以用来代表「函数类型」所能执行的功能,接口中声明的方法就是和函数体定义一一对应的。
@FunctionalInterface
下只能声明一个未实现的方法,多一个、少一个都不能编译通过。但有以下例外:
- 覆写
Object
中toString/equals
的方法不受此个数限制。 default
方法和static
方法因为带有实现体,所以也不受此限制。
@FunctionalInterface
注解不是必须的,不加这个注解的接口(前提是只包含一个方法)一样可以作为函数类型。不过,显而易见的是,加了这个注解表意更明确、更直观,是更被推荐的做法。
JDK 提供的函数类型
java.util.function
包下预定义了常用的函数类型,包括:
1 |
|
这些个定义,都是在参数个数(0、1、2 个)和有无返回值上做文章。另外还有一些将泛型类型具体化的衍生接口,比如 Predicate
、LongSupplier
等等。
1 |
|
Lambda 表达式
Lambda 表达式写法
遍历集合除了 站内文章基本的方式 以外,还可以使用 Lambda 表达式。Lambda 表达式又称函数式表达,是 Java 中典型的语法糖。
Lambda 表达式语法:
1 | (Type parameter, ... ) ->{ statements; } |
parameters
是参数列表, { statements; }
是 Lambda 表达式的主体。
简化写法:
- 参数简化:
parameters
如果只有一个参数,可以省略小括号()
;如果没有参数,也需要空括号()
。parameters
可以省略参数类型。
- 语句简化:
- 无返回值:
statements;
只包含一条语句,无返回值,可省略大括号{}
、分号;
- 有返回值:
- 一般写法:
(int x)->{return x*x;}
- 普通的表达式:
(int i,int j)->(i*j+2)
- 可省略括号:
x->x*x
- ❌错误写法:
x->return x*x
- 一般写法:
- 无返回值:
Lambda 表达式代替匿名内部类
@FunctionalInterface
声明了函数类型,Lambda 表达式就用来定义函数类型的实现体。在介绍 Lambda 简化原理之前,首先回忆以下关于 Java 接口的知识。
Java 的 interface
接口有以下特点:
- 接口不能被实例化,只能通过它的实现类来实现。
- 不允许创建接口的实例,但允许定义接口类型的引用变量,该变量引用实现了这个接口类的实例。如:
Photographable t=new Camera();
,其中Photographable
为接口类型,Camera
为实现了这个接口的类。 - 可以使用匿名内部类创建接口类型的引用变量。匿名内部类中必须实现接口的所有方法。
1 | // Flyable 为接口类型 |
在没有 Lambda 之前,有些方法要求传入接口类型的参数。我们需要先用匿名内部类创建接口类型的引用变量,再把它传进去。现在 Java 8 中可以通过 Lambda 语法糖快速实现一个单方法的匿名内部类。
更详细地说,Lambda 表达式只能赋值给声明为函数式接口的 Java 类型的变量(注解 @FunctionalInterface
)。下文例子中的 Runnable
、Consumer
、Comparator
都是被 @FunctionalInterface
标注的函数式接口,因此可以接受 Lambda 表达式。
例子 1:线程的创建
例子 2:foreach
Java 集合都实现了 java.util.Iterable
接口。forEach()
方法有一个 Consumer
接口类型的 action
参数,它包含了对集合中每个元素的具体操作行为。action
参数所引用的 Consumer
实例必须实现 Consumer
接口的 accept(T t)
方法,在该方法中指定对参数 t
所执行的具体操作。
1 | // Java Iterable 接口源代码 |
示例:
1 | public static void main(String[] args) { |
以上 Lambda 表达式相当于创建了一个 Consumer
类型的匿名对象,并实现了 Consumer
接口的 accept(T t)
方法,此处传给 accept(T t)
方法的参数为 Person
对象。在 Lambda 表达式中符号 ->
后的可执行语句块相当于 accept(T t)
方法的方法体。
例子 3:排序
详看文章:站内文章Java 中的排序。
例 4:与 Stream API 连用
Collection
接口的 stream()
方法返回一个 Stream
对象,程序可以通过这个 Stream
对象操纵集合中的元素。
Lambda 表达式可操纵的变量作用域
this
关键字实际上引用的是外部类的实例。
在 Lambda 表达式中访问的局部变量必须符合以下两个条件之一:
- 条件一:最终局部变量,即用
final
修饰的局部变量。 - 条件二:实际上的最终局部变量,即虽然没有被
final
修饰,但在程序中不会改变局部变量的值。
方法引用
在编译器能根据上下文来推断 Lambda 表达式的参数的场合,可以在 Lambda 表达式中省略参数,直接通过 ::
符号来引用方法。
方法引用就是对一个类中已经存在的方法加以引用,分以下类型:
- 引用类的静态方法
- 对类构造方法的引用,如
ClassName::new
。 - 对类静态方法的引用,如
ClassName::staticMethodName
。编译器会根据上下文推断到底用哪个对象的实例方法。
- 对类构造方法的引用,如
- 引用实例方法
objectName::instanceMethod
,或者new Test()::instanceMethod
ClassName::instanceMethod
。编译器会根据上下文推断到底用哪个对象的实例方法。
示例:
1 | x -> new BigDecimal(x) // 等同于 BigDecimal::new |