Java 函数式编程: 接口, Lambda表达式与方法引用
一. 什么是函数式编程?
函数式编程是一种编程方式, 它将电脑运算视为函数的计算, 这意味着函数在编程过程中处于首要地位, 相对于指令任何实现包括流程控制都可以是函数. 其主要思想是把运算过程尽量写成函数间的嵌套. 相较于 if
, else
, while
这样的过程控制指令, 函数式编程更强调函数的计算, 也更侧重于帮助程序员把重点放在要做的事情上, 很少会(不是不会)涉及到不做什么. 而指令集更偏向于对工作的流程的描述, 处理过程应该是怎样的流程, 碰到了其他一种或多种情况要怎么办. 其实质区别在于函数式更关注结果的达成, 而指令式变成更偏向于对实现的过程的清晰描述, 当然两者一般都能完成所要达成的目标. 两者不能说谁更好, 都是有效的工作流程的集合方式, 各有优劣.
二. Java 中函数式编程的实现
Java 自 1.8 版本开始支持函数式编程, 其主要的实现分为两个方面:
- 一方面增加了一些函数式接口(
@FunctionalInterface
), 这应该是为了兼容既有的面向对象编程的体系 - 另一方面实现了自己的 lambda 表达式语法支持, 保证了函数式编程的便捷性 (即, 不需要为每个函数式接口实现具体的类再调用).
三. 常见的函数式编程接口
函数式编程接口常见的有以下几种, 其主要差异在于, 参数数量, 是否返回结果, 以及返回结果的类型.
1. Function 接口
Function 是种接受单个参数并返回结果的函数式接口.
一个典型的 Function
的实例化示例如下:
FunctionstrLen = new Function<>() { @Override public Integer apply(String t) { return t.length(); } }; System.out.println("Function 示例, 字符串长度: " + strLen.apply("Hello"));
其中 String
泛型表示输入参数的类型, 表示 Function
接口接受 String
类型的参数, 而 Integer
泛型是返回结果的类型.
而 apply
就是 Function
接口的主体函数方法, 也就是要使 Function
有效参与函数式编程的必须实现的方法.
有关 Function
更详细的用法, 请参照下表及其中的链接:
序号 | 类型 | 名称 | 说明 |
---|---|---|---|
1 | 方法 | Function.andThen() |
生成一个组合函数, 该函数首先将当前 |
2 | 方法 | Function.apply() |
使用提供的参数应用函数并返回结果 |
3 | 方法 | Function.compose() |
生成一个组合函数, 该函数首先将 before 函数应用于当前函数的输入, 然后将当前函数应用于结果. |
4 | 方法 | Function.identity() |
生成一个总是返回输入的函数 |
2. Consumer 接口
Consumer 是种接受单个参数但不返回结果的函数式接口.
一个典型的 Consumer
实例化示例如下:
ConsumersayHello = new Consumer<>() { @Override public void accept(String t) { System.out.println("Consumer 示例: 你好, " + t); } }; sayHello.accept("21yi");
其中 String
泛型表示输入参数的类型, 表示 Consumer
接口接受 String
类型的参数.
而 accept
是 Consumer
接口的主体函数方法.
有关 Consumer
更详细的用法, 请参照下表及其中的链接:
序号 | 类型 | 名称 | 说明 |
---|---|---|---|
1 | 方法 | Consumer.accept() |
用给定参数执行该 |
2 | 方法 | Consumer.andThen() |
|
3. Supplier 接口
Supplier 是种无参数但返回结果的函数式接口.
一个典型的 Supplier
实例化示例如下:
SupplierrandSupplier = new Supplier<>() { @Override public Integer get() { return (int) (Math.random() * 100); } }; System.out.println("Supplier 示例, 1-100随机数: " + randSupplier.get());
其中 String
泛型表示返回结果的类型.
而 get
是 Supplier
接口的主体函数方法.
有关 Supplier
更详细的用法, 请参照下表及其中的链接:
序号 | 类型 | 名称 | 说明 |
---|---|---|---|
1 | 方法 | Supplier.get() |
获取生成器的结果 |
4. Predicate 接口
Predicate 是种接受单个参数的函数式接口, 所谓断言就是判断是否, 说明该函数式接口返回布尔值.
一个典型的 Predicate
实例化示例如下:
PredicateisDoubleMax = new Predicate<>() { @Override public boolean test(Double d) { return d.equals(Double.MAX_VALUE); } }; System.out.println("Predicate 示例, 是否 Double 的最大值? " + isDoubleMax.test(Double.MAX_VALUE));
其中 Double
泛型表示输入参数的类型, 表示 Predicate
接口接受 Double
类型的参数, 而返回结果是个布尔值.
而 test
就是 Predicate
接口的主体函数方法.
有关 Predicate
更详细的用法, 请参照下表及其中的链接:
序号 | 类型 | 名称 | 说明 |
---|---|---|---|
1 | 方法 | Predicate.and() |
生成一个组合断言, 表示该断言和另一个断言的短路逻辑与(AND). |
2 | 方法 | Predicate.isEqual() |
生成一个断言, 该断言根据 |
3 | 方法 | Predicate.negate() |
获取一个否定当前断言的断言 |
4 | 方法 | Predicate.or() |
生成一个组合断言, 表示该断言和另一个断言的短路逻辑或(OR). |
5 | 方法 | Predicate.test() |
用指定参数执行断言 |
5. UnaryOperator 接口
UnaryOperator 是种一元运算符函数接口. 所谓一元运算是指输入和返回的一元性, 详细说来就是对单个参数输入进行运算并返回与输入相同类型的结果.
一个典型的 UnaryOperator
实例化示例如下:
UnaryOperatormaskTel = new UnaryOperator<>() { @Override public String apply(String tel) { if(tel.length() < 8) { return tel; } return tel.substring(0, 2) + "****" + tel.substring(6, tel.length()); } }; System.out.println("UnaryOperator 示例, 掩盖电话: " + maskTel.apply("1234567890"));
其中 String
泛型表示输入参数的类型, 表示 Function
接口接受 String
类型的参数, 也是返回结果的类型.
而 apply
就是 UnaryOperator
接口的主体函数方法.
有关 UnaryOperator
更详细的用法, 请参照下表及其中的链接:
序号 | 类型 | 名称 | 说明 |
---|---|---|---|
1 | 方法 | UnaryOperator.identity() |
获取一个始终返回其输入参数的一元运算符 |
6. BiConsumer 接口
BiConsumer 是种接受两个输入参数但不返回结果的函数式接口.
一个典型的 BiConsumer
实例化示例如下:
BiConsumerhello = new BiConsumer<>() { @Override public void accept(String name, Integer age) { System.out.println("你好, 我叫" + name + ", 今年 " + age + " 岁了."); } }; System.out.print("BiConsumer 示例"); hello.accept("21yi", 1);
其中 String
泛型表示输入的第1个参数的类型, Integer
泛型表示输入的第2个参数的类型.
而 accept
就是 BiConsumer
接口的主体函数方法.
有关 BiConsumer
更详细的用法, 请参照下表及其中的链接:
序号 | 类型 | 名称 | 说明 |
---|---|---|---|
1 | 方法 | BiConsumer.accept() |
用给定参数执行该 |
2 | 方法 | BiConsumer.andThen() |
|
7. BiFunction 接口
BiFunction 是种接受两个参数并返回结果的函数式接口.
一个典型的 BiFunction
实例化示例如下:
BiFunctionexpectLength = new BiFunction<>() { @Override public Boolean apply(String t, Integer u) { return t.length() == u; } }; System.out.print("BiFunction 示例, 字符串长度是否符合预期? " + expectLength.apply("21yi", 4));
其中 String
泛型表示输入的第1个参数的类型, Integer
泛型表示输入的第2个参数的类型. Boolean
泛型则表示返回的类型.
而 apply
就是 BiFunction
接口的主体函数方法.
有关 BiFunction
更详细的用法, 请参照下表及其中的链接:
序号 | 类型 | 名称 | 说明 |
---|---|---|---|
1 | 方法 | BiFunction.andThen() |
生成一个组合函数, 该函数首先将当前 |
2 | 方法 | BiFunction.apply() |
使用提供的两个参数应用到函数并返回结果 |
四. Java 中 lambda 表达式的实现
在上面的示例中, 我们用到的始终是实例化函数化接口的方式来实现函数式编程, 显然这种方式过于"啰嗦", 无法体现函数式编程的优势. Java 既然支持了函数式编程, 自然也会考虑到这点. Java中实际通过两种方式简化了这个过程:
- 通过 lambda 表达式简化
- 通过 '
::
' 实现对方法的"引用"
1. 通过 lambda 表达式简化
在 Java 中, 可以通过类似以下 lambda 表达式来实现各种函数式接口
(arg) -> { return xxx; }
或
(arg) -> { do something; }
其中圆括号用于指定参数, 如果没有参数则可以直接使用 () 表示, 而花括号用于计算或返回计算结果.
假定我们有一个方法需要接受 Function
作为参数, 并输出 Function
的执行结果:
private static void applyFunction(Functionfunc, String p) { System.out.println("Function 的执行结果: " + func.apply(p)); }
首先我们显然可以为将一个 Function
示例作为参数传递到该方法中, 同时我们还可以通过 lambda 表达式来实现该过程, 示例如下:
applyFunction( (s) -> {return s.length();}, "21yi");
在该示例中, (s) -> {return s.length();}
表达式就实现了 Function 接口.
其中 s
是参数, 其类型符合定义的要求: String
, 花括号中则返回了计算的结果.
2. 通过 '::
' 符号实现对方法的 "引用"
如果方法本身已经实现了我们的要求, 再用 lambda 表达式再包次装显然是一种"冗余"写法, 因此 Java 中提供了 '::
' 方式对方法的引用, 所谓引用, 不是调用而是先放在这等需要计算的时候再进行计算.
一个比较简单的引用就是对于输出的引用, 示例如下:
private static void acceptConsumer(Consumerconsumer, String p) { consumer.accept(p); } acceptConsumer(System.out::println, "Hello, 21yi");
其中方法 acceptConsumer()
接受一个传递 String
参数的 Consumer
且不返回结果. 符合这种场景最常见的方法就是 System.out.println()
, 然而, 我们无法在这里调用, 因此我们采用了引用的方式 System.out::println
, 同时我们也能注意到, 引用是不需要圆括号的. 这样就不需要 (s) -> {System.out::println(s)}
这样"啰嗦"的写法了, 虽然这样的表达式也没有语法上的错误.
以上就是 Java 的函数式编程的全部内容了. 最后附上本文中完整的示例代码:
package com.yi21.course; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.function.UnaryOperator; public class Yi21FunctionalProgramming { public static void main(String[] args) { FunctionstrLen = new Function<>() { @Override public Integer apply(String t) { return t.length(); } }; System.out.println("Function 示例, 字符串长度: " + strLen.apply("Hello")); Consumer sayHello = new Consumer<>() { @Override public void accept(String t) { System.out.println("Consumer 示例: 你好, " + t); } }; sayHello.accept("21yi"); Supplier randSupplier = new Supplier<>() { @Override public Integer get() { return (int) (Math.random() * 100); } }; System.out.println("Supplier 示例, 1-100随机数: " + randSupplier.get()); Predicate isDoubleMax = new Predicate<>() { @Override public boolean test(Double d) { return d.equals(Double.MAX_VALUE); } }; System.out.println("Predicate 示例, 是否 Double 的最大值? " + isDoubleMax.test(Double.MAX_VALUE)); UnaryOperator maskTel = new UnaryOperator<>() { @Override public String apply(String tel) { if(tel.length() < 8) { return tel; } return tel.substring(0, 2) + "****" + tel.substring(6, tel.length()); } }; System.out.println("UnaryOperator 示例, 掩盖电话: " + maskTel.apply("1234567890")); BiConsumer hello = new BiConsumer<>() { @Override public void accept(String name, Integer age) { System.out.println("你好, 我叫" + name + ", 今年 " + age + " 岁了."); } }; System.out.print("BiConsumer 示例"); hello.accept("21yi", 1); BiFunction expectLength = new BiFunction<>() { @Override public Boolean apply(String t, Integer u) { return t.length() == u; } }; System.out.println("BiFunction 示例, 字符串长度是否符合预期? " + expectLength.apply("21yi", 4)); applyFunction( (s) -> {return s.length();}, "21yi"); acceptConsumer(System.out::println, "Hello, 21yi"); } private static void applyFunction(Function func, String p) { System.out.println("Function 的执行结果: " + func.apply(p)); } private static void acceptConsumer(Consumer consumer, String p) { consumer.accept(p); } }