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 知识问答

  1. Lambda 参数类型为什么可以被省略?
  2. 两个参数时可以省略圆括号吗?
  3. 一个参数什么时候可以省略圆括号?
  4. 没有参数时能不能删除 ()
  5. Lambda 方法体有两条语句时可以删除 {} 吗?
  6. 有返回值 Lambda 在什么情况下可以删除 return
  7. Comparator<T> 为什么适合使用 Lambda?
  8. compare(o1, o2) 返回负数、零和正数分别表示什么?
  9. 为什么整数 Comparator 不推荐无条件使用 a - b
  10. Integer.compare(a, b) 解决了什么问题?

7.2 代码阅读

分析:

Comparator<Integer> comparator =
        (a, b) -> Integer.compare(a, b);

回答:

  1. ab 的类型是什么?
  2. 为什么代码中没有写参数类型?
  3. 如果 a < b,比较结果符号是什么?
  4. 该 Comparator 表示升序还是降序?

分析:

Consumer<String> consumer =
        text -> System.out.println(text);

回答:

  1. 为什么 text 外面的括号可以省略?
  2. 为什么 {} 可以省略?
  3. 为什么不需要 return

7.3 手写代码

给出匿名内部类:

Arrays.sort(students, new Comparator<Student>() {
    @Override
    public int compare(
            Student o1,
            Student o2
    ) {
        return Integer.compare(
                o1.getAge(),
                o2.getAge()
        );
    }
});

要求:

  1. 改成完整 Lambda。
  2. 省略参数类型。
  3. 转换为表达式体。
  4. 最终得到最简洁、仍然清晰的 Lambda。

分别实现:

Comparator<Student>

按照以下规则排序:

  1. 年龄升序。
  2. 年龄降序。
  3. 身高升序。
  4. 身高降序。

暂时全部使用 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 比直接减法更加稳健?

能够独立完成以上内容,才算真正掌握本章。