0%

Java之Lambda表达式

1. Lambda 表达式入门

Lambda 表达式支持将代码块作为方法参数, Lambda 表达式允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。

我们来看一个示例,使用Comparator自定义比较器实现字符串按长度进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LambdaTest {
public static void main(String[] args) {
/**
* 按字符串长度进行排列
*/
String[] strArr = {"abc", "sd", "sf", "sgfhd", "sdjg"};
Arrays.sort(strArr, new Comparator<String>() {

@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});
//输出:[sd, sf, abc, sdjg, sgfhd]
System.out.println(Arrays.toString(strArr));
}
}

上述代码我们为了传入一个Comparator实例使用到了匿名内部类,但是我们可以使用Lambda表达式来创建简化的匿名内部类对象,可以改写为如下形式:

1
2
3
4
5
6
7
8
9
10
11
public class LambdaTest3 {
public static void main(String[] args) {
/**
* 按字符串长度进行排列
*/
String[] strArr = {"abc", "sd", "sf", "sgfhd", "sdjg"};
Arrays.sort(strArr, (o1, o2) -> o1.length() - o2.length());
//输出:[sd, sf, abc, sdjg, sgfhd]
System.out.println(Arrays.toString(strArr));
}
}

1.1 语法

Lambda 由如下三部分组成:

  1. 形参列表:形参列表允许省略形参类型。如果形参列表中只有一个参数,同时还可以省略形参列表的圆括号。如果 lambda 表达式没有参数,则仍要提供空括号,就像无参方法一样。
  2. 箭头(->):必须通过英文中划线和大于符号组成。
  3. 代码块:如果代码块只包含一条语句,Lambda 表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束。无需指定 Lambda 表达式的返回类型,Lambda 表达式的返回类型总是会由上下文推导得出。

注意: 如果一个 lambda 表达式只在某些分支返回一个值,而在另外一些分支不返回值,这是不合法的。例如,(int x) -> {if (x >= 0) return 1;}

下面程序示范了 Lambda 表达式的几种简化写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
interface Eatable {
void taste();
}

interface Flyable {
void fly(String weather);
}

interface Addable {
int add(int a, int b);
}

public class LambdaQs {
//调用该方法需要 Eatable 对象
public void eat(Eatable e) {
System.out.println(e);
e.taste();
}
//调用该方法需要 Flyable 对象
public void drive(Flyable f) {
System.out.println("我正在驾驶:" + f);
f.fly("【碧空如洗的晴日】");
}
//调用该方法需要 Addable 对象
public void test(Addable a) {
System.out.println("5 与 3 的和为:" + a.add(5, 3));
}

public static void main(String[] args) {
LambdaQs qs = new LambdaQs();
/* lambda 表达式没有参数,需保留括号。
* 表达式中的代码块只有一条语句,可省略括号
*/
qs.eat(() -> System.out.println("苹果的味道不错!"));

//lambda表达式的形参列表只有一个形参,可以省略圆括号
qs.drive(weather -> {
System.out.println("今天天气是:" + weather);
System.out.println("直升机飞行平稳");
});

/*
* 形参列表允许省略形参类型
* 只有一条语句,可以省略花括号;
* 可以省略return 语句
*/
qs.test((a, b) -> a + b);
}
}

2. 函数式接口

Lambda 表达式的类型,也被称为“目标类型(target tye)”,Lambda 表达式的目标类型必须是“函数式接口(functional interface)”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、静态方法,但只能声明一个抽象方法。

正如上述示例中的 Eatable、Flyable 和 Addable 都为函数式接口,它们都只有一个抽象方法。Java 8 API 有大量的函数式接口,例如:Runnable、ActionListener等接口。

提示:我们可以使用注解 @FunctionalInterface 来标记一个接口为函数式接口。该注解对程序功能没有任何作用,它用于告诉编译器执行更严格检查 — 检查该接口必须是函数式接口,否则编译器就会报错。

如在上述示例的 Eatable接口中我们可以使用注解@FunctionalInterface 来标记它是一个函数式接口,当我们在该接口多声明一个方法时编译器则会报错。
在这里插入图片描述


由于Lambda表达式具有类型(函数式接口类型),其本质是实现某个函数式接口的对象实例,我们可以使用 Lambda 表达式进行赋值操作。例如如下代码:

1
2
3
4
5
6
7
8
9
10
11
public class LambdaTest {
public static void main(String[] args) {
//将Lambda表达式赋值给一个函数式接口 Rannable
Runnable r = () -> {
for (int i = 0; i < 5; i++)
System.out.println("你好" + i);
};
Thread t = new Thread(r);
t.start();
}
}

Lambda 表达式的目标类型必须是明确的函数式接口,比如下面的操作便是不合法的,因为Object不是函数式接口。

1
2
3
4
Object obj = () -> {
for (int i = 0; i < 5; i++)
System.out.println("你好" + i);
};

当然我们可以使用强制类型转换的方式,使其可以赋值给一个Object对象,具体如下:

1
2
3
4
5
//将Lambda表达式赋值给一个函数式接口 Rannable
Object r = (Runnable)() -> {
for (int i = 0; i < 5; i++)
System.out.println("你好" + i);
};

前面已经说过,Lambda表达式其本质是实现某个函数式接口的对象实例,由于多态特性,我们便可以将其强转赋值给Object对象。

假设现在定义有如下函数式接口,其唯一的抽象方法具有和Runnable 接口的run方法一样的参数列表,这里即都是无参的函数 run。代码如下:

1
2
3
interface FkTest {
void run();
}

同样的 Lambda 表达式可以被当成不同的目标类型,唯一的要求是Lambda 表达式的形参类型列表与函数式接口唯一的抽象方法的形参列表相同。下述代码是正确的:

1
2
3
4
Object f = (FkTest)() -> {
for (int i = 0; i < 5; i++)
System.out.println("你好" + i);
};

java.util.function 包下预定义大量函数式接口,比如有如下一个很有用的函数式接口:

1
2
3
4
public interface Predicate<T> {
boolean test(T t);
// addtitonal default and static method
}

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
2
3
4
5
@FunctionalInterface
interface Converter {
//负责将String 参数转换为 Integer
Integer convert(String from);
}

下面使用Lambda 表达式来创建一个 Converter对象,并调用其 convert 方法实现字符到Integer的转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LambdaStaticMethod {
public static void main(String[] args) {
//旧版
// Converter converter = new Converter() {
// @Override
// public Integer convert(String from) {
// return Integer.valueOf(from);
// }
// };
//Lambda 表达式形式
Converter converter = from -> Integer.valueOf(from);
Integer num = converter.convert("99");
System.out.println(num + 1);//100
}
}

上面Lambda 表达式只有一行调用静态方法 valueOf 的代码,因此可以使用如下方法引用进行替换。

1
2
3
4
5
6
7
public class LambdaStaticMethod {
public static void main(String[] args) {
Converter converter = Integer::valueOf;
Integer num = converter.convert("99");
System.out.println(num + 1);//100
}
}

注意:valueOf方法中的参数列表是省略的,如果Integer类中有多个同名方法,具体的方法调用会根据函数式接口中的抽象方法的参数列表来决定。

3.2 引用特定对象的实例方法

假设定义有如下函数式接口:

1
2
3
4
5
@FunctionalInterface
interface IndexGetter {
//获取子串在字符串中出现的下标
int getIndexOfSubStr(String str);
}

下面使用Lambda 表达式来创建一个 IndexGetter 对象,并调用其 getIndexOfSubStr 方法获取子串在字符串中出现的下标。

1
2
3
4
5
6
7
public class ObjectInstance {
public static void main(String[] args) {
IndexGetter indexGetter = str -> "how to do?".indexOf(str);
int index = indexGetter.getIndexOfSubStr("to");
System.out.println(index);//4
}
}

我们完全可以将如下语句:

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
2
3
4
5
@FunctionalInterface
interface SubStrGetter {
//获取str中从s-(e-1)的子串
String getSubStr(String str, int s, int e);
}

下面使用Lambda 表达式来创建一个 SubStrGetter 对象,并调用其 getSubStr 方法获取字符串中指定范围的子串。

1
2
3
4
5
6
7
8
public class ObjectInstance {
public static void main(String[] args) {
// SubStrGetter subStrGetter = (str, s, e) -> str.substring(s, e);
SubStrGetter subStrGetter = String::substring;
String subStr = subStrGetter.getSubStr("Just do it!", 5, 7);
System.out.println(subStr);//do
}
}

对于上面的实例方法的引用,也就是调用某个String对象的substring()实例方法来实现ObjectInstance函数式接口中唯一的抽象方法,当调用getSubStr方法时,第一个调用参数将作为substring方法的调用者,剩下的调用参数会作为substring实例方法的调用参数。

3.4 引用构造器

假设定义有如下函数式接口:

1
2
3
4
5
@FunctionalInterface
interface JFrameGetter {
//获取以title为标题的JFrame对象
JFrame getJFrame(String title);
}

下面使用Lambda 表达式来创建一个 JFrameGetter 对象,并调用其 getJFrame 方法获取 JFrame 对象。

1
2
3
4
5
6
7
8
9
public class ConstructorLambda {
public static void main(String[] args) {
// JFrameGetter frameGetter = title -> new JFrame(title);
//真是令人懵逼的语法。。。
JFrameGetter frameGetter = JFrame::new;
JFrame frame = frameGetter.getJFrame("标题");
System.out.println(frame.hashCode());//666641942
}
}

对于上述构造器引用,也就是调用某个JFrame类的构造器来实现JFrameGetter函数式接口中的抽象方法getJFrame,调用getJFrame时会将参数传给JFrame的构造器并获取一个JFrame对象。

4. 变量作用域

考虑下面一个例子:

1
2
3
4
5
6
7
public static void repeatMessage(String text, int delay) {
ActionListener listener = event -> {
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay, listener).start();
}

下面是一个调用:

1
repeatMessage("Hello", 1000);

可以知道,Lambda表达式中的text变量是通过repeatMessage传递的。但是有时候可能会在执行repeatMessage很久之后才会执行lambda表达式中的代码,这时repeatMessage方法中传递的text值”Hello”已经不存在了。

其实,Lambda表达式是会存储传递进去的text变量的值的,这个过程可以称为捕获。 但是有一个重要的限制,就是text的值是不可更改的。例如,下面的做法是不合法的:

1
2
3
4
5
6
public static void countDown(int start, int delay) {
ActionListener listener = event -> {
// start--;//不合法
};
new Timer(delay, listener).start();
}

也就是说,在Java中,lambda表达式只能引用值不会改变的变量,显然上述的start在lambda表达式中被更改了。

另外如果在lambda表达式中引用变量,而这个变量可能会在外部改变,这也是不合法的。比如下述用法:

1
2
3
4
5
6
7
8
public static void countDown(int start, int delay) {
for (int i = 0; i < 500; i++) {
ActionListener listener = event -> {
// System.out.println(i);//不合法
};
new Timer(delay, listener).start();
}
}

总之,lambda表达式中捕获的变量必须实际上是最终变量,也就是该变量初始化之后就不会改变值的变量,相当于使用 final修饰的变量。


在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合适的。比如下述例子:
在这里插入图片描述


在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如下述代码:

1
2
3
4
5
6
7
8
9
10
11
public class LambdaTest {
public void init() {
ActionListener listener = event -> {
this.myPrint();
};
}

public void myPrint() {
System.out.println("。。。");
}
}

表达式this.myPrint()中的this是指代LambdaTest对象的引用,而不是ActionListener对象listener的引用。

------ 本文结束------