lambda表达式练习案例
前言
在Java 8问世之前,我们如果想给某个方法传入一段"行为",只能通过匿名内部类来实现——创建一个接口的匿名实现对象,然后在里面覆盖方法。这种写法虽然能完成任务,但代码臃肿、可读性差,充斥着大量与核心逻辑无关的样板代码。
Lambda表达式的出现,正是为了解决这个问题。它让我们可以用极其简洁的语法来表达"行为",把注意力从"对象"转移到"操作"本身。
本文通过"两数之和"和"找出偶数"两个实战练习,从匿名内部类写法逐步过渡到Lambda表达式写法,在对比中带你理解:
Lambda到底简化了什么?
为什么它能这样简化?
什么情况下能用Lambda,什么情况下不行?
一步步揭开Lambda表达式背后的设计思想和适用边界。
摘要
本文通过两个实战练习,系统讲解Lambda表达式如何优化匿名内部类的冗余写法。首先在"两数之和"和"找出偶数"练习中,分别展示匿名内部类与Lambda两种实现方式的代码对比,直观呈现Lambda在代码量上的简化效果。进而深入分析简化的本质原因:Lambda只能替换函数式接口(即只有一个抽象方法的接口)的匿名内部类,因为它省略了对象创建、方法名声明、参数类型、返回值类型等所有编译器可推断的冗余信息,只保留核心操作逻辑。文章进一步总结Lambda的三个简化层次和适用前提条件,并指出这种写法背后是从"面向对象"到"面向行为"的思维转变。最后通过延伸思考引导读者继续深入学习自定义函数式接口、Stream流式编程和方法引用等进阶内容。
目录
一、练习1:两数之和
匿名内部类写法
优化为lambda表达式
二、练习2:找出偶数
匿名内部类写法
优化为lambda表达式
思考:lambda是如何优化匿名内部类写法的?
1. 从两个练习中找规律
2. 什么是函数式接口?
3. Lambda为什么能替换匿名内部类?
4. 从面向对象到面向行为的思维转变
5. Lambda的三个简化层次
6. 使用Lambda的前提条件
7. 延伸思考
一、练习1:两数之和
匿名内部类写法
public class Main { public static void main(String[] args) { //直接调用方法,获取结果 int i = calculateNum(new IntBinaryOperator() { @Override public int applyAsInt(int left, int right) { return left + right; } }); //打印结果 System.out.println("计算结果:" + i); } //定义一个静态方法,接收一个IntBinaryOperator参数(该参数本质是一个jdk自带的接口) public static int calculateNum(IntBinaryOperator operator){ int a = 10; int b = 20; //返回a+b的和 return operator.applyAsInt(a, b); } }运行结果如下:
注意:
- 上面静态方法的参数,是jdk自带的一个接口,它既然要求接收一个该类型的接口,那我们直接创建一个接口(并实现了下面的applyAsInt方法)传进去即可。
- 不难发现,该接口下面,只有一个方法,这就叫“函数式接口”。这也是能进行lambda优化的最关键一点。
优化为lambda表达式
public class Main { public static void main(String[] args) { //直接调用方法,获取结果 int i = calculateNum((int left, int right)-> { return left + right; }); //打印结果 System.out.println("计算结果:" + i); } //定义一个静态方法,接收一个IntBinaryOperator参数(该参数本质是一个jdk自带的接口) public static int calculateNum(IntBinaryOperator operator){ int a = 10; int b = 20; //返回a+b的和 return operator.applyAsInt(a, b); } }二、练习2:找出偶数
匿名内部类写法
public class Main { public static void main(String[] args) { //调用静态方法 printNum(new IntPredicate() { @Override public boolean test(int value) { return value%2==0;//定标准:偶数则返回true,奇数则返回false } }); } //定义一个静态方法,接收一个IntPredicate参数(该参数本质是一个jdk自带的接口) public static void printNum(IntPredicate predicate){ int[] arr = {1,2,3,4,5,6,7,8,9}; //筛选出arr中,符合要求的数字 for(int i: arr){ if(predicate.test(i)){ System.out.println(i);//打印出符合要求的数字 } } } }运行结果如下:
优化为lambda表达式
public class Main { public static void main(String[] args) { //调用静态方法 printNum((int value) -> { return value%2==0;//定标准:偶数则返回true,奇数则返回false }); } //定义一个静态方法,接收一个IntPredicate参数(该参数本质是一个jdk自带的接口) public static void printNum(IntPredicate predicate){ int[] arr = {1,2,3,4,5,6,7,8,9}; //筛选出arr中,符合要求的数字 for(int i: arr){ if(predicate.test(i)){ System.out.println(i);//打印出符合要求的数字 } } } }运行结果如下:
思考:lambda是如何优化匿名内部类写法的?
通过上面两个练习的对比,我们直观地感受到了Lambda表达式带来的代码简化。但作为学习者,我们不能只停留在"会用"的层面,更要理解它为什么能这样简化,以及简化的边界在哪里。
1. 从两个练习中找规律
回顾练习1和练习2,你会发现一个共同点:
| 练习1 | 练习2 | |
|---|---|---|
| 自定义方法形参 | IntBinaryOperator operator | IntPredicate predicate |
| 形参类型 | 接口 | 接口 |
| 接口抽象方法 | int applyAsInt(int left, int right) | boolean test(int value) |
| 匿名内部类写法 | new IntBinaryOperator() { ... } | new IntPredicate() { ... } |
| Lambda写法 | (left, right) -> left + right | value -> value % 2 == 0 |
规律很清晰:当方法的形参是一个接口时,我们就可以用Lambda表达式来简化传参。
但这里有一个更精确的结论——不是所有接口都可以用Lambda替换,只有"函数式接口"才行。
2. 什么是函数式接口?
函数式接口,简单来说就是只有一个抽象方法的接口。
IntBinaryOperator:只有一个抽象方法applyAsIntIntPredicate:只有一个抽象方法test
这两个都是JDK自带的函数式接口,它们都标记了一个注解@FunctionalInterface(这个注解不是必须的,但加上后编译器会帮你检查是否只有一个抽象方法)。
注意:接口中可以存在多个方法,但只要抽象方法只有一个,它就是函数式接口。默认方法(
default修饰)和静态方法不影响。
3. Lambda为什么能替换匿名内部类?
我们来剖析一下匿名内部类的写法:
new IntBinaryOperator() { @Override public int applyAsInt(int left, int right) { return left + right; } }这段代码包含了哪些信息?
对象创建:
new IntBinaryOperator() { ... }—— 创建一个接口的匿名实现类对象方法声明:
public int applyAsInt(int left, int right)—— 方法名、返回值、参数列表方法体:
return left + right;—— 真正的业务逻辑
而Lambda表达式只写了什么?
(left, right) -> left + rightLambda把冗余信息全部省略了:
| 被省略的内容 | 原因 |
|---|---|
new 接口名() { ... } | 编译器知道这里需要一个接口的实现,不需要显式new |
方法名applyAsInt | 接口只有一个抽象方法,默认就是实现它,无需指名 |
返回值类型int | 编译器能从上下文推断出返回int |
参数类型int | 编译器能从接口方法定义推断出参数类型 |
{}和return | 只有单条语句时可以省略 |
() | 只有一个参数时可以省略 |
总结一句话:Lambda表达式关注的是"做什么",而把"谁来做"和"怎么做声明"全部交给编译器去推断。
4. 从面向对象到面向行为的思维转变
这是理解Lambda最核心的一点。
匿名内部类的思维方式:
"我需要创建一个接口的实现对象,这个对象里要覆盖那个抽象方法,方法里写具体的操作。"
这是一种"先造人,再让人干活"的面向对象思维。
Lambda的思维方式:
"我需要一个操作,这个操作接收这些数据,返回那个结果。"
这是一种"直接描述动作"的函数式思维。
用生活中的例子来类比:
匿名内部类就像:你点餐时,餐厅给你一个厨师(对象),让他去做菜(方法)。
Lambda表达式就像:你直接说"我要一份番茄炒蛋"(行为本身),不需要关心是谁做的。
5. Lambda的三个简化层次
从练习中,我们可以总结出Lambda的三个简化层级:
第一层:完整写法(省略new和接口名)
(int left, int right) -> { return left + right; }第二层:省略参数类型(编译器推断)
(left, right) -> { return left + right; }第三层:省略花括号、return和分号(单条语句时)
(left, right) -> left + right对于单参数的情况,还可以省略小括号:
value -> value % 2 == 0 // 而不是 (value) -> value % 2 == 06. 使用Lambda的前提条件
我们最后总结一下,使用Lambda替换匿名内部类需要满足的条件:
| 条件 | 说明 |
|---|---|
| ① 接口类型 | 方法的形参必须是接口,不能是抽象类或普通类 |
| ② 函数式接口 | 该接口只能有一个抽象方法 |
| ③ 上下文明确 | 编译器能推断出目标类型(即接口类型) |
特别注意:如果接口有多个抽象方法,就无法用Lambda,因为编译器不知道你要实现的是哪一个。这时候只能继续用匿名内部类。
7. 延伸思考
学到这里,你可能会想:
如果我想自己定义一个这样的接口,该怎么写?
除了替换匿名内部类,Lambda还能用在什么地方?
::这个写法又是什么?
这些问题我们留到后续文章继续探讨。但无论如何,请记住今天的核心收获:
Lambda表达式的本质,是从"创建对象来做事"转向"直接描述要做的事"。它让代码更简洁,也让Java具备了函数式编程的能力,为后续学习Stream流式编程打下了基础。
