Java内部类详解
一、内部类概述
当一个事物的内部还存在一个需要完整结构描述的部分,且该结构仅服务于外部事物时,推荐使用内部类。在Java中,可以将⼀个类定义在另⼀个 类或者⼀个⽅法的内部,前者称为内部类,后者称为外部类。内部类也是封装的⼀种体现。
基本语法
public class OutClass { // 外部类
class InnerClass { // 内部类
}
}
注意事项
- 定义在
class
花括号外部的类不是内部类
public class A {}
class B {} // A和B是两个独立类
- 编译后会生成独立字节码文件,命名格式:
外部类$内部类.class
内部类和外部类共⽤同⼀个java源⽂件,但是经过编译之后,内部类会形成单独的字节码⽂件
二、内部类分类
1. 成员内部类
① 静态内部类
public class OutClass {
public int a;
public static int b;
static class InnerClass {
public void methodInner() {
// 只能访问外部类静态成员
b = 200;
// a = 100; // 编译错误
}
}
}
特点:
- 使用
static
修饰 - 只能访问外部类的静态成员
- 无需外部类实例即可创建
OutClass.InnerClass inner = new OutClass.InnerClass();
② 实例内部类
public class OutClass {
public int a;
public static int b;
public int c = 1;
class InnerClass {
int c = 2;
public void methodInner() {
a = 100; // 访问外部类实例成员
b = 200; // 访问外部类静态成员
c = 300; // 访问内部类成员
OutClass.this.c = 400; // 访问外部类同名成员
}
}
}
特点:
- 必须通过外部类实例创建
- 可访问外部类所有成员
- 访问同名成员时使用
外部类名.this.成员名
OutClass.InnerClass inner = new OutClass().new InnerClass();
对比总结:
特性 | 静态内部类 | 实例内部类 |
---|---|---|
外部类依赖 | 无需实例 | 必须依赖实例 |
访问外部类成员 | 仅静态成员 | 全部成员 |
内存泄漏风险 | 低 | 高 |
2. 局部内部类
定义在方法体内的内部类,使用频率极低。
public class OutClass {
int a = 10;
public void method() {
int b = 20;
class InnerClass { // 局部内部类
public void print() {
System.out.println(a + " " + b);
}
}
new InnerClass().print();
}
}
特点:
- 仅在定义的方法体内有效
- 不能使用访问修饰符
- 编译后生成
外部类$数字内部类.class
3. 匿名内部类
没有类名的即时类声明,适用于一次性使用的场景。
语法格式:
new 父类/接口() {
// 类体实现
};
接口实现示例:
interface Greeting {
void greet();
}
public class Test {
public static void main(String[] args) {
Greeting greeting = new Greeting() {
@Override
public void greet() {
System.out.println("Hello!");
}
};
greeting.greet();
}
}
Java匿名内部类的访问规则如下:
1. 访问外部类的成员
规则:
如果在 实例方法 中创建匿名类,可以访问外部类的 所有成员(包括私有成员)。
如果在 静态方法 中创建匿名类,只能访问外部类的 静态成员。
class Outer {
private int instanceVar = 10; // 实例变量
static int staticVar = 20; // 静态变量
void instanceMethod() {
new Runnable() {
@Override
public void run() {
System.out.println(instanceVar); // ✅ 正确
System.out.println(staticVar); // ✅ 正确
}
};
}
static void staticMethod() {
new Runnable() {
@Override
public void run() {
// System.out.println(instanceVar); // ❌ 错误:无法访问实例变量
System.out.println(staticVar); // ✅ 正确
}
};
}
}
2. 访问局部变量
规则:
匿名类访问的 方法内的局部变量或参数 必须是 final 或 等效final(Java 8+ 允许隐式 final,但变量值不可修改)。
void demoMethod() {
final int finalVar = 30; // 显式final
int effectivelyFinal = 40; // 等效final(未被修改)
// effectivelyFinal = 50; // 如果取消注释,将不再是等效final
new Runnable() {
@Override
public void run() {
System.out.println(finalVar); // ✅ 正确
System.out.println(effectivelyFinal); // ✅ 正确
// effectivelyFinal++; // ❌ 错误:不可修改
}
};
}
3. 静态成员的限制
规则:
匿名类中 不能声明非终态的静态成员(如 static int x;),但允许声明 static final 常量。
new Runnable() {
static final int MAX = 100; // ✅ 正确:静态常量
// static int count = 0; // ❌ 错误:非终态静态成员
int instanceCount = 0; // ✅ 正确:实例变量
};
4. 构造器和初始化
规则:
匿名类没有显式构造器,但可以通过 实例初始化块 初始化成员。
new Thread() {
private String message;
{ // 实例初始化块(类似构造器)
message = "初始化完成";
}
@Override
public void run() {
System.out.println(message); // 输出:初始化完成
}
};
5. 继承与实现
规则:
匿名类 只能继承一个类 或 实现一个接口,不能同时继承和实现多个
// 正确:实现一个接口
new Runnable() {
@Override public void run() {}
};
// 正确:继承一个类
new Thread() {
@Override public void run() {}
};
// 错误:同时继承和实现(语法不允许)
// new Thread() implements Runnable { ... }
示例说明
class Outer {
private int outerField = 10;
static int staticField = 20;
void instanceMethod() {
int localVar = 30; // 等效final
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(outerField); // 正确:访问外部类实例成员
System.out.println(staticField); // 正确:访问外部类静态成员
System.out.println(localVar); // 正确:localVar是等效final
}
};
new Thread(r).start();
}
static void staticMethod() {
Runnable r = new Runnable() {
@Override
public void run() {
// System.out.println(outerField); // 错误:静态上下文中无法访问实例成员
System.out.println(staticField); // 正确:访问静态成员
}
};
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.instanceMethod();
Outer.staticMethod();
}
}
上述代码运行结果
关键点总结
- 外部成员访问:依赖上下文(实例/静态)决定能否访问非静态成员。
- 局部变量final:确保数据一致性,防止异步修改。
- 静态与构造限制:匿名类的特性导致无法定义构造器和非终态静态成员。
- 单继承/实现:语法限制,保持简洁性。
理解这些规则可避免常见错误,如修改局部变量或错误访问静态上下文中的实例成员。
注意事项:
- 可以定义成员变量,但不能包含执行语句
- 编译后生成
外部类$数字.class
- 常用于事件监听、线程实现等场景
以下是一道匿名内部类的典型题目
下面 Java 代码的运行结果为()
public class Main {
private int num = 10;
public static void main(String[] args) {
Main main = new Main() {
@Override
public void printNum() {
System.out.println(num);
}
};
main.printNum();
}
public void printNum() {
System.out.println(num);
}
}
这道题考察了Java匿名内部类的语法规则和访问特性。
在该代码中存在编译错误,主要原因是在匿名内部类中访问外部类的私有成员变量num时违反了Java的访问规则。
- 匿名内部类通过new Main(){}的方式创建,并重写了printNum()方法
- 在匿名内部类中的静态main方法直接访问外部类的私有成员实例变量num是不允许的
- 要访问外部类的私有成员,需要使用Main.this.num的形式,但由于是static方法我们无法通过Main.this.num
- 另外,匿名内部类中的变量会隐藏外部类的同名变量
要修复这个编译错误,可以有以下方法:
将num改为public访问权限,在匿名内部类中使用Main.this.num来访问,或者改用局部变量来实现相同功能
这个题目很好地体现了Java内部类访问外部类成员的规则限制。
三、使用建议
- 优先选择静态内部类:降低内存泄漏风险
- 实例内部类慎用:注意外部类实例的生命周期管理
- 匿名内部类适用场景:单次使用的接口/抽象类实现
- 避免使用局部内部类:可读性和维护性较差
四、常见面试题
-
内部类如何访问外部类同名成员?
- 使用
外部类名.this.成员名
语法
- 使用
-
为什么实例内部类会持有外部类引用?
- 为实现直接访问外部类成员的功能设计
-
如何避免内部类导致的内存泄漏?
- 使用静态内部类
- 及时解除外部类实例的强引用
通过本文可以全面掌握Java内部类的核心特性和使用场景。