提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
为什么要函数式编程
函数式编程(Functional Programming)在Java中的引入,主要带来了以下好处:
① 代码简洁和易读
② 并发编程的支持
③ 提高代码的可重用性
④ 易于测试和调试
⑤ 表达力更强
一、函数对象的优势
在Java中,函数式对象(Functional Object)通常是指实现了特定接口的对象,这些接口定义了一个或多个抽象方法,并且这些抽象方法通常接受参数并返回结果。这些接口经常用于实现函数式编程范式,即把函数作为一等公民(First-class Citizen)使用,允许函数作为参数传递,返回函数作为结果,以及把函数存储在变量中。
1.行为参数化
想象一下这样的场景,我们需要筛选所有男性学生。
public static void main(String[] args) {
List<Student> students = List.of(
new Student("张无忌", 18, "男"),
new Student("杨不悔", 16, "女"),
new Student("周芷若", 19, "女"),
new Student("宋青书", 20, "男")
);
/*
需求1:筛选男性学生
*/
System.out.println(filter(students));
}
static List<Student> filter(List<Student> students) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (student.sex.equals("男")) {
result.add(student);
}
}
return result;
}
这时我们还有一个新需求,要求筛选所有年龄小于18岁的学生。那么我们就需要定义一个新方法。
static List<Student> filter2(List<Student> students) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (student.age < 18) {
result.add(student);
}
}
return result;
}
那么能否提高代码的通用性,将这两个方法整合成一个方法呢?
上述两个方法分别对应两个规则。
规则1:筛选男性学生。
规则2:筛选年龄小于18岁的学生。
我们可以用函数来表示规则,同时我们希望规则并不是固定死的,而是灵活变通的(后续可能要实现规则3、规则4…),因此我们可以用函数对象来表示规则。
函数对象的表现形式为 参数 -> 逻辑部分
那么我们可以用函数对象来表示以上两条规则。
student -> student.sex.equals("男")
student -> student.age < 18
我们可以定义一个接口,利用接口中的抽象方法,间接的执行函数中的判断逻辑。
规则1与规则2的函数对象都是接收一个student类的对象作为参数,返回一个布尔值结果。因此抽象方法的输入参数为一个Student对象,返回值为布尔值,表示结果的真或假。
interface Lambda {
boolean test(Student student);
}
之后重新定义一个新的filter0方法,作为公共方法。由于规则不固定,函数对象要作为一个参数从外部传入方法中。
static List<Student> filter0(List<Student> students, '函数对象') {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if ('函数对象') {
result.add(student);
}
}
return result;
}
函数对象的类型为上文定义的Lambda,if语句中调用接口中的方法,从而执行判断逻辑。
static List<Student> filter0(List<Student> students, Lambda lambda) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (lambda.test(student)) {
result.add(student);
}
}
return result;
}
我们可以将之前定义的函数对象传入到filter0方法中。
// 函数对象
student -> student.sex.equals("男")
student -> student.age < 18
// main方法
public static void main(String[] args) {
List<Student> students = List.of(
new Student("张无忌", 18, "男"),
new Student("杨不悔", 16, "女"),
new Student("周芷若", 19, "女"),
new Student("宋青书", 20, "男")
);
/*
需求1:筛选男性学生
*/
System.out.println(filter0(students, student -> student.sex.equals("男")));
/*
需求2:筛选18岁以下学生
*/
System.out.println(filter0(students, student -> student.age < 18));
}
通过这种方式,未来需求中判断逻辑发生改变,我们可以通过这种方式,根据需求重新定义规则后,新规则作为参数从外部传递进方法即可,而filter0的代码不需要任何修改。这种优势叫做行为参数化,将判断逻辑进行参数化,提高方法的通用性。
2.延迟执行
延迟执行优势的体现以日志隔离作为案例,首先定义日志隔离级别和输出格式。
static Logger init(Level level) {
ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory.newConfigurationBuilder()
.setStatusLevel(Level.ERROR)
.setConfigurationName("BuilderTest");
AppenderComponentBuilder appender =
builder.newAppender("Stdout", "CONSOLE")
.addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT)
.add(builder.newLayout("PatternLayout").addAttribute("pattern", "%d [%t] %-5level: %msg%n%throwable"));
builder.add(appender)
.add(builder.newRootLogger(level).add(builder.newAppenderRef("Stdout")));
Configurator.initialize(builder.build());
return LogManager.getLogger();
}
logger.debug方法在日志级别为DEBUG的级别时才会调用expensive方法执行耗时操作后返回需要记录的日志。
static Logger logger = init(Level.DEBUG);
static String expensive() {
System.out.println("执行耗时操作...");
return "日志";
}
public static void main(String[] args) {
logger.debug("{}", expensive()); // 函数对象使得 expensive 延迟执行
}
但是如果日志级别为INFO时,理论上debug日志不会生成,但是expensive方法中的耗时操作仍然会被执行。
static Logger logger = init(Level.INFO);
为了避免这种情况,我们可以选择添加if语句的方式判断是否为DEBUG日志级别,但是代码十分冗余。
我们可以选择将expensive方法输出的String字符串修改为函数对象。
static Logger logger = init(Level.INFO);
static String expensive() {
System.out.println("执行耗时操作...");
return "日志";
}
public static void main(String[] args) {
logger.debug("{}", () -> expensive()); // 函数对象使得 expensive 延迟执行
}
日志隔离级别为INFO时,logger.debug方法不会再调用expensive方法,从而不再执行耗时操作。
如果修改日志隔离级别为DEBUG,再调用logger.debug方法
static Logger logger = init(Level.DEBUG);
logger.debug方法会调用expensive方法并打印语句:“执行耗时操作…”
main函数中如果不采用函数式编程的方式,直接调用expensive方法,无论事务隔离级别是什么,都会打印语句:“执行耗时操作…”
public static void main(String[] args) {
logger.debug("{}", expensive()); // expensive() 立刻执行
}
原因如下
查看logger中的debug方法的具体实现logIfEnabled方法
if (this.isEnabled(level, marker, message)) {
this.logMessage(fqcn, level, marker, message, paramSuppliers);
}
它的作用为进行日志级别的检查,如果满足了日志级别的条件才会进行记录。只用在if条件满足之后才会执行logMessage逻辑,调用paramSuppliers中的get方法间接的执行函数对象expensive()中的逻辑。
3.函数对象的表现形式
3.1 Lambda表达式
Lambda表达式的语法格式如下所示。
/**
* 由参数部分、箭头符号、逻辑运算三部分组成。
* 参数部分:int a,int b
* 箭头符号 ->
* 逻辑运算 a + b
*/
(int a, int b) -> a + b;
// 当逻辑运算中有多条语句时,要使用 {} 符号。并且不能省略结果的return关键字。
(int a, int b) -> {int c = a + b; return c;}
一个Lambda表达式视为一个函数对象,若上下文代码可以推导出参数部分的类型时,可以省略参数部分中的类型说明。
interface Lambda1 {
int op(int x, int y);
}
Lambda1 lambda = (a, b) -> a + b;
若Lambda表达式中只有一个参数,参数部分的 ( ) 可以省略。
int a -> a;
3.2 方法引用
方法引用的语法格式如下所示。
// 类名::该类中的静态方法名
Math::max
// 与之对应的Lambda表达式为
(int a, int b) -> Math.max(a, b);
// 类名::该类中的非静态方法名
Student::getName
(Student stu) -> stu.getName();
// 类名::该类中的非静态方法名
System.out::println
// 由于println方法没有返回结果,则该函数对象也没有返回结果
(Object obj) -> System.out.println(obj);
// 使用new关键字调用构造方法创建学生对象,这个学生对象为该函数对象的返回结果
Student::new
() -> new Student();
二、函数接口
注解用于在编译期间检查函数式接口是否满足接口中有且仅有一个抽象方法。
@FunctionalInterface
interface Lambda {
int op(int a);
}
public static void main(String[] args){
Lambda lambda = a -> a;
}
我们可以对返回值定义为泛型T,用于代表各种各样的类型,提高扩展性。
@FunctionalInterface
interface Type1<T> {
T op(int a);
}
@FunctionalInterface
interface Type2<T,R> {
T op(R a);
}
public static void main(String[] args){
Type1<Student> obj1 = () -> new Student();
Type1<List<Student>> obj2 = () -> new ArrayList<Student>();
Type2<Integer,Student> obj2 = (Student s) -> s.getAge();
// <Integer,Student>中已经注明返回类型为Integer,输入类型为Student
// 因此上式等价于Type2<Integer,Student> obj2 = s -> s.getAge();
}
JDK中也提供了一系列方法简化了接口的编写。
public static void main(String[] args){
// 返回值为boolean类型,参数为int类型
IntPredicate obj1 = a -> (a & 1) == 0;
IntPredicate obj2 = a -> BigInteger.valueOf(a).isProbablePrime(100);
// 返回值为int类型,参数为两个int类型
IntBinaryOperator obj4 = (a, b) -> a - b;
IntBinaryOperator obj5 = (a, b) -> a * b;
// 返回值为泛型,参数为空
Supplier<Student> obj3 = () -> new Student();
Supplier<List<Student>> obj4 = () -> new ArrayList<Student>();
// 返回值为泛型,参数为泛型
Function<Student, String> obj5 = s -> s.getName();
Function<Student, Integer> obj6 = s -> s.getAge();
}
常见的JDK中的函数接口如下所示。
Runnable
() -> void
Callable
() -> T
Comparator
(T,T) -> int
Consumer, BiConsumer, IntConsumer, LongConsumer, DoubleConsumer
(T) -> void(返回值为void<=>没有返回值), Bi是两参, Int、Long、Double 指参数是 int、long、double
Function, BiFunction, Int Long Double ...(返回类型可以与参数类型不同)
(T) -> R(返回类型可以与参数类型不同), Bi是两参, Int、Long、Double 指参数是 int、long、double
Predicate, BiPredicate, Int Long Double ...(用于条件判断)
(T) -> boolean, Bi是两参, Int、Long、Double 指参数是 int、long、double
Supplier, Int Long Double ...
() -> T, Int、Long、Double 指返回值是 int、long、double
UnaryOperator, BinaryOperator, Int Long Double ...
(T) -> T(返回类型必须与参数类型相同), Unary 一参, Binary 两参, Int、Long、Double 指参数是 int、long、double
三、方法引用
什么是方法引用?
方法引用是将现有的方法调用转换为函数对象。
编号 | 格式 | 特点 | 备注 |
---|---|---|---|
1 | 类名::静态方法 | 函数对象的参数与静态方法中的参数保持一致 | – |
2 | 类名::非静态方法 | 函数对象的参数要多一个该类对象,用对象调用非静态方法 | – |
3 | 对象::非静态方法 | 函数对象的参数与静态方法中的参数一致(该类对象已被提供) | – |
4 | 类名::new | 函数对象的参数与构造方法中的参数保持一致 | – |
5 | this::非静态方法 | – | 3特例,很少用 |
6 | super::非静态方法 | – | 3特例,很少用 |
① 类名::静态方法
逻辑:执行这个类的静态方法。
参数:这个静态方法的参数。
// 静态方法 Lambda表达式与方法引用
(String s) -> Integer.parseInt(s)
Integer::parseInt
案例
public static void main(String[] args) {
/*
需求:挑选出所有男性学生
*/
Stream.of(
new Student("张无忌", "男"),
new Student("周芷若", "女"),
new Student("宋青书", "男")
)
// .filter(stu -> stu.sex().equals("男")) // 过滤操作:lambda 表达式方式
.filter(MethodRef1::isMale) // 过滤操作:静态方法引用方式
// .forEach(stu -> System.out.println(stu)); // 打印操作:lambda 表达式方式
.forEach(MethodRef1::abc); // 打印操作:静态方法引用方式
}
public static boolean isMale(Student stu) {
return stu.sex().equals("男");
}
public static void abc(Student stu) {
System.out.println(stu);
}
② 类名::非静态方法
逻辑:执行此类的非静态方法。
参数:一是此类对象,二是非静态方法的参数。
// 非静态方法 Lambda表达式与方法引用
(stu) -> stu.getName()
Student::getName
(stu, name) -> stu.setName(name)
Student::setName
案例
public static void main(String[] args) {
/*
需求:挑选出所有男性学生
*/
Stream.of(
new Student("张无忌", "男"),
new Student("周芷若", "女"),
new Student("宋青书", "男")
)
.filter(Student::isMale) // 过滤操作:非静态方法引用方式
.forEach(Student::print); // 打印操作:静态方法引用方式
}
record Student(String name, String sex) {
public void print() {
System.out.println(this);
}
/*
Student::print
(stu) -> stu.print()
*/
public boolean isMale() {
return this.sex.equals("男");
}
/*
Student::isMale
(stu) -> stu.isMale()
*/
}
③ 对象::非静态方法
逻辑:执行此对象的非静态方法。
参数:非静态方法的参数。
// 非静态方法 Lambda表达式与方法引用
(obj) -> System.out.println(obj)
System.out::println
案例
static class Util {
public boolean isMale(Student stu) {
return stu.sex().equals("男");
}
public String xyz(Student stu){
return stu.name();
}
}
public static void main(String[] args) {
Util util = new Util();
Stream.of(
new Student("张无忌", "男"),
new Student("周芷若", "女"),
new Student("宋青书", "男")
)
.filter(util::isMale)
// .map(stu->stu.name()) // lambda 表达式
// .map(util::xyz) // 对象::非静态方法
.forEach(System.out::println);
}
④ 类名::new
逻辑:执行此构造方法。
参数:构造方法的参数。
// 无参构造方法 Lambda表达式与方法引用
() -> new Student()
Student::new
案例
这里为Student类提供三种构造方法,分别为无参构造,一参数构造和两参数构造。
static class Student {
private final String name;
private final Integer age;
public Student() {
this.name = "某人";
this.age = 18;
}
public Student(String name) {
this.name = name;
this.age = 18;
}
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
根据构造方法中的参数不同,返回类型也不同,与之对应的类型分别为Supplier、Function与BiFunction。
public static void main(String[] args) {
Supplier<Student> s1 = Student::new;
Function<String, Student> s2 = Student::new;
BiFunction<String, Integer, Student> s3 = Student::new;
System.out.println(s1.get());
System.out.println(s2.apply("张三"));
System.out.println(s3.apply("李四", 25));
}
⑤ this::非静态方法(第三种的特例,在类内部使用)
案例
public static void main(String[] args) {
Util util = new Util();
util.hiOrder(Stream.of(
new Student("张无忌", "男"),
new Student("周芷若", "女"),
new Student("宋青书", "男")
));
}
static class Util {
private boolean isMale(Student stu) {
return stu.sex().equals("男");
}
private boolean isFemale(Student stu) {
return stu.sex().equals("女");
}
// 过滤男性学生并打印
void hiOrder(Stream<Student> stream) {
stream
// .filter(stu->this.isMale(stu))
.filter(this::isMale)
.forEach(System.out::println);
}
}
⑥ super::非静态方法(第三种的特例,在类内部使用)
案例:在⑤的基础上新增UtilExt子类。
static class UtilExt extends Util {
// 过滤女性学生并打印
void hiOrder(Stream<Student> stream) {
super.isFemale(new Student("",""));
}
}
在main函数中调用子类中的方法。
public static void main(String[] args) {
Util util = new UtilExt();
util.hiOrder(Stream.of(
new Student("张无忌", "男"),
new Student("周芷若", "女"),
new Student("宋青书", "男")
));
}
总结
本文简单总结了函数式编程的基础内容,之后会对高级语法进行一定的扩展总结。