Lambda 省略规则与 Comparator 实战 | JavaSE
Lambda 省略规则与 Comparator 实战
一、学习目标
完成本章后,你应该能够:
- 能够从完整 Lambda 一步一步推导到简化 Lambda。
- 能够准确判断参数类型、圆括号、大括号、
return和分号何时可以省略。 - 能够避免因为错误省略导致 Lambda 编译失败。
- 能够使用 Lambda 实现
Comparator<T>排序规则。 - 能够解释
Comparator.compare()返回正数、负数和零的语义。 - 能够使用
Integer.compare()和Comparator.comparingInt()编写更加稳健、可读的比较器。
二、核心知识
2.1 Lambda 为什么可以继续简化
上一章我们学习过完整 Lambda:
Calculator calculator =
(int a, int b) -> {
return a + b;
};
但函数式接口已经提供大量信息:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
编译器已经知道:
参数数量:2
参数类型:int、int
返回类型:int
所以很多信息没有必要重复书写。
最终可以变成:
Calculator calculator =
(a, b) -> a + b;
因此 Lambda 的简化不是“随便删除字符”,而是:
删除编译器能够根据目标函数式接口推断出来的信息。
三、使用方法
3.1 规则一:参数类型可以省略
完整写法:
Calculator calculator =
(int a, int b) -> {
return a + b;
};
参数类型可以推断,因此:
Calculator calculator =
(a, b) -> {
return a + b;
};
这里:
a
b
仍然是 int。
因为:
interface Calculator {
int calculate(int a, int b);
}
已经规定了参数类型。
3.2 参数类型不能随意“省一半”
不要写成:
// 错误示例
// (int a, b) -> ...
如果使用隐式类型参数,就统一通过上下文进行类型推断:
(a, b) -> ...
或者显式写完整类型:
(int a, int b) -> ...
学习阶段建议遵循:
要么都写类型
要么都省略类型
不要混写。
3.3 规则二:只有一个隐式类型参数时可以省略圆括号
例如:
@FunctionalInterface
interface Printer {
void print(String message);
}
完整 Lambda:
Printer printer =
(String message) -> {
System.out.println(message);
};
省略类型:
Printer printer =
(message) -> {
System.out.println(message);
};
因为只有一个参数,可以进一步:
Printer printer =
message -> {
System.out.println(message);
};
最终:
message -> ...
3.4 什么情况下不能省略参数圆括号
没有参数
必须写:
() -> {
System.out.println("Hello");
}
不能写:
// -> ...
多个参数
必须保留:
(a, b) -> a + b
不能写:
// a, b -> a + b
所以可以记:
| 参数数量 | () |
| ---------: | -------------------- |
| 0 | 必须 |
| 1 | 在符合条件时可以省略 |
| 2 个及以上 | 必须 |
3.5 规则三:Lambda 方法体只有一个表达式时可以使用表达式体
例如:
Printer printer =
message -> {
System.out.println(message);
};
可以简化成:
Printer printer =
message -> System.out.println(message);
注意:
原来的代码块中:
System.out.println(message);
有分号。
变成表达式体后:
message -> System.out.println(message)
不要在 Lambda 表达式体内部额外保留原代码块的那个分号。
外层 Java 赋值语句仍然需要:
Printer printer =
message -> System.out.println(message);
最后的:
;
属于变量赋值语句,不是 Lambda 方法体中的分号。
3.6 规则四:单个返回表达式可以省略 {}、return 和内部分号
完整形式:
Calculator calculator =
(a, b) -> {
return a + b;
};
如果方法体只是返回一个表达式,可以写:
Calculator calculator =
(a, b) -> a + b;
注意不能写:
// 错误
(a, b) -> return a + b;
也不能写:
// 错误
(a, b) -> { a + b; }
对于有返回值 Lambda:
完整代码块:
(a, b) -> {
return a + b;
}
表达式体:
(a, b) -> a + b
必须成套转换。
3.7 多条语句不能删除大括号
例如:
Printer printer = message -> {
System.out.println("准备输出");
System.out.println(message);
};
这里方法体有两条语句。
不能改成:
// 错误
// message ->
// System.out.println("准备输出");
// System.out.println(message);
多语句必须保留代码块:
{
...
}
3.8 有返回值且存在多条语句时仍然需要 return
例如:
Calculator calculator = (a, b) -> {
int result = a + b;
System.out.println("正在计算");
return result;
};
这里不能省略成:
(a, b) -> result
因为前面还有其他逻辑。
所以:
Lambda 简化的核心条件不是“看起来代码很短”,而是看 Lambda Body 的结构。
四、原理与进阶
4.1 表达式体与代码块体
Lambda 方法体主要可以表现为两种形式。
表达式体
(a, b) -> a + b
右侧直接是一个表达式。
代码块体
(a, b) -> {
int result = a + b;
return result;
}
右侧是代码块。
理解这个区别以后,就不需要死记:
“只有一行就去大括号、去 return、去分号。”
更精确的理解应该是:
当 Lambda 可以使用一个表达式完整描述方法体时,可以改成表达式体。
4.2 Lambda 简化路线
例如:
Arrays.sort(
students,
new Comparator<Student>() {
@Override
public int compare(
Student o1,
Student o2
) {
return Integer.compare(
o1.getAge(),
o2.getAge()
);
}
}
);
第一步,删除匿名内部类结构:
Arrays.sort(
students,
(Student o1, Student o2) -> {
return Integer.compare(
o1.getAge(),
o2.getAge()
);
}
);
第二步,省略参数类型:
Arrays.sort(
students,
(o1, o2) -> {
return Integer.compare(
o1.getAge(),
o2.getAge()
);
}
);
第三步,转换成表达式体:
Arrays.sort(
students,
(o1, o2) ->
Integer.compare(
o1.getAge(),
o2.getAge()
)
);
简化过程本质上是:
匿名内部类
↓
保留函数式接口方法参数和方法体
↓
Lambda
↓
利用类型推断删除参数类型
↓
利用表达式体继续简化
五、实践应用
5.1 Comparator 为什么特别适合 Lambda
Comparator<T> 的核心抽象方法是:
int compare(T o1, T o2);
因此它是函数式接口。
我们只需要提供:
两个对象应该按照什么规则比较。
例如学生年龄升序:
Arrays.sort(
students,
(o1, o2) ->
Integer.compare(
o1.getAge(),
o2.getAge()
)
);
5.2 Comparator 的返回值规则
对于:
compare(o1, o2)
应该返回:
负整数:o1 应排在 o2 前面
0: 两者在当前比较规则下相等
正整数:o1 应排在 o2 后面
例如:
Integer.compare(
o1.getAge(),
o2.getAge()
)
表示:
年龄从小到大排序。
5.3 年龄降序
只需要反转比较顺序:
Arrays.sort(
students,
(o1, o2) ->
Integer.compare(
o2.getAge(),
o1.getAge()
)
);
注意:
升序:
compare(o1.age, o2.age)
降序:
compare(o2.age, o1.age)
5.4 为什么不推荐随意使用减法比较整数
在很多教学代码中可以看到:
(o1, o2) ->
o1.getAge() - o2.getAge()
如果业务数据一定是正常年龄,这段代码通常不会出现实际问题。
但把它当作通用整数比较写法并不稳健。
例如:
int a = Integer.MAX_VALUE;
int b = -1;
执行:
a - b
数学结果超过 int 最大范围,可能产生整数溢出。
因此通用代码更推荐:
Integer.compare(a, b)
例如:
(o1, o2) ->
Integer.compare(
o1.getAge(),
o2.getAge()
)
5.5 完整 Comparator 实战
import java.util.Arrays;
public class LambdaComparatorDemo {
public static void main(String[] args) {
Student[] students = {
new Student("小明", 20),
new Student("小红", 18),
new Student("小刚", 22)
};
Arrays.sort(
students,
(o1, o2) ->
Integer.compare(
o1.getAge(),
o2.getAge()
)
);
System.out.println(
Arrays.toString(students)
);
}
}
class Student {
private String name;
private int age;
public Student(
String name,
int age
) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
排序规则:
(o1, o2) ->
Integer.compare(
o1.getAge(),
o2.getAge()
)
非常接近自然语言:
拿出两个学生
→ 比较两个学生的年龄
→ 根据结果决定顺序
5.6 Comparator.comparingInt
Java 还提供:
Comparator.comparingInt(...)
用于根据一个 int 类型排序键构造 Comparator。
例如:
Arrays.sort(
students,
Comparator.comparingInt(
student -> student.getAge()
)
);
需要:
import java.util.Comparator;
完整核心代码:
Comparator<Student> comparator =
Comparator.comparingInt(
student -> student.getAge()
);
Arrays.sort(students, comparator);
这里:
student -> student.getAge()
表示:
从 Student 中提取 age 作为整数排序键。
这比手工处理比较结果更具有声明式风格:
不要告诉程序“两个对象怎么比”
而是告诉程序
“我要按照 age 这个字段排序”
下一章学习方法引用以后,这段代码还能继续简化。
本章暂时保留 Lambda 写法。
5.7 GUI 事件监听中的 Lambda
例如按钮点击:
button.addActionListener(
event -> {
System.out.println("登录成功");
}
);
如果只执行一句:
button.addActionListener(
event ->
System.out.println("登录成功")
);
这里体现的正是:
一个参数
↓
省略类型
↓
省略括号
一条表达式
↓
省略代码块
六、常见问题
6.1 一个参数一定可以省略括号吗?
不是简单地理解成“看到一个参数就删括号”。
例如显式声明类型:
(String message) ->
System.out.println(message)
不能直接改成:
// String message ->
最常见的简化过程应该是:
(String message) -> ...
先变成:
(message) -> ...
再变成:
message -> ...
6.2 多个参数为什么不能去掉括号?
正确:
(a, b) -> a + b
错误:
// a, b -> a + b
多个参数必须由圆括号形成参数列表。
6.3 为什么 return 有时候必须删?
完整代码:
(a, b) -> {
return a + b;
}
转成表达式体以后:
(a, b) -> a + b
表达式本身就是返回结果。
因此不能:
// (a, b) -> return a + b
6.4 为什么有时候不能删除大括号?
例如:
value -> {
System.out.println("value:");
System.out.println(value);
}
存在多条语句。
必须使用代码块。
6.5 Comparator 必须返回 -1、0、1 吗?
不需要。
它要求的是:
负整数
0
正整数
而不是只能返回:
-1
0
1
6.6 o1.getAge() - o2.getAge() 一定错误吗?
不是。
对于受控的小范围年龄数据通常能够得到正确结果。
问题是:
它不适合作为通用整数比较模板。
更加稳健的方式是:
Integer.compare(
o1.getAge(),
o2.getAge()
)
6.7 Lambda 越短越好吗?
不是。
例如:
(a, b) -> {
// 大量业务代码……
}
如果 Lambda 方法体非常长,继续强行塞在调用位置反而会降低可读性。
Lambda 的目的不是:
“代码字符越少越高级”。
而是:
让行为表达更加直接。
七、练习与验收
7.1 知识问答
- Lambda 参数类型为什么可以被省略?
- 两个参数时可以省略圆括号吗?
- 一个参数什么时候可以省略圆括号?
- 没有参数时能不能删除
()? - Lambda 方法体有两条语句时可以删除
{}吗? - 有返回值 Lambda 在什么情况下可以删除
return? Comparator<T>为什么适合使用 Lambda?compare(o1, o2)返回负数、零和正数分别表示什么?- 为什么整数 Comparator 不推荐无条件使用
a - b? Integer.compare(a, b)解决了什么问题?
7.2 代码阅读
分析:
Comparator<Integer> comparator =
(a, b) -> Integer.compare(a, b);
回答:
a、b的类型是什么?- 为什么代码中没有写参数类型?
- 如果
a < b,比较结果符号是什么? - 该 Comparator 表示升序还是降序?
分析:
Consumer<String> consumer =
text -> System.out.println(text);
回答:
- 为什么
text外面的括号可以省略? - 为什么
{}可以省略? - 为什么不需要
return?
7.3 手写代码
给出匿名内部类:
Arrays.sort(students, new Comparator<Student>() {
@Override
public int compare(
Student o1,
Student o2
) {
return Integer.compare(
o1.getAge(),
o2.getAge()
);
}
});
要求:
- 改成完整 Lambda。
- 省略参数类型。
- 转换为表达式体。
- 最终得到最简洁、仍然清晰的 Lambda。
分别实现:
Comparator<Student>
按照以下规则排序:
- 年龄升序。
- 年龄降序。
- 身高升序。
- 身高降序。
暂时全部使用 Lambda,不使用方法引用。
7.4 Debug
判断下面代码为什么错误:
Calculator calculator =
(int a, b) -> a + b;
修复代码。
判断:
Calculator calculator =
(a, b) -> {
a + b;
};
为什么不能作为:
int calculate(int a, int b);
的实现?
判断:
Calculator calculator =
(a, b) -> return a + b;
指出语法问题。
判断:
Printer printer =
text -> {
System.out.println(text)
};
指出缺少的语法元素。
7.5 综合训练
现有:
Student[] students
每个学生拥有:
name
age
height
score
要求分别实现:
- 按年龄升序。
- 按年龄降序。
- 按身高升序。
- 按成绩降序。
要求经历完整过程:
匿名内部类
↓
完整 Lambda
↓
省略参数类型
↓
表达式体
最后比较四个版本:
- 哪个最冗长?
- 哪个最直接?
- 哪些信息实际上已经可以由编译器推断?
- 哪些信息无论如何不能省略?
7.6 本章验收
闭卷完成:
Arrays.sort(students, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return Integer.compare(
o1.getAge(),
o2.getAge()
);
}
});
要求在不查询资料的情况下,将其逐步简化成最终 Lambda。
同时必须能够口述:
Lambda 参数类型什么时候能省?
什么时候能去掉 ()?
什么时候能去掉 {}?
什么时候必须同时去掉 return 和内部 ;?
Comparator 返回值到底表示什么?
为什么 Integer.compare 比直接减法更加稳健?
能够独立完成以上内容,才算真正掌握本章。