背景

本文是《Java 后端从小白到大神》修仙系列第十三篇,正式进入Java后端世界,本篇文章主要聊Java基础。若想详细学习请点击首篇博文,我们开始把。

文章概览

  1. Lambda表达式

1. Lambda 表达式定义

Lambda 表达式是 Java 8 引入的一种 匿名函数,用于简化函数式接口(只有一个抽象方法的接口)的实现。

  • 核心作用:替代冗余的匿名内部类,使代码更简洁。
  • 核心思想:将函数作为参数传递,支持函数式编程。

2. 语法格式

1
(参数列表) -> { 代码体 }
  • 参数列表:可以是空参数、单参数或多参数,类型可省略(编译器自动推断)。
  • 箭头符号-> 分隔参数和代码体。
  • 代码体:单行代码可省略 {}return,多行需明确写 {}return

语法示例

场景 Lambda 写法 等效匿名内部类写法
无参数,无返回值 () -> System.out.println("Hello") new Runnable() { run() { ... }}
单参数,无类型 s -> System.out.println(s)
多参数,带类型 (int a, int b) -> a + b
多行代码体 (a, b) -> { int c = a + b; return c; }

3. 使用技巧

1. 简化代码

对比传统匿名内部类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 传统写法
Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("Running");
    }
};

// Lambda 写法
Runnable r = () -> System.out.println("Running");

2. 方法引用(Method Reference)

进一步简化 Lambda,使用 :: 符号:

1
2
List<String> list = Arrays.asList("A", "B", "C");
list.forEach(System.out::println); // 等效于 s -> System.out.println(s)

3. 变量捕获

Lambda 可以访问外部的 final 或等效 final 的局部变量。

1. 为什么需要有效 final

  • 线程安全:如果多个线程同时执行同一个 Lambda 表达式,并且该表达式访问了外部变量,那么这些变量的状态必须是确定的,以避免竞争条件。
  • 编译器优化:编译器可以在知道变量不会改变的情况下进行某些优化,提高性能。
  • 代码可读性和维护性:限制变量的可变性使得代码更易于理解和维护。
1
2
3
int offset = 10;
List<Integer> nums = Arrays.asList(1, 2, 3);
nums.forEach(n -> System.out.println(n + offset)); // offset 必须是 final

2. 不允许修改捕获的局部变量

1
2
3
4
int offset = 10;
List<Integer> nums = Arrays.asList(1, 2, 3);
nums.forEach(n -> System.out.println(n + offset));
offset = 20; // 编译错误:不允许修改捕获的局部变量

3. 在 Lambda 表达式中使用可变的局部变量,解决方法

使用包装类:将变量封装在一个对象中,这样可以在不违反 有效 final 规则的情况下修改对象的属性。

1
2
3
4
5
6
7
8
9
class OffsetHolder {
    int value;
    OffsetHolder(int value) { this.value = value; }
}

OffsetHolder holder = new OffsetHolder(10);
List<Integer> nums = Arrays.asList(1, 2, 3);
nums.forEach(n -> System.out.println(n + holder.value));
holder.value = 20; // 允许修改 holder.value

使用数组:将变量放在数组中,数组本身是 有效 final 的,但数组的内容可以修改。

1
2
3
4
int[] offsetArray = {10};
List<Integer> nums = Arrays.asList(1, 2, 3);
nums.forEach(n -> System.out.println(n + offsetArray[0]));
offsetArray[0] = 20; // 允许修改数组内容

4. 结合 Stream API

Lambda 常用于集合操作:

1
2
3
4
5
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
                               .filter(n -> n % 2 == 0) // 过滤偶数
                               .map(n -> n * n)        // 平方
                               .collect(Collectors.toList()); // 收集结果

4. 使用场景

1. 替代匿名内部类

  • RunnableComparatorClickListener 等接口。
1
2
3
// 排序示例
List<String> names = Arrays.asList("Bob", "Alice", "Tom");
names.sort((s1, s2) -> s1.compareTo(s2)); // 按字母顺序排序

2. 函数式接口参数

1. 什么是函数式接口

函数式接口(Functional Interface)是 Java 中只包含一个抽象方法的接口。从 Java 8 开始,函数式接口可以使用 Lambda 表达式进行实例化,从而简化代码编写。函数式接口通常用于表示可传递的行为或逻辑

  • 特点:
    • 只有一个抽象方法(可以有多个默认方法和静态方法)。
    • 使用 @FunctionalInterface 注解标记(虽然不是必须的,但推荐使用以确保编译器检查)。
2. Java 提供了一些常用的内置函数式接口
接口 方法签名 用途
Runnable void run() 定义无参数、无返回值的任务。
Callable V call() 定义无参数、带返回值的任务,支持抛出异常。
Consumer void accept(T t) 接受单个输入参数并返回无结果的操作。
Supplier T get() 不接受参数但返回结果的函数。
Function<T, R> R apply(T t) 接受单个输入参数并返回结果的函数。
Predicate boolean test(T t) 接受单个输入参数并返回布尔值的函数,用于条件判断。
BiFunction<T, U, R> R apply(T t, U u) 接受两个输入参数并返回结果的函数。

示例代码

 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
// Runnable接口
Runnable task = () -> System.out.println("Running a task");
new Thread(task).start();

// Callable接口
Callable<Integer> task = () -> {
    System.out.println("Running a task with result");
    return 42;
};
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();
System.out.println("Result: " + futureTask.get());

// Consumer接口
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 这里定义了一个 Consumer 实例 printName,它接受一个 String 参数并打印出来
Consumer<String> printName = name -> System.out.println(name);
// forEach 方法会遍历 names 列表中的每个元素,并对每个元素调用 printName.accept(name)
names.forEach(printName);
// 显示调用accept方法
for (String name : names) {
    printName.accept(name);
}

// Supplier接口
Supplier<String> randomString = () -> UUID.randomUUID().toString();
System.out.println("Random String: " + randomString.get());

// Function接口
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Function<Integer, Integer> square = x -> x * x;
List<Integer> squares = numbers.stream().map(square).collect(Collectors.toList());
System.out.println(squares); // [1, 4, 9, 16, 25]

// Predicate接口
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Predicate<Integer> isEven = x -> x % 2 == 0;
List<Integer> evenNumbers = numbers.stream().filter(isEven).collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4]

// BiFunction接口
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println("Sum: " + add.apply(3, 5)); // 8
3. 函数式接口的参数

函数式接口的参数是指在方法签名中定义的参数类型。这些参数可以是任意类型,包括基本类型、对象类型或其他函数式接口。通过 Lambda 表达式,你可以简洁地传递行为作为参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class PredicateExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // 使用 Predicate 接口作为参数
        filter(numbers, n -> n % 2 == 0); // 过滤偶数
    }

    public static void filter(List<Integer> list, Predicate<Integer> predicate) {
        for (Integer n : list) {
            if (predicate.test(n)) {
                System.out.print(n + " ");
            }
        }
        System.out.println();
    }
}
4.定义自定义函数式接口

定义一个自定义函数式接口,只需创建一个接口并在其中声明一个抽象方法。建议使用 @FunctionalInterface 注解来确保接口符合函数式接口的要求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@FunctionalInterface
interface MathOperation {
    int operate(int a, int b);
}

public class CustomFunctionalInterfaceExample {
    public static void main(String[] args) {
        // 使用自定义函数式接口
        MathOperation addition = (a, b) -> a + b;
        MathOperation subtraction = (a, b) -> a - b;

        System.out.println("Addition: " + operate(10, 5, addition));
        System.out.println("Subtraction: " + operate(10, 5, subtraction));
    }

    public static int operate(int a, int b, MathOperation operation) {
        return operation.operate(a, b);
    }
}

3. 事件驱动编程

  • 如 GUI 事件监听。
1
button.addActionListener(e -> System.out.println("按钮被点击!"));

4. 并行处理

  • 结合 Stream 实现并行计算。
1
2
3
4
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
int sum = list.parallelStream() // 并行流
              .mapToInt(n -> n)
              .sum();

5. 代码示例

示例 1:线程启动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 传统写法
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("线程运行中");
    }
}).start();

// Lambda 写法
new Thread(() -> System.out.println("线程运行中")).start();

示例 2:过滤集合

1
2
3
4
List<String> languages = Arrays.asList("Java", "Python", "C++", "JavaScript");
List<String> filtered = languages.stream()
                                 .filter(lang -> lang.startsWith("J"))
                                 .collect(Collectors.toList()); // 输出 ["Java", "JavaScript"]

示例 3:自定义函数式接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@FunctionalInterface
interface StringProcessor {
    String process(String str);
}

public static void main(String[] args) {
    StringProcessor upperCase = s -> s.toUpperCase();
    StringProcessor reverse = s -> new StringBuilder(s).reverse().toString();
    
    System.out.println(upperCase.process("hello")); // 输出 "HELLO"
    System.out.println(reverse.process("hello"));   // 输出 "olleh"
}

6. 注意事项

  1. 函数式接口限制:Lambda 只能用于单抽象方法的接口(可通过 @FunctionalInterface 注解标记)。
  2. 变量捕获限制:Lambda 内部不可修改外部非 final 变量。
  3. 简洁性优先:避免编写过长的 Lambda 表达式,复杂逻辑可封装为方法。

总结

通过 Lambda 表达式,Java 实现了更简洁的函数式编程风格,特别适合集合操作、异步任务和事件处理等场景,熟练掌握。