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);

方法引用有四种形式:

  1. 类::静态方法(Math::max)
  2. 对象::实例方法(System.out::println)
  3. 类::实例方法(String::length)
  4. 类::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性能优化的经验:

  1. 对于小数据集(<1000元素),普通循环往往比Stream更快
  2. 避免在Stream链式调用中重复计算
    // 不好的写法
    list.stream().filter(e -> e.length() > 3).map(e -> e.length())...
    
    // 好的写法
    list.stream().map(e -> e.length()).filter(len -> len > 3)...
    
  3. 并行流适合CPU密集型操作,IO密集型任务反而可能更慢
  4. 预分配集合大小能提升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. 工具与技巧

工欲善其事,必先利其器。推荐几个我日常使用的工具:

  1. JShell :Java 9引入的REPL工具,特别适合测试Lambda表达式
  2. IntelliJ IDEA :对Lambda和方法引用有强大的重构支持
  3. VisualVM :分析Stream操作的性能瓶颈
  4. JUnit 5 :配合Lambda写测试更简洁:
    assertAll(
        () -> assertEquals(4, calculator.add(2, 2)),
        () -> assertTrue(list.contains("key"))
    );
    

调试Lambda时有个小技巧:可以先写成完整的匿名类形式,调试通过后再转换为Lambda。IDEA的"Show Context Actions"(Alt+Enter)能自动完成这种转换。

更多推荐