1. Lambda 表达式入门
Lambda 表达式支持将代码块作为方法参数, Lambda 表达式允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。
我们来看一个示例,使用Comparator自定义比较器实现字符串按长度进行排序。
1 | public class LambdaTest { |
上述代码我们为了传入一个Comparator实例使用到了匿名内部类,但是我们可以使用Lambda表达式来创建简化的匿名内部类对象,可以改写为如下形式:
1 | public class LambdaTest3 { |
1.1 语法
Lambda 由如下三部分组成:
- 形参列表:形参列表允许省略形参类型。如果形参列表中只有一个参数,同时还可以省略形参列表的圆括号。如果 lambda 表达式没有参数,则仍要提供空括号,就像无参方法一样。
- 箭头(->):必须通过英文中划线和大于符号组成。
- 代码块:如果代码块只包含一条语句,Lambda 表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束。无需指定 Lambda 表达式的返回类型,Lambda 表达式的返回类型总是会由上下文推导得出。
注意: 如果一个 lambda 表达式只在某些分支返回一个值,而在另外一些分支不返回值,这是不合法的。例如,(int x) -> {if (x >= 0) return 1;}
下面程序示范了 Lambda 表达式的几种简化写法。
1 | interface Eatable { |
2. 函数式接口
Lambda 表达式的类型,也被称为“目标类型(target tye)”,Lambda 表达式的目标类型必须是“函数式接口(functional interface)”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、静态方法,但只能声明一个抽象方法。
正如上述示例中的 Eatable、Flyable 和 Addable 都为函数式接口,它们都只有一个抽象方法。Java 8 API 有大量的函数式接口,例如:Runnable、ActionListener等接口。
提示:我们可以使用注解 @FunctionalInterface 来标记一个接口为函数式接口。该注解对程序功能没有任何作用,它用于告诉编译器执行更严格检查 — 检查该接口必须是函数式接口,否则编译器就会报错。
如在上述示例的 Eatable接口中我们可以使用注解@FunctionalInterface 来标记它是一个函数式接口,当我们在该接口多声明一个方法时编译器则会报错。
由于Lambda表达式具有类型(函数式接口类型),其本质是实现某个函数式接口的对象实例,我们可以使用 Lambda 表达式进行赋值操作。例如如下代码:
1 | public class LambdaTest { |
Lambda 表达式的目标类型必须是明确的函数式接口,比如下面的操作便是不合法的,因为Object不是函数式接口。
1 | Object obj = () -> { |
当然我们可以使用强制类型转换的方式,使其可以赋值给一个Object对象,具体如下:
1 | //将Lambda表达式赋值给一个函数式接口 Rannable |
前面已经说过,Lambda表达式其本质是实现某个函数式接口的对象实例,由于多态特性,我们便可以将其强转赋值给Object对象。
假设现在定义有如下函数式接口,其唯一的抽象方法具有和Runnable 接口的run方法一样的参数列表,这里即都是无参的函数 run。代码如下:
1 | interface FkTest { |
同样的 Lambda 表达式可以被当成不同的目标类型,唯一的要求是Lambda 表达式的形参类型列表与函数式接口唯一的抽象方法的形参列表相同。下述代码是正确的:
1 | Object f = (FkTest)() -> { |
java.util.function 包下预定义大量函数式接口,比如有如下一个很有用的函数式接口:
1 | public interface Predicate<T> { |
ArrayLIst 类有一个 removeIf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式的。例如下面的语句将从一个ArrayList中删除所有的null值:
1 | list.removeIf(e -> e == null); |
3. 方法引用与构造器引用
如果 lambda 表达式的代码块只有一条代码,除了可以省略花括号,我们还可以在代码块中使用方法引用和构造器引用。方法的引用和构造器的引用都需要使用两个英文冒号“::”。
种类 | 示例 | 说明 | 对应的 Lambda 表达式 |
---|---|---|---|
引用类方法 | 类名::静态方法 | 函数式接口中被实现方法的全部参数传给该静态方法作为参数 | (a,b,…) -> 类名.静态方法(a,b,…) |
引用特定对象的实例方法 | 特定对象::实例方法 | 函数式接口中被实现方法的全部参数传给该方法作为参数 | (a,b,…) -> 特定对象.实例方法(a,b,…) |
引用某类的实例方法 | 类名::实例方法 | 函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数 | (a,b,…) -> a.实例方法(b,…) |
引用构造器 | 类名::new | 函数式接口中被实现方法的全部参数传给该构造器作为参数 | (a,b,…) -> new 类名(a,b,…) |
3.1 引用静态方法
假设定义有如下函数式接口:
1 |
|
下面使用Lambda 表达式来创建一个 Converter对象,并调用其 convert 方法实现字符到Integer的转换。
1 | public class LambdaStaticMethod { |
上面Lambda 表达式只有一行调用静态方法 valueOf 的代码,因此可以使用如下方法引用进行替换。
1 | public class LambdaStaticMethod { |
注意:valueOf方法中的参数列表是省略的,如果Integer类中有多个同名方法,具体的方法调用会根据函数式接口中的抽象方法的参数列表来决定。
3.2 引用特定对象的实例方法
假设定义有如下函数式接口:
1 |
|
下面使用Lambda 表达式来创建一个 IndexGetter 对象,并调用其 getIndexOfSubStr 方法获取子串在字符串中出现的下标。
1 | public class ObjectInstance { |
我们完全可以将如下语句:
1 | IndexGetter indexGetter = str -> "how to do?".indexOf(str); |
替换成如下形式:
1 | IndexGetter indexGetter = "how to do?"::indexOf; |
对于上述的实例方法引用,也就是调用”how to do?”字符串对象的indexOf()实例方法来实现函数式接口 IndexGetter中的唯一抽象接口 getIndexOfSubStr。
3.3 引用某类对象的实例方法
假设定义有如下函数式接口:
1 |
|
下面使用Lambda 表达式来创建一个 SubStrGetter 对象,并调用其 getSubStr 方法获取字符串中指定范围的子串。
1 | public class ObjectInstance { |
对于上面的实例方法的引用,也就是调用某个String对象的substring()实例方法来实现ObjectInstance函数式接口中唯一的抽象方法,当调用getSubStr方法时,第一个调用参数将作为substring方法的调用者,剩下的调用参数会作为substring实例方法的调用参数。
3.4 引用构造器
假设定义有如下函数式接口:
1 |
|
下面使用Lambda 表达式来创建一个 JFrameGetter 对象,并调用其 getJFrame 方法获取 JFrame 对象。
1 | public class ConstructorLambda { |
对于上述构造器引用,也就是调用某个JFrame类的构造器来实现JFrameGetter函数式接口中的抽象方法getJFrame,调用getJFrame时会将参数传给JFrame的构造器并获取一个JFrame对象。
4. 变量作用域
考虑下面一个例子:
1 | public static void repeatMessage(String text, int delay) { |
下面是一个调用:
1 | repeatMessage("Hello", 1000); |
可以知道,Lambda表达式中的text变量是通过repeatMessage传递的。但是有时候可能会在执行repeatMessage很久之后才会执行lambda表达式中的代码,这时repeatMessage方法中传递的text值”Hello”已经不存在了。
其实,Lambda表达式是会存储传递进去的text变量的值的,这个过程可以称为捕获。 但是有一个重要的限制,就是text的值是不可更改的。例如,下面的做法是不合法的:
1 | public static void countDown(int start, int delay) { |
也就是说,在Java中,lambda表达式只能引用值不会改变的变量,显然上述的start在lambda表达式中被更改了。
另外如果在lambda表达式中引用变量,而这个变量可能会在外部改变,这也是不合法的。比如下述用法:
1 | public static void countDown(int start, int delay) { |
总之,lambda表达式中捕获的变量必须实际上是最终变量,也就是该变量初始化之后就不会改变值的变量,相当于使用 final修饰的变量。
在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合适的。比如下述例子:
在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如下述代码:
1 | public class LambdaTest { |
表达式this.myPrint()中的this是指代LambdaTest对象的引用,而不是ActionListener对象listener的引用。