哎,跟你聊点掏心窝子的话。我见过太多开发者了,简历上写着“精通Java”,结果一到面试现场,问到Java 8那几个核心特性,要么支支吾吾背定义,要么写出跑不通的代码。Java 8发布于2014年,说老不老,说新也不新,但它就像那个基础扎实的师兄,至今依然是面试的“常客”。为什么?因为Lambda和Stream已经改写了Java编程的DNA,现在的业务代码里到处都是它们的身影。今天咱们不整那些枯燥的教科书式定义,我就像老朋友一样,带你把这事儿捋清楚,顺便把那些面试官最爱挖的坑给你指出来。
咱们先说说Lambda表达式。很多人一听这个名字就头大,觉得它玄乎。其实吧,Lambda说白了就是“匿名函数”的语法糖。你想想,以前在Java里,你想传一段行为给别的代码(比如按年龄排序一群用户),你得写个匿名内部类:
List<User> users = ...;
Collections.sort(users, new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
return Integer.compare(u1.getAge(), u2.getAge());
}
});
是不是感觉代码又臭又长,核心逻辑(比较年龄)被包裹在一大堆模板代码里?Lambda就是来拯救这种啰嗦的。有了Lambda,上面这段可以写成:
List<User> users = ...;
users.sort((u1, u2) -> Integer.compare(u1.getAge(), u2.getAge()));
看,清爽多了!(u1, u2) -> 就是Lambda的语法,箭头左边是参数,右边是表达式体。如果代码块只有一行,连大括号和return都可以省掉。这不仅是写起来舒服,更重要的是,它让“函数式编程”思想进入了Java的主流视野。
但别高兴太早,这里有个大坑,很多新手包括一些老手都会踩。Lambda里的this和外部类的this指向的是同一个对象,但Lambda不能修改外部类的成员变量(除非是final或事实上的final)。为啥?因为Lambda捕获的是变量的值,而不是变量本身。如果你在里面试图修改一个外部局部变量,编译器会直接报错,提示“变量必须是final或effectively final”。这设计是为了防止并发问题,因为Lambda经常被用在多线程环境下。举个例子:
int baseSalary = 5000;
// 下面这行代码会编译报错!
List<User> filtered = users.stream()
.filter(u -> {
baseSalary = 6000; // 错误:变量 baseSalary 从 final 类中访问,需要声明为 final
return u.getSalary() > baseSalary;
})
.collect(Collectors.toList());
所以,记住:Lambda喜欢“纯净”的数据,别试图在里面搞“副作用”操作去修改外部状态。
接下来,咱得聊聊Java 8的灵魂伴侣——Stream流处理。如果说Lambda是语法革新,那Stream就是处理数据的范式革命。以前你要对一堆数据做过滤、映射、归约,大概率得写个for循环,中间可能还嵌套几个if。Stream让你能像流水线一样,把数据“流”过去,经过一系列操作,最终得到结果。
// 传统方式:找出所有成年用户,提取他们的名字,按字母排序
List<String> names = new ArrayList<>();
for (User u : users) {
if (u.getAge() >= 18) {
names.add(u.getName());
}
}
Collections.sort(names);
// Stream方式:一气呵成
List<String> namesStream = users.stream()
.filter(u -> u.getAge() >= 18) // 过滤
.map(User::getName) // 映射,提取名字
.sorted() // 排序
.collect(Collectors.toList()); // 收集到列表
是不是感觉逻辑清晰得像读英语句子?filter、map、sorted、collect 这些都是Stream的中继操作(intermediate operations),它们都是懒执行的,只有遇到终止操作(terminal operation,比如collect、count、forEach)时,整个流水线才会启动。
避坑指南时间! Stream虽好,但用错了就是灾难。这里列举几个最常见的坑:
坑一:Stream只能消费一次!
Stream一旦被终止操作“消费”过,它就关闭了。再想用,就得重新获取。很多人写代码时不注意,以为能反复用,结果抛出IllegalStateException: stream has already been operated upon or closed。
Stream<User> userStream = users.stream();
long count = userStream.count(); // 第一次消费
List<User> list = userStream.collect(Collectors.toList()); // 报错!Stream已关闭
坑二:不要混用命令式风格!
Stream的目的是声明式编程,描述“做什么”,而不是“怎么做”。如果你在一个Stream操作里写复杂的逻辑,比如用forEach去修改外部变量,或者在里面做数据库查询,那就完全背离了Stream的初衷,代码变得难以维护和并行化。
// 错误示范:在Stream中做副作用操作
users.stream()
.forEach(u -> {
saveToDatabase(u); // 数据库操作应该在循环外,或者用专门的批量方法
totalSalary += u.getSalary(); // 修改外部变量,线程不安全且难以理解
});
坑三:并行Stream要小心!
parallelStream()看起来能提升性能,但并非万能。它适合计算密集型、数据量大、操作无状态的任务。但如果你的操作有副作用(比如修改共享状态),或者数据量很小(并行化的开销反而更大),它只会让代码更慢、更难调试。而且,parallelStream()默认使用ForkJoinPool.commonPool,如果多个地方都用,可能会互相抢占资源。
// 谨慎使用并行流
List<String> result = largeList.parallelStream()
.filter(...)
.map(...)
.collect(Collectors.toList());
坑四:忘记处理Optional!
Java 8引入了Optional类来处理空值问题,避免NullPointerException。但在Stream中,如果一个元素可能是空,直接调用方法可能会抛异常。比如,map(User::getName)如果getName()返回null,后续操作可能出问题。应该结合Optional来安全处理。
List<String> names = users.stream()
.map(u -> Optional.ofNullable(u.getName()).orElse("Unknown"))
.collect(Collectors.toList());
最后,说说其他重要特性,面试也常考:
- 接口默认方法(default methods):允许在接口中提供方法实现,方便在不破坏现有实现的情况下扩展接口。比如
Collection接口新增了stream()默认方法。 - 新的日期时间API(java.time包):
LocalDate、LocalTime、Instant等,线程安全、不可变,比老的Date和Calendar好用太多。 - 方法引用(Method Reference):Lambda的简洁写法,比如
User::getName等同于(u) -> u.getName()。 - 函数式接口(Functional Interface):只有一个抽象方法的接口,可以用Lambda表达式实现。
@FunctionalInterface注解可以强制检查。常见的如Predicate<T>(过滤)、Function<T,R>(映射)、Consumer<T>(消费)、Supplier<T>(供应)。
面试官喜欢问:“Lambda和匿名内部类有什么区别?” 你可以从语法简洁性、是否能访问外部非final变量、以及性能(Lambda通常更高效,因为JVM可以优化)这几个角度回答。
还有,“Stream的map和flatMap有什么区别?” map是一对一转换,flatMap是一对多转换,它会把多个流扁平化成一个个元素。比如,你有一个包含多个姓名列表的列表,用flatMap可以把所有姓名合并成一个列表。
说到底,Java 8的这些新特性不是为了炫技,而是为了解决实际问题:让代码更简洁、更易读、更安全、更能利用多核CPU。你在面试中展示对这些特性的深入理解,以及避坑经验,会大大加分。
希望这篇指南能帮到你。别光看不练,动手写几个例子,把那些坑都踩一遍,下次面试你绝对能从容应对。加油!
