Java8特性之Lambda表达式
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/13/ )。
1 概述
Java 8由Oracle公司于2014年3月18日发布,至今已过去数年之久。然而,直到今日仍有许多软件开发者对其相关特性不了解,这可能主要是Java教材更新缓慢的原因。为了使大家对与Java8的特性有全面系统的了解,本公众号将连续几篇文章介绍Java8中的各个特性。
Java8中新增的特性主要有:
- Lambda 表达式 − Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中。
- 方法引用 − 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
- 默认方法 − 默认方法就是一个在接口里面有了一个实现的方法。
- 新工具 − 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。
- Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。
- Date Time API − 加强对日期与时间的处理。
- Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
- Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。
本文介绍其中的Lambda表达式。其它特性会在后续文章中陆续介绍。
2 Lambda表达式
经常听到一个概念:闭包。闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
其实,就是Lambda表达式,Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
2.1 出现原因
面向对象式编程就应该纯粹的面向对象,于是经常看到这样的写法: 如果你想写一个方法,那么就必须把它放到一个类里面,然后new出来对象,对象调用这个方法。最大的问题就在于其冗余的语法,有人戏称匿名类型导致了“高度问题”(height problem): 大多匿名内部类的多行代码中仅有一行在做实际工作。因此JAVA8中就提供了这种“函数式编程”的方法 —— lambda表达式,供我们来更加简明扼要的实现内部匿名类的功能。
也就是说,你在某处就真的只需要一个能做一件事情的函数而已,连它叫什么名字都无关紧要。 Lambda 表达式就可以用来做这件事。
2.2 语法介绍
2.2.1 函数式接口
函数式接口(Functional Interface):定义的一个接口,接口里面必须 有且只有一个抽象方法(可以有默认方法和静态方法) ,这样的接口就成为函数式接口。在可以使用lambda表达式的地方,方法声明时必须包含一个函数式的接口。
如果我们提供的这个接口包含一个以上的Abstract Method,那么使用lambda表达式则会报错。 因为这不是函数式接口。
例如:
@FunctionalInterface
interface MathOperation {
int operation(int a, int b);
}
Java 8为函数式接口引入了一个新注解@FunctionalInterface,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。你不加的话,就不做检查。
注意点: * 函数式接口中可以额外定义多个Object的public方法一样抽象方法:接口最终有确定的类实现, 而类的最终父类是Object。 因此函数式接口可以定义Object的public方法。 * 函数式接口的抽象方法可以声明 可检查异常(checked exception)。 在调用目标对象的这个方法时必须catch这个异常。 * 函数式接口中除了那个抽象方法外还可以包含静态方法、默认方法。
任何函数式接口都可以使用lambda表达式替换。 例如系统已经有的:ActionListener、Comparator、Runnable。JDK 8之前已有的函数式接口:
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener
Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:
Predicate<T>——接收T对象并返回boolean
Consumer<T>——接收T对象,不返回值
Function<T, R>——接收T对象,返回R对象
Supplier<T>——提供T对象(例如工厂),不接收值
UnaryOperator<T>——接收T对象,返回T对象
BinaryOperator<T>——接收两个T对象,返回T对象
另外,还有好多好多的接口,可以到具体的java.util.function包下去看一下。
那么在参数为这些接口的地方,我们就可以直接使用lambda表达式了!
2.2.2 Lambda表达式
(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
statment1;
statment2;
//.............
return statmentM;
}
- 当lambda表达式的参数个数只有一个,可以省略小括号;
- 当lambda表达式只包含一条语句时,可以省略大括号、return和语句结尾的分号。自动返回该语句的结果。
Lambda 表达式的简单例子:
// 1. 不需要参数,返回值为 5
() -> 5
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y
// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)
2.3 特性与注意点
2.3.1 lambda表达式中的this概念
在lambda中,this不是指向lambda表达式产生的那个SAM对象,而是声明它的外部对象。
不举例子了,是真的。
2.3.2 类型推导
先上个小例子举例:
public class TestBean {
public static void main(String[] args) {
MathOperation addition = (a, b) -> a + b;
Integer ans = addition.operation(7,9);
System.out.println(ans.toString());
}
interface MathOperation {
int operation(int a, int b);
}
}
编译器负责推导lambda表达式的类型。它利用lambda表达式所在上下文所期待的类型进行推导, 这个被期待的类型被称为目标类型。就是说我们传入的参数可以无需写类型了!
因此,在定义函数时:
MathOperation addition = (a, b) -> a + b;
我们并没有在入参里说明入参a,b的类型,因为如果没有类型推导,得这么写:
(int a, int b) -> a + b;
2.3.3 函数嵌套
我们知道,系统已经帮我们创建了几个函数时接口,例如:
Function<T, R>——接收T对象,返回R对象
它的代码如下:
@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;
}
}
它内置了两个写出了默认实现的方法,基于它们,可以实现函数的嵌套。
public class TestBean {
public static void main(String[] args) {
Function<String, String> fun1 = input -> input + ">";
Function<String, String> fun2 = input -> input + "+";
System.out.println(fun1.apply("a"));
System.out.println(fun2.apply("a"));
System.out.println(fun1.compose(fun2).apply("b"));
System.out.println(fun1.andThen(fun2).apply("c"));
}
}
---
a>
a+
b+>
c>+
首先,这是一个函数式接口,真正的抽象函数只有一个。
接下来我们看一下,compose() 和 andThen() 这两个默认实现为什么能够实现级联操作:首先我们定义了两个,分别是fun1和fun2。那,对于fun1.apply(“a”)操作,我们就是将“a”作为参数,传给函数fun1,然后fun1把它执行掉,故输出“a>”。
我们着重要分析的是fun1.compose(fun2).apply("b")
。分析compose函数,before=fun2
。因此,(V v) -> apply(before.apply(v))
最终变为:(V v) -> apply(fun2.apply(v))
,即为:fun1.apply(fun2.apply(v))
,因此是v先给fun2处理,得到“b+
”;然后“b+”传给fun1,得到“b+>
”。因此,实现了两个函数的嵌套。
同理,fun1.andThen(fun2).apply("c")
,after=fun2
,因此(T t) -> after.apply(apply(t))
;即为:fun2.apply(apply(t))
。apply(t)
是针对于fun1的,因此是fun1处理完完后又fun2处理。
不过,这两个方法是系统内置的,会用就可以了。
3 总结
Java8 提供的Lambda表达式使得Java程序更为便捷、灵活,不过在使用Lambda表达式时也要注意相关指向问题,防止迷失。
关于Java8中Lambda表达式的介绍就到这里,在后续的文章中我们将介绍Java8中的其它特性。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。