S02-09 面向对象-内部类
[TOC]
概述
什么是内部类
在 Java 中,内部类(Inner Class) 是指一个直接定义在另一个类或接口内部的类。
内部类不仅是一种逻辑上的分组机制,更重要的是,它允许内部类直接访问其外部类(Outer Class)的成员(包括私有成员)。这种特性使得内部类在处理复杂的业务逻辑、事件驱动编程以及隐藏实现细节时非常有用。
为什么要使用内部类:
封装性(隐藏实现):可以将内部类隐藏在外部类中,不允许同一个包中的其他类访问。
访问特权:内部类可以无条件地访问外部类的所有成员,即使是
private成员。逻辑分组:如果一个类只为另一个类服务,将它作为内部类定义可以使代码结构更紧凑、可读性更强。
多重继承的补充:Java 的类不支持多继承,但通过让不同的内部类继承不同的父类,可以变相实现多重继承的效果。
内部类分类
内部类的四大分类:
Java 中的内部类主要分为四种:成员内部类、静态内部类、局部内部类和匿名内部类。
按定义位置分:
- 外部类的成员位置:
- 有 static:静态内部类
- 没有 static:成员内部类
- 方法内或代码块内:
- 有类名:局部内部类
- 有没类名:匿名内部类
对比四大内部类:
| 比较维度 | 成员内部类 (Member Inner Class) | 静态内部类 (Static Nested Class) | 局部内部类 (Local Inner Class) | 匿名内部类 (Anonymous Inner Class) |
|---|---|---|---|---|
| 定义位置 | 外部类的成员位置 | 外部类的成员位置 | 方法内或代码块内 | 方法内或代码块内(声明与实例化合一) |
static 修饰符 | 无(Java 16 起内部可定义静态成员) | 有(static 修饰类本身) | 无 | 无 |
| 是否有类名 | 有 | 有 | 有 | 无(直接给出父类或接口名) |
| 实例化方式 | 依赖外部类对象:outer.new Inner() | 不依赖外部类对象:new Outer.Inner() | 只能在所在的方法内部直接 new | 在定义的同时 new(new 接口/父类() {...}) |
| 访问外部类成员 | 可以无限制访问(包括非静态、静态、私有) | 只能直接访问静态成员 | 可以无限制访问(前提是所在方法非静态) | 可以无限制访问(前提是所在方法非静态) |
| 访问所在方法局部变量 | 不适用 | 不适用 | 必须是 final 或事实上的不可变 | 必须是 final 或事实上的不可变 |
| 是否持有外部类引用 | 是(隐式持有 Outer.this) | 否 | 是(若在非静态方法中) | 是(若在非静态方法中) |
| 编译后的 class 文件名 | Outer$Inner.class | Outer$Inner.class | Outer$1Inner.class (带数字序号) | Outer$1.class (仅数字序号) |
| 典型应用场景 | 隐藏细节且需直接操作外部类状态(如 ArrayList 的 Itr 迭代器) | 高内聚辅助类、Builder 建造者模式、防内存泄漏 | 仅在方法内需要用到复杂的局部类(极少使用) | 接口/抽象类的单次实现、事件监听器、回调函数、多线程 |
核心记忆口诀:
- 成员内部类:寄生虫,没外部类活不了,外部类的东西随便用。
- 静态内部类:独立单身汉,自己管自己,只能用外部类的共享(静态)资源。
- 局部内部类:方法里的私房钱,出了门谁都不认,用外面的变量还得锁死(final)。
- 匿名内部类:一次性筷子,没名字,用完就扔,现在经常被 Lambda 抢饭碗。
编译后的底层产物
编译后的底层产物:
需要注意的是,无论内部类嵌套得多深,Java 编译器最终都会把它们编译成独立的 .class 文件:
- 成员内部类、静态内部类:会被编译为
外部类名$内部类名.class(例如Outer$Inner.class)。 - 局部内部类:会被编译为
外部类名$数字内部类名.class(例如Outer$1LocalInner.class)。 - 匿名内部类:会被编译为
外部类名$数字.class(例如Outer$1.class)。
成员内部类
成员内部类(Member Inner Class) 是 Java 内部类中最基础、最核心的一种形态。它就像是外部类的一个普通成员(类似于一个实例变量或者实例方法),没有 static 修饰符。
为了让你全面彻底地掌握成员内部类,我们将从它的核心特性、对象创建、变量访问规则以及底层原理来逐一拆解。
核心特性
核心特性:
- 实例级别:成员内部类是寄生在外部类的实例(对象) 上的。这意味着,没有外部类的对象,就不可能存在成员内部类的对象。
- 无限制访问:它可以无条件地访问外部类的所有成员属性和成员方法,包括被声明为
private的成员。 - 访问修饰符:和普通的类成员一样,成员内部类可以使用所有的访问修饰符进行限制,比如
public、protected、默认(包可见)以及private。你可以通过private将内部类完全隐藏在外部类内部。
对象创建方式
对象的创建方式:
由于成员内部类依赖于外部类的实例,因此在不同的位置实例化它,语法会有所不同。
方式 A:在外部类内部创建:
在外部类的非静态方法中,由于当前已经存在外部类的实例(也就是 this),你可以直接像创建普通对象一样创建内部类对象。
public class Outer {
public class Inner {
public void work() {
System.out.println("Inner is working.");
}
}
public void createInner() {
// 直接实例化
Inner inner = new Inner();
inner.work();
}
}方式 B:在外部类外部(或其他类的静态方法中)创建:
这是初学者最容易写错的地方。因为必须先有外部类对象,所以必须通过外部类的对象去 new 内部类。
public class Test {
public static void main(String[] args) {
// 第一步:先实例化外部类
Outer outer = new Outer();
// 第二步:通过外部类实例,去 new 内部类
// 注意语法格式:外部类名.内部类名 变量名 = 外部类对象.new 内部类();
Outer.Inner inner = outer.new Inner();
inner.work();
}
}变量同名遮蔽
变量的“同名遮蔽”与访问规则(重点):
当局部变量、内部类成员变量、外部类成员变量发生同名冲突时,Java 采用就近原则。如果想打破就近原则访问更外层的变量,需要使用特定的 外部类名.this 语法。
public class Outer {
private String name = "外部类的属性";
public class Inner {
private String name = "内部类的属性";
public void showName(String name) {
// 1. 就近原则,访问方法参数(局部变量)
System.out.println(name);
// 2. 访问当前内部类的成员变量
System.out.println(this.name);
// 3. 访问外部类的成员变量(核心语法:外部类名.this.变量名)
System.out.println(Outer.this.name);
}
}
}说明:Outer.this 是 Java 提供的一个特殊语法,专门用来获取指向当前所依附的外部类实例的引用。
底层原理@
底层原理:为什么内部类能访问外部类的私有成员:
从 JVM 的角度来看,并不存在所谓的“内部类”,所有的类最终都会被编译成独立的 .class 文件。成员内部类会被编译为 Outer$Inner.class。
魔法发生在这里:
当 Java 编译器编译成员内部类时,会在其底层偷偷地执行以下操作:
自动为内部类添加一个隐式的成员变量,这个变量的类型就是外部类。
自动修改内部类的构造器,将外部类的实例作为参数传进去,并赋值给那个隐式变量。
也就是说,成员内部类在创建时,就悄悄绑定了创建它的那个外部类对象的引用。当我们调用外部类的私有属性时,实际上编译器帮我们转化为了类似 OuterClassReference.property 的调用。
JDK 16+ 新特性
JDK 16 及以后的规则变化(关于 Static):
在 Java 16 之前,有一个非常严格的规定:成员内部类中不能包含任何 static 声明的变量或方法(除非是 static final 的编译期常量)。因为成员内部类是依赖对象的,而 static 是类级别的,两者在逻辑上冲突。
更新:从 Java 16 开始(JEP 395 引入的特性),这个限制被打破了。现在的成员内部类中也可以定义静态成员和静态方法了。不过,这些静态成员仅仅属于这个内部类本身,通常用于内部类自身的辅助逻辑。
应用场景
经典源码中的应用场景:
成员内部类在 JDK 源码中最经典的应用莫过于迭代器模式(Iterator Pattern)。
例如在 ArrayList 源码中,迭代器 Itr 就是一个成员内部类:
public class ArrayList<E> {
// 外部类的实际数据
transient Object[] elementData;
private int size;
// ... 其他代码 ...
// 内部类实现了 Iterator 接口
private class Itr implements Iterator<E> {
int cursor; // 下一个要返回的元素的索引
public boolean hasNext() {
// 直接访问外部类的 size 属性!
return cursor != size;
}
public E next() {
// 直接操作外部类的 elementData 数组!
return (E) elementData[cursor++];
}
}
public Iterator<E> iterator() {
return new Itr();
}
}好处:Itr 作为一个内部类,它可以天经地义地、零距离地直接读取 ArrayList 内部的数组 elementData 和 size,不需要 ArrayList 暴露额外的 get/set 方法,完美保证了 ArrayList 的封装性。
练习题【

静态内部类
在 Java 中,静态内部类(Static Nested Class,静态嵌套类) 虽然名字里带有“内部类”,但严格来说,Java 官方文档将其定义为静态嵌套类。
你可以把它理解为一个“恰好写在另一个类里面的普通类”。加上了 static 关键字后,它与外部类的关系变得非常松散,完全摆脱了对外部类实例的依赖。
核心特性
核心特性:切断与外部类实例的羁绊:
- 无需外部类实例:静态内部类不依赖于外部类的对象。它的创建不依赖于外部类的生命周期。
- 没有隐式引用:在上一篇讲成员内部类时,我们提到它底层会偷偷绑定一个
Outer.this引用。静态内部类没有这个引用。这是它与普通成员内部类最本质的区别。 - 可包含任何成员:静态内部类内部既可以定义静态变量和静态方法,也可以定义普通的实例变量和实例方法。
对象的创建方式
对象的创建方式:
因为不需要依赖外部类的实例,创建静态内部类对象的语法非常直接,类似于调用外部类的静态属性。
public class Outer {
// 1. 静态内部类
public static class StaticInner {
public void sayHello() {
System.out.println("Hello from Static Inner!");
}
}
}
public class Test {
public static void main(String[] args) {
// 2. 直接通过 外部类名.内部类名 进行实例化,不需要 new Outer()
Outer.StaticInner inner = new Outer.StaticInner();
inner.sayHello();
}
}访问限制
访问规则(作用域限制):类似静态方法
因为静态内部类在内存中独立存在(不依赖外部类对象),所以它无法直接知道外部类实例的状态。
只能访问外部类的静态成员:它可以无缝访问外部类的所有静态变量和静态方法(即使是
private的)。绝对不能直接访问外部类的非静态成员:如果你想访问外部类的普通成员变量,必须自己手动创建一个外部类的对象,然后通过该对象去访问。
javapublic class Outer { private static String staticDesc = "外部类的静态私有变量"; private String instanceDesc = "外部类的普通实例变量"; public static class StaticInner { public void testAccess() { // 1. 访问静态成员 System.out.println(staticDesc); // ✅ 正确:直接访问外部类的静态成员 // 2. 访问非静态成员 // System.out.println(instanceDesc); // ❌ 报错:无法直接从静态上下文中引用非静态变量 Outer outer = new Outer(); // ✅ 必须通过实例化外部类才能访问非静态成员 System.out.println(outer.instanceDesc); } } }
应用场景
为什么要用静态内部类(黄金应用场景):
既然它和普通的类这么像,为什么不直接在外面单独写一个类呢?
场景 A:高内聚的辅助类(Namespace 分组):
如果一个类
B只为类A服务,并且不依赖A的对象状态,将B作为A的静态内部类,可以隐藏B的存在,让包结构更干净,代码具备极强的高内聚性。例如:Java 源码中
HashMap的Node<K,V>节点类,就是一个静态内部类。因为节点本身只是一种数据结构,不需要知道整个 HashMap 对象的具体状态。场景 B:大名鼎鼎的 Builder(建造者)模式:
这是静态内部类最经典的实战场景。 建造者模式通过一个静态内部类来链式构造一个复杂的外部类对象。
javapublic class User { private final String name; // 必填 private final int age; // 可选 // 1. 私有化构造器,强制通过 Builder 创建对象 private User(Builder builder) { this.name = builder.name; this.age = builder.age; } // 静态内部类 Builder public static class Builder { private String name; private int age = 0; // 默认值 // 2. 必填属性通过构造器传入 public Builder(String name) { this.name = name; } // 3. 可选属性通过链式调用 public Builder age(int age) { this.age = age; return this; // 返回当前 Builder 对象 } // 4. 最终构建外部类对象 public User build() { return new User(this); } } } // 使用场景 User user = new User.Builder("Alice").age(25).build();场景 C:避免内存泄漏(特别是在 Android 和长生命周期应用中):
普通成员内部类会隐式持有外部类的引用(
Outer.this)。如果内部类的生命周期比外部类长(例如作为一个后台线程、定时任务或事件监听器),就会导致外部类哪怕没用了,也无法被垃圾回收器 (GC)回收,从而引发内存泄漏。改为静态内部类 + 弱引用(WeakReference) 是解决这类内存泄漏的标准答案。
静态内部类 vs 成员内部类
总结对比:静态内部类 vs 成员内部类:
| 特性 | 成员内部类 (Member Inner Class) | 静态内部类 (Static Nested Class) |
|---|---|---|
static 修饰符 | 无 | 有 |
| 依赖外部类实例 | 是(必须先 new Outer()) | 否(直接 new Outer.Inner()) |
| 持有外部类引用 | 是(隐式持有 Outer.this) | 否 |
| 访问外部类静态成员 | 可以直接访问 | 可以直接访问 |
| 访问外部类非静态成员 | 可以直接访问 | 不可直接访问 |
| 内存泄漏风险 | 较高(容易因隐式引用导致泄漏) | 极低(互相独立) |
局部内部类
在 Java 中,局部内部类(Local Inner Class) 就像是方法里的“私有财产”。它是定义在外部类的一个方法或者某个代码块(比如 if 语句块、for 循环、构造代码块等)内部的类。
如果说成员内部类是类的“实例变量”,那么局部内部类就是类的“局部变量”。它的存在感在日常开发中相对较低,但理解它对于彻底掌握 Java 的作用域和闭包(Closure)概念至关重要。
核心特性
访问权限
核心特性:访问权限(类似局部变量)
访问外部类成员:可以无限制、直接访问(前提是所在方法非静态)。
外部类访问局部内部类的方法:可以在内部类所在方法中创建内部类实例,通过该实例调用内部类的方法。
外部其他类无法访问局部内部类:局部内部类的地位是一个局部变量。
作用域受限
核心特性:极度受限的作用域(类似局部变量):
作用域受限:
局部内部类的作用域仅限于定义它的那个方法或代码块内部。出了这个大括号
{},外界(哪怕是同一个外部类中的其他方法)对它一无所知,更无法使用它。没有访问修饰符(除了
final):- 就像你不能在一个方法里给局部变量加上
public或private一样,局部内部类也不能使用public、protected、private或static来修饰。 - 可以用
final修饰,表示不能在当前方法中被继承,局部变量也可以用final修饰。
- 就像你不能在一个方法里给局部变量加上
实例化位置受限:
你必须在定义了局部内部类之后,且在同一个方法/代码块内部,才能通过
new关键字来创建它的对象。
基础示例
来看一个标准的使用场景:在方法内部定义并使用局部内部类。
public class Outer {
private String outerField = "外部类成员变量";
public void myMethod() {
// 方法内的局部变量
String localStr = "方法局部变量";
// 1. 定义局部内部类
class LocalInner {
public void printInfo() {
// a. 可以直接访问外部类的成员变量和方法
System.out.println(outerField);
// b. 也可以访问当前方法的局部变量和方法(有特殊限制,后文详述)
System.out.println(localStr);
}
}
// 2. 只能在方法内部实例化
LocalInner inner = new LocalInner();
inner.printInfo();
}
public void anotherMethod() {
// 3. ❌ 报错:在这里完全不知道 LocalInner 的存在
// LocalInner inner = new LocalInner();
}
}灵魂考点:Effectively Final 规则
灵魂考点:Effectively Final 规则(重点!):
局部内部类最大的坑,也是面试最爱问的地方:局部内部类访问所在方法的局部变量时,该变量必须是 final 的。
从 Java 8 开始,引入了 Effectively Final(事实上的 final) 概念:你可以不显式地写 final 关键字,但这个变量在初始化之后,绝对不能再被修改。如果尝试修改它,编译器会直接报错。
public class Outer {
public void test() {
int count = 10; // 事实上的 final (effectively final)
class LocalInner {
public void display() {
System.out.println(count);
}
}
// ❌ 如果在这里修改 count,上面的 display() 方法里的 count 就会报错!
// count = 20;
new LocalInner().display();
}
}为什么会有这个奇葩的限制
为什么会有这个奇葩的限制(底层原理):
这本质上是生命周期不一致引发的问题:
局部变量的生命周期:存放在 JVM 的**栈(Stack)**内存中。当
test()方法执行完毕后,栈帧出栈,局部变量count就灰飞烟灭了。局部内部类对象的生命周期:通过
new创建出来的对象存放在 JVM 的**堆(Heap)**内存中。即使test()方法执行结束,只要这个内部类对象还在被引用,它就不会被垃圾回收器(GC)回收。
矛盾点:如果方法结束了,变量 count 销毁了,但局部内部类的对象还活着,并且还要调用 display() 去访问 count,去哪找呢?
Java 的解决手段(变量拷贝):
Java 编译器在编译局部内部类时,会偷偷把局部内部类要访问的局部变量,复制一份作为内部类的私有成员变量。
为了保证“方法里的原版变量”和“内部类里的复刻版变量”数据永远保持一致,Java 强行规定:这个变量不准改!(即必须是 final)。
编译后的底层产物
编译后的底层产物:
因为不同的方法里面可能会起同名的局部内部类,为了防止类名冲突,编译器在生成 .class 文件时,会加上数字序号来区分。
编译上述代码后,会生成类似这样的文件:
Outer.classOuter$1LocalInner.class(带有数字前缀的类名)
局部内部类存在的意义是什么
局部内部类存在的意义是什么:
老实说,在现代 Java 开发中,你几乎不会直接手写局部内部类。
它的存在主要是为了解决极其特殊的场景:
你需要在方法内部实现某个接口(或者继承某个类)。
并且,你在该方法内部需要多次实例化这个实现类(如果只实例化一次,我们通常会使用它的“究极进化版”——匿名内部类)。
匿名内部类@
概述
匿名内部类是 Java 内部类家族中出场率最高、在实际开发中最常用的一位。
如果说局部内部类是为了在方法内部重用代码,那么匿名内部类就是为了“用完即走”。当你只需要实例化某个类(或接口的实现类)一次时,专门为它写一个完整的类显得极其臃肿,这时候匿名内部类就派上用场了。
匿名内部类(Anonymous Inner Class) 本质上是一个没有名字的局部内部类。
既然没有名字,它就无法在后面被再次调用,因此它的定义和实例化必须在同一时间完成。
基本语法
基础语法结构:
new 父类构造器(参数) 或 接口() {
// 匿名内部类的类体部分
// 可以重写方法,也可以定义自己的变量和方法(但外界无法调用自定义的方法)
};注意:末尾必须要有一个分号 ;,因为它本质上是一个表达式(也就是一行代码)。
应用场景
经典应用场景与代码演示:
匿名内部类最常见的场景是:创建线程(Runnable)、事件监听器(Listener)、以及集合排序(Comparator)。
场景 A:实现接口(以多线程为例):
假设你想启动一个线程,传统做法是写一个类实现
Runnable接口,然后再把这个类的对象传给Thread。使用匿名内部类,你可以一步到位:javapublic class Test { public static void main(String[] args) { // 写法一: // 1. 直接 new 接口,并在花括号内实现抽象方法 Runnable myRunnable = new Runnable() { @Override public void run() { System.out.println("匿名内部类实现的线程正在运行!"); } }; // 2. 启动线程 new Thread(myRunnable).start(); }javapublic class Test { public static void main(String[] args) { // 写法二: // 甚至可以写得更极致(直接当做参数传递): new Thread(new Runnable() { @Override public void run() { System.out.println("极致缩写的匿名内部类!"); } }).start(); } }场景 B:继承父类并重写方法:
除了实现接口,匿名内部类也可以用来继承一个普通的类或抽象类,并重写它的方法。
javaclass Animal { public void speak() { System.out.println("动物发出叫声"); } } public class Test { public static void main(String[] args) { // 1. 创建一个继承自 Animal 的匿名内部类对象,并重写 speak 方法 Animal dog = new Animal() { @Override public void speak() { System.out.println("汪汪汪!"); } }; // 2. 调用实例对象的方法 dog.speak(); // 输出:汪汪汪! } }
匿名内部类的三大限制
匿名内部类的“三大限制”:
虽然它很好用,但因为它没有名字,所以在语法上有几条铁律:
二选一:匿名内部类必须且只能继承一个父类,或者实现一个接口。不能同时继承类又实现接口,也不能实现多个接口。
没有构造器:因为构造器的名字必须和类名一样,而匿名内部类没有名字,所以它不可能有显式的构造方法。如果需要初始化,通常利用构造代码块
{ ... }来初始化。闭包限制(Effectively Final):因为它本质上还是一个“局部内部类”,所以它如果想访问所在方法的局部变量,该局部变量依然必须是
final或者事实上不可变(effectively final) 的(底层原因我们在上一篇提到过,是为了防止局部变量被销毁后内外数据不一致)。
底层编译产物
底层编译产物:
因为没有名字,Java 编译器在把匿名内部类编译成 .class 文件时,会使用数字编号来给它命名,该类名可以通过 实例.getClass() 返回。
如果在 Outer 类中定义了两个匿名内部类,编译后会生成:
Outer.classOuter$1.class(第一个匿名内部类)Outer$2.class(第二个匿名内部类)
被 Lambda 替代
时代变迁:被 Lambda 表达式“降维打击”:
在 Java 8 之前,匿名内部类是处理回调和单次实现的王者。但它依然有一个缺点:太啰嗦了,模板代码太多。
从 Java 8 开始,引入了 Lambda 表达式。对于只有一个抽象方法的接口(即函数式接口,Functional Interface),Lambda 表达式可以直接替代匿名内部类,让代码变得极其优雅。
对比演示:
// 时代眼泪:匿名内部类写法
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello, 匿名内部类!");
}
});
// 现代写法:Lambda 表达式
Thread t2 = new Thread(() -> System.out.println("Hello, Lambda!"));关键注意点:
Lambda 表达式只能替代基于“函数式接口”的匿名内部类。如果你要继承一个抽象类,或者实现一个包含多个方法的接口,你依然只能老老实实地使用匿名内部类。
练习题【


代码实现:
