一文搞懂 Lambda 表达式与匿名内部类:从能用到真正理解
一文搞懂 Lambda 表达式与匿名内部类:从能用到真正理解
一、先给结论:Lambda 是什么?它解决了什么问题?
Lambda 表达式用于简化匿名内部类对函数式接口的实现方式。
在 Java 中,只要一个接口中有且仅有一个抽象方法,它就可以作为 Lambda 的目标类型,这类接口被称为:
函数式接口(Functional Interface)
@FunctionalInterface
public interface Runnable {
void run();
}
@FunctionalInterface 注解不是强制的,但非常推荐使用,因为它可以:
-
在编译期校验接口是否真的只有一个抽象方法
-
明确表达:这个接口就是为 Lambda 准备的
一句话先记住:
👉 Lambda 的目的不是炫技,而是让“行为”的表达更简单。
二、如果没有 Lambda,我们以前是怎么写代码的?
在 Java 8 之前,Java 是不能直接传递一段“要做的事情”的。
如果你想把一段逻辑交给别人执行,只能:
-
把逻辑写进一个类
-
再把这个类的对象传递出去
也就是说:
逻辑必须依附在“对象”上存在
而承担这个角色最多的,就是——匿名内部类。
三、匿名内部类是什么?(Lambda 出现之前的答案)
匿名内部类:在创建对象的同时,定义类的实现,但这个类没有名字。
它的核心特点只有一句话:
这是一个“真实存在的类”,只是没有名字。
3.1 实现接口的匿名内部类(最常见)
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
这一行代码,其实同时做了三件事:
-
定义了一个没有名字的类
-
这个类实现了 Runnable 接口
-
并立即创建了这个类的对象
因为这个类没有名字,所以称为 匿名内部类。
3.2 继承类的匿名内部类
Thread t = new Thread() {
@Override
public void run() {
System.out.println("Hello");
}
};
这里并没有显式写:
class Xxx extends Thread { }
而是:
-
临时继承了
Thread -
重写了它的方法
-
只用一次,用完即弃
3.3 匿名内部类的特点总结
| 特点 | 说明 |
|---|---|
| 没有类名 | 无法复用 |
| 定义即创建 | 只能创建一个实例 |
| 本质是类 | 有自己的 this |
| 常用于一次性逻辑 | 回调、监听器 |
四、匿名内部类的能力边界
4.1 接口中有多个抽象方法,也可以用匿名内部类吗?
答案是:可以,而且完全合法。
这是匿名内部类无法被 Lambda 替代的重要原因之一。
4.2 示例:包含多个抽象方法的接口实现
interface Animal {
void eat();
void sleep();
}
使用匿名内部类实现:
Animal animal = new Animal() {
@Override
public void eat() {
System.out.println("动物在吃东西");
}
@Override
public void sleep() {
System.out.println("动物在睡觉");
}
};
原因其实很简单:
匿名内部类,本质上是一个“完整的类”。
只要是类,就必须实现接口中定义的所有抽象方法。
五、Lambda 是如何简化匿名内部类的?
对比最直观。
匿名内部类:
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello Lambda");
}
};
Lambda:
Runnable r2 = () -> System.out.println("Hello Lambda");
变化只有一个:
👉 去掉了所有“与行为无关的结构代码”。
六、Lambda 表达式代码示例(从简单到常见)
6.1 Runnable 示例
匿名内部类写法:
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello Lambda");
}
};
Lambda 写法:
Runnable r2 = () -> System.out.println("Hello Lambda");
可以看到:
- 接口名、方法名全部消失
- 只剩下“要执行的逻辑”
这是 Lambda 最直观的价值体现。
6.2 Comparator 示例
假设我们要按照字符串长度排序。
匿名内部类写法:
Comparator<String> comparator1 = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
Lambda 写法:
Comparator<String> comparator2 = (a, b) -> a.length() - b.length();
这里:(a, b) 对应方法参数-> 右边是比较逻辑
编译器会根据目标接口,自动推断参数类型。
6.3 Lambda + Stream API(最常见)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.forEach(n -> System.out.println(n));
这段代码几乎就是自然语言:
过滤 → 转换 → 消费
七、为什么 Lambda 只能用于函数式接口?
因为:
一个 Lambda 表达式,只能承载“一段行为”。
如果接口中有多个抽象方法:
-
编译器无法判断你这段 Lambda 是给哪个方法的
-
语义也会变得不清晰
所以 Java 直接规定:
👉 Lambda 只能用于“只有一个抽象方法”的接口。
八、Lambda 与匿名内部类的本质区别
8.1 匿名内部类this 指向的不同(重点)
public class Test {
void test() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this);
}
};
Runnable r2 = () -> {
System.out.println(this);
};
}
}
这里的关键点在于:
new Runnable() { ... }
这行代码创建了一个新的类,并立即创建了这个类的一个对象。因此:
- 匿名内部类是一个“真实存在的类”
- 它有自己的对象
- 它自然也有自己的
this
所以在匿名内部类中:
this指向的就是匿名内部类对象本身(也就是变量r所引用的那个对象)。
8.2 Lambda 表达式中的 this
再看使用 Lambda 的写法:
Runnable r = () -> {
System.out.println(this);
};
这一次,你并没有写 new Xxx(),也没有创建新的类。
这是理解 Lambda 的关键:
Lambda 表达式不会生成新的类或新的对象。
它只是外部类中一段普通的方法逻辑,被“传递”给了函数式接口。因此,Lambda 中:
不存在属于 Lambda 自己的
this。
既然 Lambda 没有自己的 this,那么:
Lambda 中的
this,只能指向它所在的外部类对象。
例如:
public class Demo {
public void test() {
Runnable r = () -> {
System.out.println(this);
};
r.run();
}
}
这里的 this,指向的就是 Demo 的实例对象。
8.3 变量捕获(effectively final)
int x = 10;
Runnable r = () -> {
System.out.println(x);
};
// x = 20; // 编译错误
Lambda 只能使用:
-
final变量 -
或 事实不可变(effectively final)变量
这是为了保证行为一致性和并发安全。
九、什么时候该用 Lambda?什么时候该用匿名内部类?
| 场景 | 建议 |
|---|---|
| 接口只有一个抽象方法 | Lambda |
| 接口有多个抽象方法 | 匿名内部类 |
| 需要字段或状态 | 匿名内部类 |
| Stream / 集合操作 | Lambda |
| 依赖自身 this | 匿名内部类 |
十、全文总结
Lambda:写得少,看得快,表达“要做什么”。
匿名内部类:结构完整,能力更强,是 Lambda 的基础。
最后记住一句就够了:
👉 能用 Lambda 的地方优先用 Lambda;用不了的时候,匿名内部类一定能兜底。
更多推荐



所有评论(0)