一、内部类(Inner Class) / 嵌套类(Nested Class)
1、定义
- 将一个 类定义在另一个类里面或一个方法里面,这样的类称为 内部类。
- 内部类 - 寄生类
- 外部类 - 宿主类
2、内部类 的 类型
类型 | 修饰符 | 依赖外部实例 | 访问外部成员 | 作用域 | 典型场景 |
---|---|---|---|---|---|
静态嵌套类 | static | 否 | 仅静态成员 | 全局 | 工具类、辅助类 |
成员内部类 | 无 | 是 | 所有成员 | 外部类内部 | 迭代器、紧密耦合逻辑 |
局部内部类 | 无 | 是 | 方法内的 有效 final | 方法 / 代码块 内 | 临时逻辑封装 |
匿名内部类 | 无 | 是 | 方法内的 有效 final | 单次使用 | 回调、事件监听 |
3、内部类 的 修饰符
内部类类型 | 允许的修饰符 |
---|---|
静态内部类 | 访问控制修饰符:public / protected / 默认 / private 非访问修饰符:static(必须)、final、abstract、strictfp |
成员内部类 | 访问控制修饰符:public / protected / 默认 / private 非访问修饰符:final、abstract、strictfp |
局部内部类 | 访问控制修饰符:无 非访问修饰符:final、abstract、strictfp |
匿名内部类 | 无修饰符 |
二、外部类 与 静态内部类、成员内部类 的相互访问关系
1、静态内部类
- 外部类 访问 静态内部类 的 静态方法、静态属性:
- 静态内部类.静态方法(); 静态内部类.静态属性; 静态内部类.静态常量 ;
- 外部类 访问 静态内部类 的 成员方法、成员属性:
- 静态内部类对象.成员方法(); 静态内部类对象.成员属性;
2、成员内部类
- 外部类 访问 成员内部类 的 成员方法、成员属性:
- 成员内部类对象.成员方法(); 成员内部类对象.成员属性;
- 外部类 访问 成员内部类 的 静态常量:【 成员内部类 中 不能有 静态方法、静态属性】
- 成员内部类.静态常量 ;
三、局部内部类 和 匿名内部类 不能有访问控制修饰符?
内部类类型 | 是否有类名? | 作用域 | 是否允许访问修饰符 | 设计目的 |
---|---|---|---|---|
静态嵌套类 | ✔️ | 全局(按修饰符限制) | ✔️ | 工具类、辅助类 |
成员内部类 | ✔️ | 外部类实例相关 | ✔️ | 紧密耦合外部实例的逻辑 |
局部内部类 | ✔️ | 方法/代码块内 | ❌ | 临时逻辑封装 |
匿名内部类 | ❌ | 定义的代码块内 | ❌ | 一次性实现接口或继承类 |
1、作用域的限制
- 局部内部类 和 匿名内部类 只能在定义它的方法或代码块内部被实例化和使用 。
- 即:作用域仅限于该方法或代码块内部。外部无法直接访问该类。
- 因此,访问修饰符在此场景下没有意义。即便允许添加修饰符,也无法改变其作用域的限制。
2、Java 语言规范限制
- 根据 Java 语法规则,局部内部类 和 匿名内部类 不能声明任何访问修饰符。
- 局部内部类是方法的局部成员,而不是类的成员,因此,不适用类成员的访问控制规则。
- 若允许修饰符,会导致语法冗余和矛盾,因为,局部内部类的作用域已经隐式受限。
- 由于匿名内部类 没有显式的 类名,修饰符无法附加到类声明上。
- 因此,语法上直接禁止使用修饰符。
3、设计哲学:简化与一致性
- 简化语义:
- 局部内部类的作用是辅助方法内部逻辑,无需暴露给外部。
- 匿名内部类的设计目标是快速实现 接口 或 继承类,专注于 一次性使用。无需暴露给外部。
- 禁止修饰符可以避免开发者误用(如:声明 public 但实际无法全局访问)。
- 一致性:
- Java 其它局部定义的元素(如:局部变量)也不能使用访问修饰符。
- 局部内部类的规则与此一致。
四、成员、局部、匿名 内部类中为何不可以定义静态成员?
- Java 16 之前的限制如下:
内部类类型 | 允许的静态成员 | 禁止的静态成员 | 根本原因 |
---|---|---|---|
成员内部类 | static final 常量 | 普通 static 方法、变量 | 依赖外部类实例,与静态成员的独立性矛盾 |
局部内部类 | static final 常量 | 普通 static 方法、变量 | 作用域临时性,无法保证类级别存在 |
匿名内部类 | static final 常量 | 普通 static 方法、变量 | 匿名性导致无法通过类名访问 |
静态内部类 | 所有 static 成员 | 无 | 独立于外部类实例,生命周期一致 |
- Java 16 开始,就没有以上的限制了。
- 如:匿名内部类 的代码示例
interface Workable{
void work();
}
/**
* 匿名内部类
* JDK 16 之前,匿名内部类 的 内部:
* 可以有 static final 修饰的常量、初始化块、成员变量、成员方法。
* 不能有 静态成员变量,静态初始化块、静态方法。
* JDK 16 开始,匿名内部类 的 内部:
* 可以有 static final 修饰的常量、初始化块、成员变量、成员方法、静态成员变量,静态初始化块、静态方法。
*/
public class AnonymousInnerClass {
public static void main(String[] args) {
// 此处相当于:
// 1,创建 Workable 的匿名的实现类。
// 2,并创建匿名的实现类的实例。
// 将实现类的实例赋给接口变量,典型的向上转型。
Workable w = new Workable(){
static final int B = 1000;
static int aaa = 1000;
// 由于匿名内部类没有类名,所以没有构造器。
private int age = 20;
static {
System.out.println(aaa);
test11();
}
{
System.out.println("匿名内部类的初始化块!");
test();
}
@Override
public void work(){
System.out.println("aaa");
}
public void test(){
System.out.println("匿名内部类的 普通方法");
}
public static void test11(){
System.out.println("匿名内部类的静态方法!!!");
}
};
}
}
1、成员内部类:
- 1、成员内部类:
- 默认持有外部类的实例引用(OuterClass.this),其存在依赖于外部类的实例 。
- 理解:
- 成员内部类作为外部类的成员变量 。
- 因此,要访问成员内部类,就必须先创建外部类的实例。
- 利用外部类的实例,才能访问成员内部类。
- 2、静态属性属于类级别:
- 静态属性在类加载时进行初始化,不依赖任何实例。
- 3、矛盾点:
- 如果允许成员内部类定义普通静态成员,这些静态成员需要独立于外部类实例 存在。
- 但是,要访问成员内部类,就必须先创建外部类的实例。
- 这就导致逻辑冲突。
- 允许 static final 常量的原因:
- static final 常量在编译期确定值,不依赖运行时状态。
- 如: static final int MAX = 100 。
- 因此,不会破坏成员内部类的实例绑定逻辑。
2、局部内部类 (不存在 static 修饰的 局部内部类)
1、Java 语言规范的限制
- Java语言规范(JLS)明确规定:
- 局部内部类不能包含静态声明(static字段、方法或静态初始化块)。
- 编译时常量除外,即: static final 常量。
- 设计目的:
- 避免复杂性。
- 局部内部类的作用:封装与当前方法相关的逻辑,静态成员通常用于全局状态或工具方法。
- 与局部作用域的设计初衷冲突。
2、隐式依赖外部实例
- 如果局部内部类定义在非静态上下文中(如:实例方法)时:
- 它会隐式持有 外部类的实例引用(即使未显式使用 Outer.this)。
- 这使得局部内部类的实例与外部类实例绑定。
- 静态成员不依赖实例:
- 它们独立于对象存在。
- 若允许静态成员,会导致语义矛盾:
- 静态成员需要不依赖外部实例,但局部内部类本身却隐式依赖外部实例。
3、生命周期与作用域的不匹配
- 作用域限制:
- 局部内部类的作用域仅限于定义它的代码块。
- 局部内部类的存在依赖于外部方法的调用。
- 当方法执行结束后,局部内部类理论上可能不再可用。
- 静态成员属于类级别:
- 与类的生命周期绑定(类加载时初始化,程序结束时销毁)。
- 如果允许局部内部类定义静态成员,会导致矛盾:
- 1、静态成员需要在类加载时初始化,但局部内部类可能仅在方法调用时存在。
- 2、若方法被多次调用,静态成员的状态可能被意外修改,引发不可预测的行为。
- 允许 static final 常量的原因:
- static final 常量在编译期直接写入常量池,不依赖类加载过程,
- 因此,即使局部内部类未被实例化,常量值依然有效。
3、匿名内部类:
1、Java 语言规范的限制
- Java语言规范(JLS)明确规定:
- 匿名内部类不能包含静态声明( static 字段、方法或静态初始化块)。
- 编译时常量 除外,即: static final 常量。
- 设计目的:
- 匿名内部类的核心用途是快速实现接口或继承类,并封装与当前上下文相关的逻辑。
- 静态成员通常用于全局共享状态或工具方法。
- 这与匿名内部类的临时性、局部性设计目标冲突。
2、隐式依赖外部实例
- 如果匿名内部类定义在非静态上下文中(如:实例方法)时:
- 它会隐式持有 外部类的实例引用(即使未显式使用 Outer.this)。
- 这使得匿名内部类的实例与外部类实例绑定。
- 静态成员不依赖实例:
- 它们独立于对象存在。
- 若允许静态成员,会导致语义矛盾:
- 静态成员需要不依赖外部实例,但匿名内部类本身却隐式依赖外部实例。
3、匿名内部类的本质
- 静态成员属于类级别:
- 与类的生命周期绑定(类加载时初始化,程序结束时销毁)。
- 如果允许匿名内部类定义静态成员,会导致矛盾:
- 1、静态成员属于类级别,但匿名内部类没有 显式的类名,无法通过类名直接访问静态成员。
- 2、匿名内部类的实例可能被多次创建,静态成员的状态可能被意外共享或污染。
- 如:在循环中。
五、成员、局部、匿名 内部类 中为何 可以定义静态常量?
1、静态常量 与 普通静态成员的对比
特性 | 普通静态成员 | 常量( static final ) |
---|---|---|
初始化时机 | 类加载时初始化 | 编译期确定值,直接嵌入 字节码 |
生命周期依赖 | 依赖类的加载和卸载 | 无依赖,与程序共存 |
状态管理风险 | 可能被修改,需考虑线程安全 | 不可变,天然线程安全 |
内部类中是否允许 | 成员、局部、匿名 内部类 中 禁止 | 允许(需满足常量条件) |
2、常量的特性:编译期 确定 值
- 常量( static final 且初始化为字面量或常量表达式)的值在 编译期 就已确定,并被直接嵌入到字节码中。
- 无需类加载过程:常量的值不依赖类的初始化或实例化,因此不会与内部类的生命周期冲突。
3、不可变性:避免状态管理问题
- 常量不可修改:
- final 关键字确保其值不可变。因此,不存在多线程或多次调用导致的共享状态问题。
- 静态成员的风险:
- 普通静态成员可能因状态变化引发问题,但常量天然线程安全且无状态依赖。
4、Java语言规范的特殊允许
- 规范支持:Java语言规范(JLS)对常量有特殊豁免。
- 只要满足以下条件,内部类可以声明 static final 常量:
- 字段是 static final。
- 初始化值为 常量表达式(如:字面量、算术表达式、其它常量等)。
void method() {
class LocalInner {
static final String GREETING = "Hello"; // 允许:字符串字面量
static final int SUM = 1 + 2; // 允许:常量表达式
}
}
5、生命周期无关性
- 局部内部类 和 匿名内部类 的生命周期依赖于方法或代码块的执行。
- 但常量的值在编译期已固定,无需依赖类的实例化或方法调用。
- 成员内部类 的生命周期与外部类实例绑定,但常量的值 独立于任何实例存在。
六、局部内部类 和 匿名内部类 只能访问局部 final 变量?
1、Java 8 的 effectively final
- 从 Java 8 开始,语法进一步简化,即:effectively(有效的) final:
- 没有显式声明 final,只要变量在初始化后未被修改,编译器会自动视为 final 变量。
- 示例:
void example() {
final int x = 10;
new Thread(() -> System.out.println(x)).start();
}
// effectively final : 就是将 上边 x 前边的 final 去掉了而已。
void example() {
// effectively final(未再修改)
int x = 10;
// 合法
new Thread(() -> System.out.println(x)).start();
}
- 如果后边要修改 x 变量的值,就需要将 x 的重新 复制 一份,传递到匿名内部类中。
void example() {
int x = 10;
// 想要将 x 的值,传入子线程中,并且,主线程后边还会修改 x 的值,就必须将 x 的复制一份。
// 即:实现 effectively final(未再修改)
int finalX = x;
new Thread(() -> System.out.println(finalX)).start();
// 再次修改 x 的变量。
x = 20;
}
2、局部变量 与 匿名内部类、局部内部类 的 生命周期 存在 冲突
- 代码示例(错误示范):
- 编译错误:Variable ‘count’ is accessed from within inner class, needs to be final or effectively final.
public class LifecycleConflictExample {
public static void main(String[] args) {
Runnable runnable = createRunnable();
runnable.run(); // 预期输出 10,实际可能存在问题
}
public static Runnable createRunnable() {
// 非 final 的局部变量
int count = 10;
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Count: " + count); // 编译错误!
}
};
// 修改 局部变量 的 值。
count = 20;
return r;
}
}
3、原因分析
- 生命周期冲突:
- 方法 createRunnable() 执行完毕后,局部变量 count 的栈内存会被释放。
- 但createRunnable()方法,返回的 Runnable 对象(匿名内部类实例)。
- 可能在其他地方被调用,其生命周期远超 createRunnable() 方法。
- 如:main 方法。
- 数据不一致风险:
- 匿名内部类中访问的 count 实际上是编译器 自动生成的副本。
- 若允许修改原变量 count ,内部类副本的值(初始值 10)与原变量不一致,导致逻辑混乱。
- 如:count = 20。
4、解决方案
- 方案 1:使用 final 变量
public static Runnable createRunnable() {
final int count = 10; // 声明为 final
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Count: " + count); // 合法
}
};
// count = 20; // 禁止修改
return r;
}
- 方案 2:利用 effectively final(Java 8+)
public static Runnable createRunnable() {
int count = 10; // effectively final(未再修改)
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Count: " + count); // 合法
}
};
// count = 20; // 若取消注释,会破坏 effectively final,导致编译错误
return r;
}
- 特殊场景:绕过引用修改
- 即使变量是 final,仍可修改其指向对象的内部状态:
public static Runnable createRunnable() {
final List<String> list = new ArrayList<>(); // final 引用
Runnable r = new Runnable() {
@Override
public void run() {
list.add("Hello"); // 合法:修改对象内容,而非引用本身
System.out.println(list);
}
};
// list = new ArrayList<>(); // 禁止修改引用
return r;
}
七、为什么在 Java 中需要内部类?
- Java 引入内部类,核心目的是通过逻辑分组、增强封装和简化协作,提供更灵活的代码组织方式。
- 它在设计模式(如:工厂模式、迭代器模式)、GUI 编程、回调机制等场景中广泛应用。
- 内部类是 Java 面向对象设计的重要补充。
1、增强封装性
- 隐藏实现细节:内部类可以对外部完全隐藏,仅服务于其外部类。
- 如:一个链表(LinkedList)的内部节点类(Node)通常不需要暴露给外部。
- 通过内部类实现能更好地封装数据结构。
- 访问控制:内部类可以声明为 private 或 protected ,限制其它类的访问。
- 确保只有外部类或特定子类能使用它。
2、直接访问外部类成员
- 成员内部类隐式持有外部类的引用。
- 因此,可以直接访问外部类的所有成员(包括:私有属性和方法)。
- 无需通过 getter 或 public 暴露字段。
- 这在需要频繁交互的场景(如:迭代器实现)中非常方便。
3、代码组织与可读性
- 逻辑分组:将紧密关联的类放在一起,提升代码的可维护性。
- 如:一个 Car 类可以包含 Engine 、 Wheel 等内部类。
- 明确表达它们属于 Car 的组成部分。
- 减少命名冲突:内部类的作用域被限定在外部类内,避免了全局类名的重复。
4、简化回调与事件处理
- 在需要快速实现接口(如:事件监听器)时,匿名内部类可以就地定义并实例化。
- 尤其在 Java 8 之前广泛用于 GUI 事件处理(如:Swing)。
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked!");
}
});