JDK8新特性实战(Lambda、Stream、Optional)

JDK8 新特性实战(Lambda / Stream / Optional)

对应学习计划:第一月第1周「Java 基础 + 集合源码」

面试定位:JDK8 是后端开发的事实标准,Lambda/Stream/Optional 是必须熟练的基础能力,几乎每次面试都会被问到。


一、本文能帮你回答的面试题

  1. Lambda 表达式的本质是什么?为什么说它只是「语法糖」?
  2. @FunctionalInterface 是什么?哪些是 JDK 自带的函数式接口?
  3. Stream 的中间操作和终止操作有什么区别?map 和 flatMap 区别?
  4. Stream 为什么不能重复使用?
  5. Optional 是什么?怎么用?什么时候不该用?
  6. 方法引用有哪四种形式?
  7. 接口默认方法(default method)解决了什么问题?

二、概念介绍

JDK8 于 2014 年发布,是 Java 历史上最重要的一次版本升级,核心变化包括:

新特性 作用
Lambda 表达式 把「匿名内部类」的样板代码压缩为一行
函数式接口 Lambda 的「类型」载体
Stream API 用声明式方式处理集合,支持链式调用 + 并行流
Optional 优雅处理空指针(NPE)
方法引用 :: 语法,进一步简化 Lambda
接口默认方法/静态方法 给接口增加方法而不破坏实现类
新时间 API LocalDate/LocalDateTime 替代 Date/Calendar
CompletableFuture 异步编程(第10篇详细讲)

一句话总结:JDK8 让 Java 从「面向对象的语言」进化为「也能写出函数式风格的代码」的语言。


三、原理深入

3.1 Lambda 的本质:是语法糖,不是新指令

java 复制代码
// 写法1:传统匿名内部类
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

// 写法2:Lambda(等价)
Runnable r2 = () -> System.out.println("Hello");

原理 :Lambda 表达式编译后会被转换为 invokedynamic 指令 + 生成一个内部的函数式接口实现,最终仍是一个对象。它没有引入新的字节码指令,所以叫「语法糖」。

3.2 函数式接口:Lambda 的「身份证」

Lambda 能用的前提:接口中只有一个抽象方法 。用 @FunctionalInterface 注解标记(不加也能用,加了会在编译期校验)。

JDK8 自带四大核心函数式接口(背下来):

接口 参数 返回值 典型场景
Predicate<T> T boolean 过滤条件
Function<T,R> T R 类型转换
Consumer<T> T void 遍历消费
Supplier<T> 无 T 工厂创建对象

3.3 Stream 两段式设计:中间操作 vs 终止操作

复制代码
数据源 (集合/数组) → 中间操作链 (惰性) → 终止操作 (触发执行)
  • 中间操作 (lazy):filter / map / flatMap / sorted / distinct / limit / skip,不执行,只是记录到流水线。
  • 终止操作 (eager):forEach / collect / count / reduce / anyMatch / findFirst,一调用就触发整条流水线执行。

为什么这么设计?

  • 惰性求值可以避免不必要的中间结果(像 SQL 执行计划一样优化)。
  • 全部在终止操作的时候一次遍历完成,减少遍历次数。

⚠️ 坑 :没有终止操作的 Stream 不会执行,写 list.stream().filter(x -> x > 3) 是不生效的。

3.4 map vs flatMap

java 复制代码
// map:一对一映射,结果是 List<String[]>
List<String[]> m = words.stream()
        .map(w -> w.split(""))
        .collect(Collectors.toList());

// flatMap:一对多展平,结果是 List<String>
List<String> fm = words.stream()
        .flatMap(w -> Arrays.stream(w.split("")))
        .collect(Collectors.toList());

记忆口诀 :map 结果是「一个进一个出」,flatMap 结果是「一个进、多个出且被摊平」。

3.5 Optional:用类型系统消灭 NPE

java 复制代码
// 传统写法:多层 null 判断,容易漏
public String getCity(User user) {
    if (user != null) {
        Address addr = user.getAddress();
        if (addr != null) {
            return addr.getCity();
        }
    }
    return "未知";
}

// Optional 链式写法:不优雅地兜底
public String getCity(User user) {
    return Optional.ofNullable(user)
            .map(User::getAddress)
            .map(Address::getCity)
            .orElse("未知");
}

Optional 常用 API:

方法 作用
of(obj) 包非空值,为 null 直接抛 NPE
ofNullable(obj) 可包装 null 值
orElse(v) 为空返回默认值(无论空不空都会先计算)
orElseGet(() -> v) 为空才执行 Supplier 求值(推荐,惰性)
orElseThrow(Supplier) 为空抛自定义异常
map() / flatMap() 值转换
ifPresent() / filter() 条件消费 / 过滤

⚠️ 面试加分点(装逼) :orElse 和 orElseGet 的区别 ------ orElse 的参数是已经求值好的对象 ,即使 Optional 非空也会执行;orElseGet 是 Supplier,只在为空时求值。性能敏感场景(如创建昂贵对象)必须用 orElseGet。

3.6 方法引用四种形式

形式 语法 示例
静态方法 类名::静态方法 Math::max
实例方法(特定对象) 对象::实例方法 System.out::println
任意对象实例方法 类名::实例方法 String::toUpperCase
构造器 类名::new ArrayList::new

3.7 接口 default 方法:多继承问题的折中

java 复制代码
interface Vehicle {
    void run();
    // 新加的方法不影响已有实现类
    default void honk() { System.out.println("滴滴"); }
    static void info() { System.out.println("我是车"); }
}

解决的问题 :给接口加新方法(如集合的 forEach)时,不用强制所有实现类修改。

引入的问题 :接口默认方法多继承会有歧义,需要 X.super.方法() 显式指定。


四、实战例子

实战 1:用 Stream 完成「按年龄分组统计」

java 复制代码
import java.util.*;
import java.util.stream.*;

public class StreamDemo {
    record Person(String name, int age, String city) {}

    public static void main(String[] args) {
        List<Person> people = List.of(
                new Person("张三", 18, "北京"),
                new Person("李四", 25, "上海"),
                new Person("王五", 30, "北京"),
                new Person("赵六", 25, "深圳"));

        // 1. 按城市分组
        Map<String, List<Person>> byCity = people.stream()
                .collect(Collectors.groupingBy(Person::city));
        System.out.println(byCity);

        // 2. 过滤年龄 >= 25,转成名字列表,按名字排序
        List<String> names = people.stream()
                .filter(p -> p.age() >= 25)
                .map(Person::name)
                .sorted()
                .collect(Collectors.toList());
        System.out.println(names);

        // 3. 统计平均年龄
        double avg = people.stream()
                .mapToInt(Person::age)
                .average()
                .orElse(0);
        System.out.println("平均年龄: " + avg);

        // 4. 取年龄最大的前2个人
        List<Person> top2 = people.stream()
                .sorted(Comparator.comparingInt(Person::age).reversed())
                .limit(2)
                .collect(Collectors.toList());
        System.out.println(top2);
    }
}

实战 2:Optional 实战------用户服务

java 复制代码
public class UserService {
    // 模拟数据库查询,可能返回 null
    public User findById(Long id) { return null; }

    // 推荐写法:链式 + 兜底 + 显式异常
    public AddressDto getAddress(Long userId) {
        return Optional.ofNullable(findById(userId))
                .map(User::getAddress)
                .map(a -> new AddressDto(a.getCity(), a.getStreet()))
                .orElseThrow(() -> new RuntimeException("用户地址不存在: " + userId));
    }

    // 集合操作里替代 for + if null
    public List<String> getCityList(List<User> users) {
        return users.stream()
                .filter(Objects::nonNull)
                .map(User::getAddress)
                .filter(Objects::nonNull)   // 防止 Address 为 null
                .map(Address::getCity)
                .distinct()
                .collect(Collectors.toList());
    }
}

实战 3:并行流(Parallel Stream)

java 复制代码
// 大数据量计算用并行流,内部走 ForkJoinPool 公共池
long sum = LongStream.rangeClosed(1, 10_000_000L)
        .parallel()           // 注意:list.parallelStream() 也行
        .sum();
System.out.println(sum);

⚠️ 坑 :并行流共享全局 ForkJoinPool,遇到 IO 阻塞任务会堵死其他并行流;数据量小的时候并行反而更慢(有切换开销)。不要在业务中盲目用 parallel()。


五、面试话术(背下来)

Q:Lambda 为什么快/是什么?

「Lambda 是一个语法糖,本质是编译成 invokedynamic 指令,把匿名内部类转换成 JVM 能惰性生成的函数式接口实现,避免每次创建匿名内部类都生成一个类文件,减少类加载和对象创建开销。」

Q:Stream 中间操作和终止操作的区别?

「中间操作是惰性的,只是把操作记在流水线上不执行;终止操作一调用,整条流水线一次性触发。好处一是能优化执行计划避免多余遍历,二是可以短路优化,比如 limit(10) 找到 10 个就停止。」

Q:怎么防 NPE?

「能用 Optional.ofNullable 链式处理就用,避免多层 if null;对象字段设计上尽量不让它为空,用 orElseGet 做惰性兜底。」

Q:orElse 和 orElseGet?

「orElse 参数已求值好的对象,无论 Optional 空不空都执行创建;orElseGet 传 Supplier,只有为空时才创建。创建昂贵对象必须用 orElseGet。」

Q:并行流注意什么?

「并行流基于公共 ForkJoinPool,占用 IO 的任务会互相影响;数据量小并行更慢;线程安全也要自己保证。不建议在 Web 应用里无脑 parallelStream。」


六、踩坑总结(易错点)

# 坑 说明
1 忘写终止操作 Stream 不会执行,代码「看起来对」但没跑
2 Stream 不可复用 stream.forEach(...) 后 stream 已关闭,再操作抛 IllegalStateException
3 orElse 提前求值 传 new Object() 会无条件创建对象
4 并行流乱用 公共池互相干扰 + 数据量大才有收益
5 Lambda 里修改外部变量 外部局部变量必须 final 或 effectively final
6 map 后仍有 null map 不自动判空,结果可能为「null 值的元素」,接 filter(Objects::nonNull)
7 groupingBy 的 key 为 null Collectors.groupingBy 默认 key 不能为 null,会 NPE

七、课后作业

  1. 用 groupingBy + counting() 统计一个单词列表的词频,并输出 Top3。
  2. 用自己的话解释为什么 Stream 只能消费一次(提示:Spliterator 状态)。
  3. 用 reduce 手写一个「求最大数」的聚合操作,对比 max() 的写法差异。
  4. 项目里找一个 for 循环,改写成 Stream,对比可读性。
相关推荐
鱼宵1 小时前
工厂模式实战:消灭魔法数字,把写死的 new 收进一个厂里
开发语言·python
JosieBook1 小时前
【多线程】两台机器同时抢一个字典:一次并发 Bug 的排查
java·网络·bug
niucloud-admin1 小时前
NIUCLOUD LITE AI开发文档--后台系统结构
开发语言
牛战士coding1 小时前
LLM 选错工具怎么办?提示词之外还需要确定性规则兜底
java·人工智能
今天的砖头有点烫手啊1 小时前
Java 序列化到底怎么选?从 Serializable 到 Protobuf 的演进与选型
java·json
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 登录发送验证码的测试验证与经验复盘
java·开发语言·交友
夕除1 小时前
redis--主从还是集群,内存
java·spring·mybatis
百数平台1 小时前
低代码安全技术实践:表单 / 流程 / 报表三级权限体系设计解析
java·大数据·数据库
白露与泡影2 小时前
Spring Boot 实现验证码:Hutool-captcha、AJ-Captcha、EasyCaptcha 到底怎么选?
java·spring boot·后端