在 Java 中,内部类就是定义在另一个类内部的类。它主要用于逻辑分组、增强封装以及实现更优雅的回调(如事件处理)。内部类主要分为四种:成员内部类、静态嵌套类(常被视为第四种,但严格说它不属于内部类)、局部内部类和匿名内部类。
- 成员内部类
定义在外部类的成员位置,与实例变量和方法同级。
· 特性:可以无条件访问外部类的所有成员(包括 private),这相当于持有外部类实例的隐式引用。但外部类要访问内部类成员,必须先创建内部类对象。
· 实例化:必须通过外部类对象来创建。Outer outer = new Outer(); Outer.Inner inner = outer.new Inner();
· 限制:不能定义任何静态成员(除非是 static final 常量),因为它的存在依赖于外部类实例。
- 静态嵌套类
使用 static 修饰的成员内部类,严格说它不属于"内部类"。
· 特性:行为类似于独立的顶级类。它只能访问外部类的静态成员(包括 private static),不能直接访问实例成员。
· 实例化:不需要外部类实例。Outer.StaticNested nested = new Outer.StaticNested();
· 使用场景:常用于构建与外部类逻辑相关但又不依赖其实例的组件,比如 Builder 模式或 Map.Entry 接口的实现。
- 局部内部类
定义在方法或作用域(如 if、for 块)内部的类。
· 特性:作用域仅限于所在的方法/代码块。它可以访问外部类的所有成员,还能访问方法内的局部变量,但该局部变量必须是final 或事实上的不可变(在 Java 8+ 中,称为 effectively final)。
· 原因:内部类对象的生命周期可能比方法长,为保证能安全访问局部变量,Java 会进行值拷贝。加 final 可以保证拷贝的值与原始值始终一致。
· 实例化:只能在定义它的方法内部,在代码执行到 new 时创建对象。
- 匿名内部类
局部内部类的一种特殊形式,没有类名,必须继承一个父类或实现一个接口,且只能创建一个实例。
· 特性:通常用于快速实现回调或事件监听(如 Runnable、ActionListener)。它同样能访问外部类成员和所在方法的 final 局部变量。
· 限制:不能定义构造器,也不能定义静态成员。在 Java 8 之前,要求访问的局部变量必须显式声明 final,之后要求是 effectively final。
· 语法:new 父类/接口() { // 实现或重写方法 };。
核心区别一览
· 能否有静态成员:成员内部类、局部内部类、匿名内部类都不能定义静态成员(常量除外)。静态嵌套类可以。
· 是否需要外部类实例:成员、局部、匿名内部类都需要。静态嵌套类不需要。
· 作用域:成员内部类在整个外部类中有效;局部内部类只在方法/代码块内;匿名内部类在表达式内;静态嵌套类在外部类作用域内。
内存与使用建议
· 内存泄漏风险:成员、局部、匿名内部类都会隐式持有外部类实例的引用。如果一个内部类对象生命周期很长(比如被静态集合引用),就会阻止外部类被垃圾回收。这种情况可以考虑使用静态嵌套类或弱引用来解决。
· 选择建议:内部类不需要访问外部实例成员时,优先使用静态嵌套类(更省内存)。需要实例成员且逻辑内聚,用成员内部类。只在某个方法内部使用一次,用局部内部类。需要快速实现接口或继承类且逻辑简单,用匿名内部类。
常见面试题
- 成员内部类能否有静态方法?
不能,除非该方法是 static final 修饰的常量。
- 为什么局部内部类访问局部变量要 final?
因为内部类对象可能比方法存活更久,Java 通过拷贝变量值来保证一致性,加 final 能防止变量被修改导致拷贝值过期。
- 匿名内部类能否实现多个接口?
不能,它只能继承一个类或实现一个接口。要实现多接口回调,建议使用 Lambda(限函数式接口)或改用局部内部类。