1. Lambda 表达式与集合
回顾一下,在 Lambda 表达式出现之前我们是如何处理集合的。假设有一个字符串列表,我们想把它们打印到屏幕上:
List<String> list = Arrays.asList("Cat", "Dog", "Hedgehog");
for (String s : list) {
System.out.println(s);
}
这很简单,但如果我们想从列表中删除所有空字符串,就不得不写带条件的循环,有时还要使用迭代器(否则会抛出 ConcurrentModificationException)。再比如,按字符串长度排序:
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
哪怕是这样一个简单任务——也要写 5 行代码,加上一堆“噪声”括号。如何优化?答案你已经知道了:使用 Lambda 表达式。
在集合方法中使用 Lambda 表达式
自 Java 8 起,集合接口新增了接收函数式接口的方法——这意味着我们可以向其中传入 Lambda 表达式。最常用的有:
- forEach(Consumer<T> action)
- removeIf(Predicate<T> filter)
- sort(Comparator<T> c)
- replaceAll(UnaryOperator<T> operator)
示例:forEach
将列表的所有元素输出到屏幕(旧方式):
for (String s : list) {
System.out.println(s);
}
现在——用 Lambda:
list.forEach(s -> System.out.println(s));
或者更短,如果你想装一把“Java 大师”:
list.forEach(System.out::println); // method reference,我们稍后再讲
示例:removeIf
删除列表中的所有空字符串:
List<String> animals = new ArrayList<>(Arrays.asList("Cat", "", "Dog", "Hedgehog", ""));
animals.removeIf(s -> s.isEmpty());
System.out.println(animals); // [Cat, Dog, Hedgehog]
示例:sort
按字符串长度对列表排序:
List<String> animals = Arrays.asList("Cat", "Dog", "Hedgehog", "Elephant");
animals.sort((a, b) -> a.length() - b.length());
System.out.println(animals); // [Cat, Dog, Hedgehog, Elephant]
示例:replaceAll
将所有字符串转换为大写:
List<String> animals = new ArrayList<>(Arrays.asList("cat", "dog", "hedgehog"));
animals.replaceAll(s -> s.toUpperCase());
System.out.println(animals); // [CAT, DOG, HEDGEHOG]
2. Stream API 与 Lambda 表达式
随着 Java 8 的发布,出现了强大的 Stream API,用于以函数式风格处理集合。通过方法链,流可以进行过滤、转换、排序、收集等操作。而这些方法都接收 Lambda 表达式!
重要:对 Stream API 的完整讲解稍后再展开,这里只给出一些基础示例,帮助理解 Lambda 的作用。
示例:过滤
只保留长度大于 3 的字符串:
List<String> animals = Arrays.asList("cat", "elephant", "eel", "crocodile");
animals.stream()
.filter(s -> s.length() > 3)
.forEach(System.out::println); // elephant, crocodile
示例:转换(map)
把所有字符串变为大写:
List<String> animals = Arrays.asList("cat", "elephant", "hedgehog");
List<String> upper = animals.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
System.out.println(upper); // [CAT, ELEPHANT, HEDGEHOG]
示例:排序
获得按长度排序的新列表(不改变原列表):
List<String> animals = Arrays.asList("cat", "elephant", "eel", "crocodile");
List<String> sorted = animals.stream()
.sorted((a, b) -> a.length() - b.length())
.collect(Collectors.toList());
System.out.println(sorted); // [cat, eel, elephant, crocodile]
3. 与匿名类的对比
让我们比较一下,同样的代码用匿名类和用 Lambda 分别是什么样子。
按字符串长度排序
匿名类:
animals.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Lambda 表达式:
animals.sort((a, b) -> a.length() - b.length());
结论:
Lambda 表达式可以显著减少代码行数,让代码更易读。括号更少、噪声更少——更聚焦于核心逻辑!
删除空字符串
匿名类:
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
});
Lambda 表达式:
animals.removeIf(s -> s.isEmpty());
4. 实践:用 Lambda 操作用户列表的小练习
让我们在一个小程序中试试,用 Lambda 表达式操作用户列表。
示例 1:按年龄过滤用户
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = Arrays.asList(
new User("Alice", 17),
new User("Bob", 25),
new User("Charlie", 15)
);
users.stream()
.filter(u -> u.age >= 18)
.forEach(System.out::println); // Bob (25)
示例 2:按姓名排序用户
List<User> users = Arrays.asList(
new User("Alice", 17),
new User("Bob", 25),
new User("Charlie", 15)
);
users.sort((u1, u2) -> u1.name.compareTo(u2.name));
System.out.println(users);
// [Alice (17), Bob (25), Charlie (15)]
示例 3:将用户列表转换为姓名列表
List<String> names = users.stream()
.map(u -> u.name)
.collect(Collectors.toList());
System.out.println(names); // [Alice, Bob, Charlie]
示例 4:删除所有未成年人
List<User> users = new ArrayList<>(Arrays.asList(
new User("Alice", 17),
new User("Bob", 25),
new User("Charlie", 15)
));
users.removeIf(u -> u.age < 18);
System.out.println(users); // [Bob (25)]
5. 一些有用的细节
特性:作用域与“有效的 final”变量
Lambda 表达式可以使用外部方法中的变量,但前提是这些变量是 final 或“有效的 final”(即初始化后未被修改)。
int minAge = 18;
users.stream()
.filter(u -> u.age >= minAge)
.forEach(System.out::println);
如果你试图在 Lambda 使用之后再修改 minAge,编译器会报错。
表格:集合与流的常用方法及其 Lambda 用法
| 集合/流方法 | 用途 | Lambda 形态 | 示例 |
|---|---|---|---|
|
对每个元素执行操作 | |
|
|
按条件删除元素 | |
|
|
对元素排序 | |
|
|
替换每个元素 | |
|
|
过滤流 | |
|
|
映射/转换元素 | |
|
|
遍历流 | |
|
|
在流中排序 | |
|
7. 常见错误
错误 1:Lambda 过长。 如果你的 Lambda 里已经写了 5 行代码、条件、循环和 try-catch——很可能应该把逻辑提取到独立方法。Lambda 更适合短小的逻辑。
错误 2:在 Lambda 中使用可变的外部变量。 如果你试图在 Lambda 内修改外部方法中的变量(例如计数器),编译器不会允许。该变量必须是 final 或“有效的 final”。
错误 3:忘记集合/流的方法并不总是修改原集合。 例如,stream().filter(...) 并不会改变原有列表,而是返回一个新的流。如果你想得到集合,请使用 collect(Collectors.toList())。
错误 4:Lambda 表达式的类型不匹配。 如果方法接收的是 Comparator<T>,而你传入的是只有一个参数的 Lambda(而不是两个),就会产生编译错误。
错误 5:过度嵌套导致可读性下降。 如果你有一连串 map、filter、forEach,并且每个 Lambda 里还有 Lambda——代码将变得很难读。这种情况下最好把表达式拆成若干步骤,或提取到方法中。
GO TO FULL VERSION