为什么需要内部类
有时候一个类只为另一个类服务,没必要单独建一个文件,直接塞进去更干净
就像你的钱包里有卡槽——卡槽只为钱包服务,没必要单独存在
Java 有四种内部类:静态内部类、成员内部类、局部内部类、匿名内部类
静态内部类 —— 最常用、最简单
用 static 修饰的内部类,和外部类关系最”疏远”——不依赖外部类的实例
可以理解为碰巧写在另一个类里面的普通类,只是逻辑上有归属关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class School { private String schoolName = "清华大学";
public static class Address { String city; String street;
public Address(String city, String street) { this.city = city; this.street = street; }
public void print() { System.out.println(city + " " + street); } } }
@Test public void testStaticInner() {
School.Address addr = new School.Address("北京", "清华路1号"); addr.print(); }
|
使用场景:
Builder 模式(Person.Builder)
Map 里的 Map.Entry
把逻辑相关的小类组织在一起
实际开发中最推荐用这种,没有乱七八糟的隐式引用
成员内部类 —— 绑定外部实例
没有 static 修饰的内部类,每个内部类对象都绑定一个外部类对象
就像每个”器官”都依附于一个”人”,不能独立存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class Person { private String name;
public Person(String name) { this.name = name; }
public class Heart { public void beat() { System.out.println(name + "的心脏在跳动"); } } }
@Test public void testMemberInner() { Person person = new Person("张三");
Person.Heart heart = person.new Heart(); heart.beat(); }
|
注意事项:
成员内部类会隐式持有外部类的引用(Person.this)
这可能导致内存泄漏——外部类对象本该被回收,但内部类还引用着它
所以如果不需要访问外部类的实例成员,优先用静态内部类
局部内部类 —— 几乎不用
定义在方法体内部的类,作用域仅限于这个方法
就像临时工,干完这个活就走,别的地方不认识它
1 2 3 4 5 6 7 8 9 10 11 12
| public void someMethod() {
class LocalHelper { void help() { System.out.println("我是临时工"); } }
LocalHelper helper = new LocalHelper(); helper.help(); }
|
实际开发几乎不用,知道有这个东西就行,通常用匿名类或 Lambda 代替
匿名内部类 —— 一次性的类
没有名字的类,写完就用,用完就扔,常用于回调和事件处理
就像叫外卖——你不需要知道骑手的名字,只需要他把东西送到就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Test public void testAnonymousClass() {
Comparator<String> comp = new Comparator<String>() { @Override public int compare(String a, String b) { return a.length() - b.length(); } };
List<String> names = Arrays.asList("Alice", "Bob", "Christina"); Collections.sort(names, comp); System.out.println(names); }
|
更常见的写法是直接内联,不用变量接收:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Test public void testAnonymousInline() { List<String> names = Arrays.asList("Alice", "Bob", "Christina");
Collections.sort(names, new Comparator<String>() { @Override public int compare(String a, String b) { return a.length() - b.length(); } });
System.out.println(names); }
|
使用场景:
实现只用一次的接口或抽象类
事件监听器(Swing/Android开发大量使用)
线程创建:new Thread(new Runnable() { ... })
匿名类 → Lambda 的进化(预告)
Java 8 引入了 Lambda 表达式,可以大幅简化匿名类的写法
条件:接口只有一个抽象方法(叫”函数式接口”)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Test public void testAnonymousVsLambda() { List<String> names = Arrays.asList("Alice", "Bob", "Christina");
Collections.sort(names, new Comparator<String>() { @Override public int compare(String a, String b) { return a.length() - b.length(); } });
Collections.sort(names, (a, b) -> a.length() - b.length());
Collections.sort(names, Comparator.comparingInt(String::length)); }
|
看到没?6行代码变成了1行。Lambda本质上就是匿名类的语法糖
后面学到 函数式编程 会详细讲 Lambda,现在只需要知道匿名类是它的”前身”
四种内部类对比表
| 特性 |
静态内部类 |
成员内部类 |
局部内部类 |
匿名内部类 |
| 关键字 |
static class |
class |
方法内 class |
new 接口/类() { } |
| 能否访问外部类实例成员 |
❌ |
✅ |
✅ |
✅ |
| 创建方式 |
new 外部类.内部类() |
外部对象.new 内部类() |
方法内直接 new |
定义时就创建 |
| 有没有名字 |
有 |
有 |
有 |
没有 |
| 使用频率 |
⭐⭐⭐ 高 |
⭐⭐ 中 |
⭐ 极低 |
⭐⭐⭐ 高(Java 8 前) |
| 推荐程度 |
首选 |
谨慎用 |
不推荐 |
Java 8 后用 Lambda 替代 |
访问修饰符方面:静态内部类和成员内部类可以用四种修饰符(参见 访问修饰符),局部和匿名类不能加修饰符
常见坑
坑1:成员内部类导致内存泄漏
成员内部类隐式持有外部类的引用 OuterClass.this
如果内部类对象的生命周期比外部类长,外部类就无法被垃圾回收
解决:能用静态内部类就用静态的
坑2:匿名类访问局部变量必须是 final 或 effectively final
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Test public void testFinalVariable() { int count = 10;
Runnable r = new Runnable() { @Override public void run() { System.out.println(count); } };
}
|
原因:匿名类拷贝了一份局部变量的值,如果允许修改就会导致数据不一致
坑3:匿名类不能定义构造器
因为没有类名,就没法写构造器。如果需要初始化逻辑,用 { } 实例初始化块
坑4:匿名类中的 this
匿名类中的 this 指向匿名类自己,不是外部类
要访问外部类用 外部类名.this
练习题
题1:下面的代码输出什么?
1 2 3 4 5 6 7 8 9 10 11 12
| public class Outer { String msg = "外部类";
class Inner { String msg = "内部类"; void print() { System.out.println(msg); System.out.println(this.msg); System.out.println(Outer.this.msg); } } }
|
答案:内部类、内部类、外部类。当内外部类有同名成员时,优先访问内部类自己的,要访问外部类的需要用 Outer.this.msg
题2:为什么推荐优先使用静态内部类?
答案:静态内部类不持有外部类的隐式引用,不会导致内存泄漏,也更容易理解和维护
题3:把下面的匿名类改写成 Lambda 表达式
1 2 3 4 5 6
| Runnable r = new Runnable() { @Override public void run() { System.out.println("Hello"); } };
|
答案:Runnable r = () -> System.out.println("Hello");