Java SE 8核心特性实战自测,你的Lambda与Stream掌握度如何?
1. Lambda表达式:从匿名类到函数式编程的进化
Java SE 8最革命性的特性莫过于Lambda表达式了。记得我第一次看到这种写法时,整个人都是懵的——这跟传统的Java代码风格差别太大了。但用顺手之后才发现,这简直是集合操作的"瑞士军刀"。
先来看个典型场景:我们要给按钮添加点击事件。在Java 8之前得这么写:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("按钮被点击了");
}
});
而有了Lambda之后,代码可以简化为:
button.addActionListener(e -> System.out.println("按钮被点击了"));
这种转变不仅仅是语法糖,更代表着编程思维的转变。Lambda表达式的核心结构是:
(参数列表) -> { 方法体 }
当方法体只有一行时,花括号和return都可以省略。我在实际项目中踩过的坑是:如果方法体有多行语句,必须用花括号包裹,否则编译会报错。
2. Stream API:让集合操作行云流水
如果说Lambda是子弹,那么Stream就是枪。两者配合使用才能真正发挥Java 8的威力。我经常用Stream来处理各种集合数据,比如要从员工列表中找出所有薪资超过1万的开发工程师:
List<Employee> highPaidDevs = employees.stream()
.filter(e -> e.getSalary() > 10000)
.filter(e -> "Developer".equals(e.getPosition()))
.collect(Collectors.toList());
Stream的操作分为中间操作和终端操作两类。常见的中间操作包括:
- filter():过滤元素
- map():元素转换
- sorted():排序
- distinct():去重
终端操作则会触发实际计算,比如:
- collect():收集结果
- forEach():遍历元素
- count():计数
- reduce():归约运算
有个容易掉坑的地方:Stream是"一次性"的,执行完终端操作后就不能再用了。我有次尝试重复使用同一个Stream对象,结果抛出了IllegalStateException。
3. 方法引用:让代码更简洁的语法糖
方法引用是Lambda的孪生兄弟,本质上也是函数式接口的实例。当Lambda只是调用已有方法时,可以用方法引用进一步简化代码。比如:
// Lambda写法
list.forEach(s -> System.out.println(s));
// 方法引用写法
list.forEach(System.out::println);
方法引用有四种形式:
- 类::静态方法(Math::max)
- 对象::实例方法(System.out::println)
- 类::实例方法(String::length)
- 类::new(Employee::new)
我在代码审查时发现,很多同事会混淆第三种和第四种形式。记住:带new的是构造器引用,不带new的是实例方法引用。
4. 实战陷阱:这些坑我都踩过
4.1 Lambda中的变量捕获
Lambda可以捕获外围的final或等效final的局部变量。但要注意,在Lambda内部修改这些变量会导致编译错误:
int count = 0;
list.forEach(e -> {
count++; // 编译错误!
});
解决方案是使用原子类如AtomicInteger,或者重构代码避免修改外部变量。
4.2 并行流的线程安全问题
并行流(parallelStream)虽然能利用多核优势,但容易引发线程安全问题:
List<Integer> unsafeList = new ArrayList<>();
IntStream.range(0, 10000).parallel()
.forEach(unsafeList::add); // 可能抛出ArrayIndexOutOfBoundsException
正确做法是使用线程安全的收集器:
List<Integer> safeList = IntStream.range(0, 10000).parallel()
.boxed()
.collect(Collectors.toList());
4.3 空指针异常链
Stream操作链中任何一个环节出现null都会导致NPE:
List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
.map(String::toUpperCase) // 在null元素处抛出NPE
.forEach(System.out::println);
解决方法是用Optional包装或者提前过滤null值:
names.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.forEach(System.out::println);
5. 性能优化小贴士
经过大量实测,我总结出一些Stream性能优化的经验:
- 对于小数据集(<1000元素),普通循环往往比Stream更快
-
避免在Stream链式调用中重复计算
// 不好的写法 list.stream().filter(e -> e.length() > 3).map(e -> e.length())... // 好的写法 list.stream().map(e -> e.length()).filter(len -> len > 3)... - 并行流适合CPU密集型操作,IO密集型任务反而可能更慢
-
预分配集合大小能提升collect性能:
list.stream().collect(Collectors.toCollection(() -> new ArrayList<>(list.size())));
6. 经典面试题解析
来看几道考察Lambda和Stream掌握程度的典型题目:
题目1 :下面的代码输出是什么?
List<String> list = Arrays.asList("a", "b", "c");
String result = list.stream()
.reduce("", (partial, elem) -> partial + elem.toUpperCase());
System.out.println(result);
答案:输出"ABC"。reduce的第一个参数是初始值,Lambda表达式将每个元素转为大写并拼接。
题目2 :如何用Stream统计文本中每个单词的出现频率?
解决方案:
Map<String, Long> wordCount = Files.lines(Paths.get("text.txt"))
.flatMap(line -> Arrays.stream(line.split("\\W+")))
.filter(word -> !word.isEmpty())
.collect(Collectors.groupingBy(
String::toLowerCase,
Collectors.counting()
));
题目3 :下面的方法引用等价于哪个Lambda表达式?
Function<String, Integer> parser = Integer::parseInt;
答案:等价于:
Function<String, Integer> parser = s -> Integer.parseInt(s);
7. 工具与技巧
工欲善其事,必先利其器。推荐几个我日常使用的工具:
- JShell :Java 9引入的REPL工具,特别适合测试Lambda表达式
- IntelliJ IDEA :对Lambda和方法引用有强大的重构支持
- VisualVM :分析Stream操作的性能瓶颈
-
JUnit 5
:配合Lambda写测试更简洁:
assertAll( () -> assertEquals(4, calculator.add(2, 2)), () -> assertTrue(list.contains("key")) );
调试Lambda时有个小技巧:可以先写成完整的匿名类形式,调试通过后再转换为Lambda。IDEA的"Show Context Actions"(Alt+Enter)能自动完成这种转换。
更多推荐
所有评论(0)