- 0
- 404 words
面向对象学了这么多,你有没有想过一个问题:一个类,能不能写在另一个类的里面?
我第一次听到这个说法的时候,第一反应就是——啊?类套类?这不是套娃吗?
但等你真搞懂了,会发现内部类(Inner Class)是 Java 里设计得非常精巧的一个语法。它不是炫技,而是解决了一些外部类内部逻辑高内聚的问题——说到底就是:有些类只服务于另一个类,没必要扔到外面跟人家平起平坐。
一、什么是内部类?为什么要用?
定义很简单:在某个类的内部再定义一个类,就叫内部类。
class Outer {
class Inner {
// 内部类
}
}
你可能会说,分开定义不就行了,何必嵌套?
场景来了:你写一个 LinkedList(双向链表)。链表由一个个节点组成,这个 Node 类只服务于 LinkedList,对外界来说 Node 就是一个实现细节,你完全没必要把它定义成一个公开的顶层类。
内部类的核心优势:
- 封装性:外部不需要知道内部类的存在,实现细节藏得严严实实
- 直接访问外部类成员:内部类天然持有外部类对象的引用(隐式的),可以自由访问外部类的 所有 成员,包括
private的 - 代码组织性:逻辑上属于一起的代码,物理上也放在一起
二、内部类的四种类型
Java 的内部类分四种,一个一个来:
- 成员内部类(Member Inner Class)—— 最基础的那种
- 静态内部类(Static Nested Class)—— 加了
static的 - 局部内部类(Local Inner Class)—— 写在方法里的
- 匿名内部类(Anonymous Inner Class)—— 连名字都没有的
这篇我们先讲前两种——成员内部类和静态内部类。局部内部类和匿名内部类下篇再说。
三、成员内部类(Member Inner Class)
成员内部类就是最简单粗暴的:把一个类不加 static、不写方法里,就放在外部类的成员位置。
3.1 基本写法
public class Outer {
private String name = "我是外部类";
// 成员内部类
public class Inner {
public void show() {
// 内部类直接访问外部类的 private 成员
System.out.println(name);
}
}
}
调用的时候要注意,成员内部类依赖外部类对象而存在:
public class Test {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner(); // 注意这个语法
inner.show(); // 输出:我是外部类
}
}
outer.new Inner() 这个写法第一次见肯定觉得奇怪——记住就行,它的意思是「通过一个外部类对象来创建内部类对象」。
3.2 内部类持有外部类引用 —— Outer.this
成员内部类背后有个重要机制:每个成员内部类对象内部,都隐式持有一个外部类对象引用,通过 Outer.this 访问。
比如外部类和内部类有同名成员变量:
public class Outer {
private String msg = "外部消息";
public class Inner {
private String msg = "内部消息";
public void show() {
String msg = "局部消息";
System.out.println(msg); // 局部消息
System.out.println(this.msg); // 内部消息
System.out.println(Outer.this.msg); // 外部消息
}
}
}
Outer.this 就是那个隐式的外部类引用。三层同名变量,层层都能区分,Java 的命名空间拆得很细。
3.3 权限修饰符
成员内部类可以加 private、protected、默认、public——它跟成员变量一样,访问控制全看你需要。
最常见的用法就是 private:
public class LinkedList {
// 内部类私有化,外部完全不知道 Node 的存在
private class Node {
int data;
Node prev;
Node next;
Node(int data) {
this.data = data;
}
}
private Node head;
public void addFirst(int data) {
Node newNode = new Node(data);
newNode.next = head;
if (head != null) {
head.prev = newNode;
}
head = newNode;
}
}
这就是内部类最经典的应用场景:实现细节对外界完全隐藏。调用者只需要 list.addFirst(10),根本不知道里面还有个 Node。
四、静态内部类(Static Nested Class)
成员内部类加个 static 就成了静态内部类——但它俩的差别不小。
4.1 静态内部类的特点
- 不持有外部类引用:它不依赖外部类对象,跟
static的语义一致 - 只能访问外部类的静态成员:不能直接访问实例成员
- 创建不需要外部类对象:
new Outer.StaticInner()搞定
public class Outer {
private static String staticMsg = "静态消息";
private String instanceMsg = "实例消息";
// 静态内部类
public static class StaticInner {
public void print() {
System.out.println(staticMsg); // ✅ 可以访问外部类静态成员
// System.out.println(instanceMsg); // ❌ 不能访问实例成员
}
}
}
// 使用方式
public class Test {
public static void main(String[] args) {
Outer.StaticInner inner = new Outer.StaticInner();
inner.print(); // 不需要外部类实例
}
}
4.2 经典的建造者模式
静态内部类最出名的应用就是 Builder 模式。当你有一个类需要很多可选参数时,用构造器重载能把你搞疯:
// 传统方式——参数一多构造器就爆炸
User user1 = new User("张三", 20, "北京", "男", "138xxxx", "zhangsan@qq.com");
// 哪个参数对应哪个?每次都得数位置,烦不烦
用静态内部类 + Builder 模式:
public class User {
private final String name; // 必填
private final int age; // 必填
private final String city; // 可选
private final String gender; // 可选
private final String phone; // 可选
private final String email; // 可选
// 私有构造器——只能通过 Builder 创建
private User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.city = builder.city;
this.gender = builder.gender;
this.phone = builder.phone;
this.email = builder.email;
}
// 静态内部类作为 Builder
public static class Builder {
private final String name; // 必填,设为 final
private final int age; // 必填
private String city;
private String gender;
private String phone;
private String email;
public Builder(String name, int age) {
this.name = name;
this.age = age;
}
public Builder city(String city) { this.city = city; return this; }
public Builder gender(String gender) { this.gender = gender; return this; }
public Builder phone(String phone) { this.phone = phone; return this; }
public Builder email(String email) { this.email = email; return this; }
public User build() {
return new User(this);
}
}
}
调用起来就清爽了:
User user = new User.Builder("张三", 20)
.city("北京")
.gender("男")
.email("zhangsan@qq.com")
.build();
// 链式调用,每一行都看得懂参数是什么意思
这是静态内部类最优雅的用法。每当你写一个构造器参数超过 3 个的类,就该考虑 Builder 模式了。
五、成员内部类 vs 静态内部类
| 对比维度 | 成员内部类 | 静态内部类 |
|---|---|---|
| 持有外部类引用 | ✅ 持有(隐式 Outer.this) | ❌ 不持有 |
| 访问外部实例成员 | ✅ 可以 | ❌ 不可以 |
| 访问外部静态成员 | ✅ 可以 | ✅ 可以 |
| 创建方式 | outer.new Inner() | new Outer.Inner() |
| 内存占用 | 额外持有外部类引用,稍大 | 没有额外引用,更轻量 |
| 典型场景 | 需要双向访问(如链表 Node) | 辅助类(如 Builder) |
六、编译后的秘密
内部类编译后会生成独立的 .class 文件。你编译 Outer.java 之后,会看到:
Outer.class
Outer$Inner.class // 成员内部类
Outer$StaticInner.class // 静态内部类
那个 $ 就是 Java 区分内部类的标志。如果你在命令行里看到 Outer$Inner.class 这种文件,别觉得是系统抽风——那是内部类编译后的产物。
这个规则我记得有一次写代码,反编译看字节码的时候被这个 $ 搞懵过,找了半天才发现是内部类……话说回来,搞懂这个对面试很有用,面试官经常问「内部类编译后生成几个文件」。
七、小结
- 内部类就是类中定义的类,实现了高内聚的封装
- 成员内部类持有隐式外部类引用,可以访问外部类全部成员
- 静态内部类不持有外部类引用,只能访问外部类的静态成员
- 内部类编译后生成
外部类$内部类.class文件 - Builder 模式是静态内部类的经典应用,参数多的类用它准没错
下一篇我们聊聊局部内部类和匿名内部类——特别是匿名内部类,你在写 GUI 事件、Lambda 表达式转译、线程创建时处处都会见到它,非常实用。