背景
在JDK1.8之前,我们经常会遇到下面这几种场景:
无法传入方法,只能传入对象
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread());
}
});
即使是简单方法,也仍然需要创建完整函数体
public int add(String s) {
return a + b;
}
这两种场景的缺点是:
- 代码冗余严重
- 使用不灵活
然而,在JDK1.8中,迎来了lambda表达式,上述两种代码段可以改造为下面的形式:
Thread thread = new Thread(() -> System.out.println(Thread.currentThread()));
(c1, c2) -> c1 + c2
具体是怎么操作的,我们接下来慢慢分析
介绍
函数式编程的一个特点就是函数可以作为参数和返回值,可以把函数当成一个字段值来进行使用,在JDK1.8之前,我们是没办法传入一个方法的,只能将方法封装到对象中,这样就带来了很大的不便,于是在JDK1.8中,引入了以Function为首的一批为函数式编程服务的接口
lambda表达式
lambda表达式的形式通常为:
(T t, V v) -> { /* code */ }
通常我们不能单独声明一个lambda表达式,就像我们不能单独声明一个字面值常量却不将它赋给任何变量,我们通常会手动约束需要使用lambda表达式参数的格式,所以可以简写成以下形式:
(t, v) -> { /* code */ }
再进一步化简
t, v -> /* code */
如果没有参数,则形式可以为:
() -> /* code */
@FunctionalInterface注解
在JDK1.8中,很多类(如Comparator)上都新增了一个@FunctionalInterface的注解,这个注解的原理在本篇中不深究,我们通过举一个例子来让大家了解这个注解的用处
这个注解是注解在接口上的,如下:
@FunctionalInterface
public interface Movable {
void move(String person);
}
如果是在JDK1.7中,假如我们想临时创建一个实现Movable接口的对象,一般采用以下方式:
Movable movable = new Movable() {
@Override
public void move(String person) {
// 这里我们就简单地打印下内容
System.out.println(person);
}
};
但是现在,我们可以采用以下的方式:
Movable movable = person -> System.out.println(person);
因为我们直接将参数原样输出,所以有以下的简化方式:
Movable movable = System.out::println;
是不是简洁度一下子就上去了
我们把注解去掉,发现依然能编译通过,甚至也能正常运行,那可能就要怀疑了,这个注解岂不是没用?其实Java提供的很多原生注解基本都是用来约束开发的,也就是让你在码代码的时候就能发现错误,总比写完了编译运行的时候报错要好
JDK1.8中,把lambda表达式叫做SAM类型(Single Abstract Method),即单一抽象方法类型,从字面上来看是说一个接口中只有一个抽象方法
@FunctionalInterface作为一个编译级错误检查注解,当你想用一个lambda表达式来代替一个接口(更准确地说是接口中的方法)时,就需要满足以下规范,否则就会在编译时报错:
- 注解的类型必须是一个接口
- 接口中只能有一个抽象方法(静态方法、默认方法和重写Object的方法除外)
满足这两个条件的就可以用以上形式来方便地进行编码
Function系列接口
刚刚我们只说了怎么通过lambda表达式方便地创建一个实现接口的对象,现在我们就要来了解JDK1.8中提供的最重要的功能,将lambda表达式作为参数和返回值使用
既然是Function系列接口,那么肯定不止Function接口这一个,不过我们先把重点放在Function上,如下
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
/* 在本方法执行前执行另一个方法 */
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
/* 在本方法执行后执行另一个方法 */
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
可以看出,Function接口中只有一个抽象方法apply(),负责接收T参数,然后返回R参数,剩下的compose()方法可以在执行本方法前执行传入Function接口的apply()方法,andThen()则是与之相反,在本身方法之后执行
说了那么多,Function始终还是一个接口,到底怎么实例化?这时候又得说回lambda表达式了,Function系列接口都是通过一个lambda表达式来进行实例化的,如下:
Function<Integer, String> func = num -> {
System.out.println("Run Success...");
return "[num]" + num;
};
String out = func.apply(20);
System.out.println("out: " + out);
输出结果:
out: [num]20
还可以使用compose()方法在方法执行前进行其他操作,使用方法如下:
Function<Integer, String> func = num -> "[num]" + num;
Function<Integer, Integer> before = num -> num + 100;
String out = func.compose(before).apply(20);
System.out.println("out: " + out);
输出结果:
out: [num]120
这里一定要注意一点,如果我们把原Function接口声明为传入T参数,输出R类型的话,before接口对象传入的参数一定要是最终的apply()方法传入参数的类型或其父类,返回对象一定要是T或T的子类,这里留一个简单的问题,建议自己思考一下andThen()方法传入的Function接口,它的参数和返回值需要满足需要满足什么约束
既然说了是Function接口系列,那自然有一系列类似的接口,总不能都是只传一个参数,返回一个参数的这种简单形式
这里我借鉴了一下java.util.function-Function接口这篇博文的表格,如下
结尾
JDK1.8中的函数式编程的特性基本就是这样了,在开发中虽然不推荐过度使用,不过很多时候合理地运用可以减少很多重复的代码,lambda表达式还是很值得学习的