Java内部类
1 概述
1.1
Java内部类,是JVM为实现“闭包式对象封装”而设计的完整对象模型。每一个非静态内部类实例,都隐式持有一个外部类实例的引用,这个引用在编译期被注入为合成字段,并在构造时绑定。
1.2
Java内部类共分四类:成员内部类、静态内部类、局部内部类、匿名内部类。每一种,都对应不同的JVM行为。
非静态内部类不能独立存在 —— 它必须依附于外部类实例;
静态内部类是独立的嵌套类 —— 它不持有外部引用,行为类似顶级类;
匿名类是局部类的无名版本 —— 它的类名由JVM动态生成,但本质仍是内部类;
所有内部类都会生成独立的 .class 文件 —— JVM加载时,它们是完全独立的类,只是命名被编译器特殊处理。
2 局部内部类
内部类必须通过外部类实例创建
外部类不能访问内部类的属性和方法
内部类不能访问外部类的属性和方法
2.1 示例代码
public class InnerDemo1 {
public static void main(String[] args) {
//必须通过外部类实例创建
Car.Engine carEngine= new Car().new Engine();
carEngine.show2();
}
}
class Car{
/*
final Car this$0;
Car$Engine(Car car) {
this.this$0 = car; // 构造时绑定外部实例
super();
}
*/
String carName;
String carColor;
String quality;
private String carDoor;
public void carRun(){
//编译错误
String a=engineType;
}
//通过外部类实例创建
public Engine createInner(){
return new Engine();
}
//成员内部类如同类的成员
class Engine{
String engineType;
String quality;
public void accessOuter() {
//显式获取外部类引用
//Car.this即this$0
Car thisOuter = Car.this;
}
public void show1(){
String b=carName+engineType;
carRun();
}
public void show2(){
String c=Car.this.carName+this.engineType;
Car.this.carRun();
}
public void showQuality(){
String d1 = quality;//等同于String d1 = this.quality;
String d3 = Car.this.quality;
}
public void showPrivateFieldCarDoor(){
String e = carDoor;
}
}
}
2.2 从JVM层面理解示例代码
编译后生成两个文件
Car.class
Car$Engine.class
Car$Engine.class 的字节码中,编译器自动添加了一个合成字段this$0,并且重写构造方法
final Car this$0;// 编译器自动生成的引用字段
Car$Engine(Car car) {
this.this$0 = car; //构造时绑定外部实例
super();
}
堆内存布局:
[Car实例]
[Engine实例] ────> this$0 ────────┐
(引用) │
▼
[Car实例] ←─ 通过 this$0 可访问所有外部成员
2.3 注意的问题
1、不能声明静态成员(除 static final 常量)
class Inner {
static int x = 10; // 编译错误:内部类不能有静态成员
static final int Y = 20; // 允许:编译时常量
}
- 因为静态成员属于类级别,而内部类依赖于外部实例,JVM无法为“依赖实例的类”分配类加载时的静态存储区。
2、访问外部类私有成员
内部类可以访问外部类的 private 字段和方法。JVM层面,编译器会生成桥接方法(synthetic accessor):
//访问Car的私有成员变量
public void showPrivateFieldCarDoor(){
String e = carDoor;
}
// 编译器自动生成:
static String access$000(Car car) {
return car.carDoor;
}
- 调用Engine().showPrivateFieldCarDoor() 实际里面有调用access$000(this$0),绕过访问控制。
2 静态内部类
class Outer {
private String outerField = "Outer field";
static String outerStaticWord = "word";
static String name= "OuterTom";
// 静态内部类
static class StaticInner {
String staticInnerField = "staticInnerField";
//静态成员
static int count = 0;
public void print() {
// System.out.println(outerField); // ❌ 编译错误:不能访问非静态成员
String a=outerStaticWord;
String b= staticInnerField;
}
//同名类变量
static String name= "StaticInnerTom";
public static void show(){
//访问内部类的name
String c = name;
//访问外部类的name
String d = Outer.name;
}
}
public static void main(String[] args) {
Outer.StaticInner inner = new Outer.StaticInner(); // ✅ 直接创建,无需外部实例
inner.print();
System.out.println(Outer.StaticInner.count); // ✅ 可直接通过类名访问
}
}
- 编译后:Outer.class 和 Outer$StaticInner.class
- Outer$StaticInner.class 没有 this$0 字段
- 构造方法是默认的 Outer$StaticInner()
- 它完全独立于外部类实例,就像一个顶级类,只是命名空间被限定在 Outer 下
- JVM行为 :
-
- 加载 Outer$StaticInner.class(类加载器在首次使用时加载)
-
- 在堆中创建 Outer$StaticInner 实例
-
- 不涉及 Outer 实例的创建或引用
-
- 静态内部类不持有外部类引用
3 局部内部类
public class Outer {
private String outerField = "Outer";
public void method() {
final String localVar = "Local"; // 必须是 final
// 局部内部类:定义在方法内
class LocalInner {
void print() {
System.out.println(outerField); // ✅ 访问外部类成员
System.out.println(localVar); // ✅ 访问局部变量(必须是final)
}
}
LocalInner li = new LocalInner();
li.print();
}
public static void main(String[] args) {
new Outer().method();
}
}
3.2 JVM与类加载视角
-
编译后生成:Outer.class 和 Outer$1LocalInner.class
-
LocalInner 的构造方法:
Outer$1LocalInner(Outer outer, String localVar) {
this.this$0 = outer;
this.val$localVar = localVar; // 编译器将局部变量复制为合成字段
super();
}
3.3 对象实例化与“final”要求
- 局部变量 localVar 被复制到内部类的合成字段中
- 局部变量存储在栈帧中,方法结束后栈帧销毁。但内部类对象可能在方法结束后仍存活(如被返回)。为保证一致性,JVM复制值而非引用。若允许修改,会导致数据不一致。
// ❌ 错误示例
public void method() {
String localVar = "Local";
class LocalInner {
void print() {
System.out.println(localVar);
}
}
localVar = "Changed"; // ❌ 编译错误:localVar 不再是 final
LocalInner li = new LocalInner();
li.print(); // 如果允许,应该输出 "Local" 还是 "Changed"?不确定!
}
4 匿名内部类
5 JVM类加载与内部类的加载时机
类加载过程(Class Loading)对内部类的影响
| 类型 | 加载时机 | 是否触发外部类加载 |
|---|---|---|
| 静态内部类 | 首次使用 Outer.StaticInner 时 | ✅ 是(因为需要解析 Outer) |
| 成员内部类 | 创建 outer.new Inner() 时 | ✅ 是(必须先有 Outer 实例) |
| 局部内部类 | 方法中首次 new LocalInner() 时 | ✅ 是 |
| 匿名内部类 | 执行 new Runnable(){...} 时 | ✅ 是 |
- 即使未创建内部类实例,只要代码中引用了内部类的类名(如 Outer.Inner.class),JVM就会加载 Outer$Inner.class,并触发 Outer.class 的加载。
Class<?> clazz = Outer.Inner.class; // ✅ 触发 Outer 和 Outer$Inner 加载
浙公网安备 33010602011771号