函数式编程
什么是函数式编程这个问题一直困扰着我,直到最近花时间好好拜读了《Java8 函数式编程》这本书后有了一个较为清晰的认识,用我个人的理解来说函数式编程这个概念是为了区别于通常的编程思维模式,传统的java开发中,通常我们写一个函数需要入参,这里的入参指的是数据,而函数式编程希望传入的是一个行为,在jdk8以前java其实也能支持这种方式,但有很大的局限性,那就是传入的行为需要固化在一个对象的方法中,再借此对象做为入参。随着jdk8将函数式编程概念提到明面上后,借助Lambda表达式将函数式编程变动更加简化。
Lambda表达式
Lambda表达式是一个匿名方法,将行为像数据一样进行传递,入上面说的,lambda表达式并没有影响java的基础,其实质仅仅是便于开发者贯彻函数式编程理念的语法糖而已。
这里有一点特别要提的是,通常在Lambda表达中使用的变量必须是final
的,但如果该局部变量在既成事实上是final
的,那么也可以不使用final关键字
函数接口
函数接口指仅具有单个抽象方法的接口,用来表示Lambda表达式的类型,其实质也就是用类来包裹行为,以便于可以不更改java的基础规则。函数接口通常建议使用@FunctionalInterface
进行标注,更便于使用者理解也便于编译器识别,但也可以不使用该注解,一个接口只要实质上满足函数式接口的规则那它就是一个函数式接口,一下情形都满足函数式接口的规则:
- 接口中仅包含一个抽象(未实现)方法
- 接口中的
default
方法不受规则1限制 - 接口中允许有静态方法
- 接口中抽象方法覆盖的是
Object
中的public方法时不受规则1限制
default方法
默认方法三定律
- 类胜于接口。如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义的方法。(为了让代码向后兼容)
- 子类胜于父类。如果一个接口继承了另一个接口,且两个接口都定义了一个默认方法,那么子类中定义的方法胜出。
- 如果上面两条规则不适用,子类要么需要实现该方法,要么将该方法声明为抽象方法。
一些重要的函数接口说明及示例
Jdk中默认已经实现了一些常用函数式接口,入下表中所示:
函数接口 | 参数 | 返回类型 | 示例 |
---|---|---|---|
Predicate |
T | boolean | 判断是否 |
Consumer |
T | void | 输入一个参数 |
BiConsumer<T,R> | (T,R) | void | 输入两个参数 |
Function<T,R> | T | R | 获得对象的名字 |
Supplier |
none | T | 工厂方法 |
UnaryOperator |
T | T | 一元运算符 |
BinaryOperator |
(T,T) | T | 二元运算符 |
Lambda表达式作为参数时,其类型由它的目标类型推导得出,推导过程遵循如下规则:
- 如果只有一个可能的目标类型,由相应函数接口里的参数类型推导得出;
- 如果有多个可能的目标类型,由最具体的类型推导得出;
- 如果有多个可能的目标类型且最具体的类型不明确,则需人为指定类型。
例子一
使用Lambda改动前
1 | public interface Action { |
定义一个动作接口Action
1 | public class Open implements Action{ |
该动作接口有三个实现类Open
、Save
、Close
,实现了不同的动作
1 | public class Macro { |
定义一个宏命令类Macro
,其中的record
方法记录不同的Action
,run
方法负责一次性执行记录的所有Action
1 | public class UseLambdaTest1 { |
UseLambdaTest1
为具体的实现类,执行输出结果如下:
1 | 打开文档 |
使用Lambda改动后
首先可以对Macro
类的run
方法进行改写,具体如下所示
1 | public class Macro { |
值得一提的是这里的Lambda表达式还可以用方法引用代替,写成actions.forEach(Action::perform)
,但这不是本例使用Lambda的核心意义。
接着改动UseLambdaTest1
,代码如下:
1 | public class UseLambdaTest1 { |
运行后会发现输出结果完全一致,但却可以不再去编写Open
、Save
、Close
这三个类,并且Action
的方法perform
的实现一目了然,所以这里的Action
其实就是个函数式接口,理应给它加一个@FunctionalInterface
注解。
注意:虽然这种方式书写很方便,但也有它的缺陷,非常明显的一点是当方法实现非常复杂时,->
右边的代码会变得非常冗长,这种时候就不适合使用Lambda来进行书写了,毕竟Lambda的本意并不是破坏一些既定的代码最佳实践。
例子二
使用Lambda改动前
1 | public class Product { |
首先实现一个商品类Product
1 | public class Filter { |
接着实现一个Filter
类,该类中实现了两个方法checkBigger
和checkSmaller
,分别判断传入的商品尺寸是否大于3,以及商品的尺寸是否小于5
1 | public class UseLambdaTest2 { |
最后是在UseLambdaTest2
的main
方法中分别调用上述比较方法,这里可以看到checkBigger
和checkSmaller
方法本质上都是在做比较这件事,只是比较的规则不同,那么我们可以将这个规则抽象成一个函数式接口做为方法的入参,这样一来就可以将两个比较方法合二为一了。
使用Lambda改动后
1 | public class Filter { |
如上所示,在Filter
中增加了一个check
方法,该方法入参Predicate<T>
之前有提到过,是Java内置的函数式接口,其抽象函数test
的返回值是boolean型的,正好能为我们所用。
注:其实Java内置的函数式接口只是在抽象方法中帮我们限定了入参和出参,了解这两点就够了
1 | public class UseLambdaTest2 { |
改造后的main
方法如上所示,可以发现使用Lambda前如果要增加判断规则需要在Filter
中增加新的方法,而这里我们可以很容易根据新的规则进行扩展。
方法引用
方法引用是Lambda表达式的等价形式,其本质仍是Lambda表达式,通常有以下四种形式:
序号 | 类型 | 示例 |
---|---|---|
1 | 引用静态方法 | ContainingClass::staticMethodName |
2 | 引用某个对象的实例方法 | containingObject(也可以使用super/this)::instanceMethodName |
3 | 引用某个类型的任意对象的实例方法 | ContainingType::methodName |
4 | 引用构造方法 | ClassName::new |
例子:
s -> String.valueOf(s)
等价于String::valueOf
(x, y) -> Math.pow(x, y)
等价于Math::pow
s -> new Person().goWalking(s);
等价于new Person()::goWalking
String[] stringArray = { "Barbara", "James", "Mary" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
(name, nationality) -> new Artist(name, nationality)
等价于Artist::new
x -> new int[x]
等价于int[]::new