Java 内部类详解
一、什么是内部类?
在 Java 中,内部类(Inner Class) 就是定义在另一个类内部的类。
java
public class OuterClass { // 外部类(外层的类)
class InnerClass { // 内部类(定义在外部类里面)
}
}
简单来说:
- 外部类:包含其他类的那个类
- 内部类:被包含在另一个类里面的类
二、为什么需要内部类?
在讲具体分类之前,我们先理解内部类存在的意义:
1. 逻辑上的归属关系
有些类,它的存在只对某一个类有意义,没有必要单独写成一个独立的类文件。
比如:汽车有发动机,发动机只属于汽车,我们可以把发动机类写在汽车类内部。
2. 访问权限的便利性
内部类可以直接访问外部类的所有成员 ,包括 private 修饰的私有成员,这是普通类做不到的。
3. 实现更好的封装
把只在某个类内部使用的类,隐藏在外部类里面,外界无法直接访问,提高了封装性。
三、内部类的四种分类
Java 内部类分为四种,这是面试中最常被问到的知识点:
| 类型 | 定义位置 | 关键特点 |
|---|---|---|
| 成员内部类 | 类的内部,方法的外部 | 最普通的内部类 |
| 静态内部类 | 类的内部,用 static 修饰 | 不依赖外部类实例 |
| 局部内部类 | 方法内部 | 只在方法内有效 |
| 匿名内部类 | 方法内部,没有名字 | 一次性使用 |
四、成员内部类(Member Inner Class)
4.1 基本定义
成员内部类是最基础的内部类,定义在外部类的成员位置(和成员变量、成员方法平级)。
java
public class Outer {
private String name = "外部类的name";
private int age = 18;
// 成员内部类
class Inner {
private String name = "内部类的name";
public void show() {
// 内部类可以直接访问外部类的私有成员
System.out.println("外部类的age: " + age);
// 如果内部类和外部类有同名变量,用 外部类名.this.变量名 区分
System.out.println("外部类的name: " + Outer.this.name);
System.out.println("内部类的name: " + name);
}
}
public void test() {
// 外部类访问内部类,需要创建内部类对象
Inner inner = new Inner();
inner.show();
}
}
4.2 如何创建成员内部类的对象
这是面试常考点!成员内部类的对象创建方式比较特殊:
java
public class Test {
public static void main(String[] args) {
// 第一步:创建外部类对象
Outer outer = new Outer();
// 第二步:通过外部类对象创建内部类对象
Outer.Inner inner = outer.new Inner();
// 调用内部类方法
inner.show();
}
}
⚠️ 注意:成员内部类依赖于外部类的实例,必须先有外部类对象,才能创建内部类对象。
4.3 成员内部类的访问规则总结
java
public class Outer {
private int x = 10;
class Inner {
private int x = 20;
public void method() {
int x = 30;
System.out.println(x); // 30,局部变量
System.out.println(this.x); // 20,内部类的成员变量
System.out.println(Outer.this.x);// 10,外部类的成员变量
}
}
}
五、静态内部类(Static Inner Class)
5.1 基本定义
静态内部类就是用 static 关键字修饰的内部类。
java
public class Outer {
private String name = "外部类name";
private static int count = 100; // 外部类静态变量
// 静态内部类
static class StaticInner {
public void show() {
// 静态内部类 不能 直接访问外部类的非静态成员
// System.out.println(name); // ❌ 编译错误!
// 静态内部类 可以 直接访问外部类的静态成员
System.out.println(count); // ✅ 可以访问
}
}
}
5.2 如何创建静态内部类的对象
静态内部类不需要外部类的实例,可以直接创建:
java
public class Test {
public static void main(String[] args) {
// 直接创建静态内部类对象,不需要先创建外部类对象
Outer.StaticInner inner = new Outer.StaticInner();
inner.show();
}
}
5.3 静态内部类 vs 成员内部类 对比
| 对比项 | 成员内部类 | 静态内部类 |
|---|---|---|
| 是否需要外部类实例 | ✅ 需要 | ❌ 不需要 |
| 能否访问外部类非静态成员 | ✅ 能 | ❌ 不能 |
| 能否访问外部类静态成员 | ✅ 能 | ✅ 能 |
| 自身能否有静态成员 | ❌ 不能(JDK16之前) | ✅ 能 |
六、局部内部类(Local Inner Class)
6.1 基本定义
局部内部类定义在方法内部,就像局部变量一样,只在该方法内有效。
java
public class Outer {
public void method() {
int num = 10; // 局部变量
// 局部内部类,定义在方法内部
class LocalInner {
public void show() {
// 可以访问外部类的成员
// 可以访问所在方法的局部变量(必须是 final 或 effectively final)
System.out.println("num = " + num);
}
}
// 在方法内部创建并使用局部内部类
LocalInner localInner = new LocalInner();
localInner.show();
}
}
6.2 关于 effectively final
局部内部类访问方法中的局部变量时,该变量必须是 final 或事实上不可变的(effectively final)。
java
public void method() {
int a = 10; // 没有被修改过,是 effectively final ✅
int b = 20;
b = 30; // 被修改了,不是 effectively final ❌
class LocalInner {
public void show() {
System.out.println(a); // ✅ 可以访问
// System.out.println(b); // ❌ 编译错误
}
}
}
💡 为什么有这个限制?
局部变量存在于栈内存中,方法执行完就销毁了。但内部类对象可能还存活着。为了保证内部类访问的变量值是一致的,Java 要求该变量不能被修改。
七、匿名内部类(Anonymous Inner Class)⭐
7.1 这是最重要的内部类!
匿名内部类是没有名字的内部类,它在定义的同时就创建了对象。
匿名内部类必须继承一个类 或者实现一个接口。
7.2 基本语法
java
new 父类/接口() {
// 重写方法
// 类体内容
};
7.3 具体示例
场景:有一个接口
java
// 定义一个接口
interface Animal {
void speak();
}
传统方式(需要单独定义实现类):
java
// 需要专门写一个实现类
class Dog implements Animal {
@Override
public void speak() {
System.out.println("汪汪汪");
}
}
// 使用
Animal animal = new Dog();
animal.speak();
匿名内部类方式(直接在使用的地方定义):
java
// 不需要单独定义实现类,直接用匿名内部类
Animal animal = new Animal() {
@Override
public void speak() {
System.out.println("汪汪汪");
}
};
animal.speak();
7.4 匿名内部类的本质
java
Animal animal = new Animal() {
@Override
public void speak() {
System.out.println("汪汪汪");
}
};
这行代码做了三件事:
- 定义了一个类,这个类实现了 Animal 接口
- 创建了这个类的对象
- 把对象赋值给 animal 变量
这个类没有名字,所以叫"匿名"内部类。
7.5 匿名内部类的常见使用场景
场景1:作为方法参数传递
java
public class Test {
// 方法接收一个 Runnable 接口
public static void doSomething(Runnable r) {
r.run();
}
public static void main(String[] args) {
// 传入匿名内部类
doSomething(new Runnable() {
@Override
public void run() {
System.out.println("执行任务");
}
});
}
}
场景2:排序时传入比较器
java
import java.util.*;
public class Test {
public static void main(String[] args) {
List<String> list = Arrays.asList("banana", "apple", "cherry");
// 使用匿名内部类实现 Comparator 接口
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
System.out.println(list); // [apple, banana, cherry]
}
}
7.6 匿名内部类 vs Lambda 表达式
在 Java 8 之后,函数式接口(只有一个抽象方法的接口)的匿名内部类可以用 Lambda 表达式简化:
java
// 匿名内部类写法
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// Lambda 表达式写法(更简洁)
Runnable r2 = () -> System.out.println("Hello");
⚠️ Lambda 只能替代函数式接口的匿名内部类,普通类或有多个方法的接口不能用 Lambda。
八、内部类的完整总结
内部类
├── 成员内部类 → 定义在类中,需要外部类实例才能创建
├── 静态内部类 → 用 static 修饰,不需要外部类实例
├── 局部内部类 → 定义在方法中,只在方法内有效
└── 匿名内部类 → 没有名字,定义时立即创建对象,最常用
九、面试题与回答话术
面试题 1:Java 内部类有哪几种?分别有什么特点?
回答话术:
Java 内部类分为四种:
第一种是成员内部类 ,定义在外部类的成员位置,可以访问外部类的所有成员包括私有成员。创建成员内部类的对象时,必须先创建外部类的对象,语法是
外部类对象.new 内部类()。第二种是静态内部类 ,用 static 修饰,不依赖外部类的实例,可以直接通过
new 外部类.内部类()创建。静态内部类不能直接访问外部类的非静态成员,只能访问静态成员。第三种是局部内部类,定义在方法内部,只在该方法内有效。局部内部类访问方法中的局部变量时,该变量必须是 final 或 effectively final。
第四种是匿名内部类,没有名字,必须继承一个类或实现一个接口,在定义的同时创建对象。常用于事件监听、回调、排序等场景,在 Java 8 之后函数式接口的匿名内部类可以用 Lambda 表达式替代。
面试题 2:成员内部类和静态内部类有什么区别?
回答话术:
主要有以下几点区别:
依赖关系:成员内部类依赖外部类的实例,必须通过外部类对象来创建;静态内部类不依赖外部类实例,可以直接创建。
访问权限:成员内部类可以访问外部类的所有成员,包括非静态成员;静态内部类只能访问外部类的静态成员,不能直接访问非静态成员。
使用场景:如果内部类需要访问外部类的实例成员,用成员内部类;如果内部类不需要访问外部类的实例成员,用静态内部类,这样可以避免内存泄漏,因为静态内部类不持有外部类的引用。
面试题 3:匿名内部类是什么?有什么使用限制?
回答话术:
匿名内部类是没有名字的内部类,它在定义的同时就创建了对象。匿名内部类必须继承一个类或者实现一个接口。
使用限制主要有:
- 匿名内部类没有名字,所以不能定义构造方法
- 匿名内部类不能是 abstract 的,因为创建的同时就要实例化
- 匿名内部类不能定义静态成员(JDK16之前)
- 访问外部方法的局部变量时,该变量必须是 final 或 effectively final
常见使用场景是作为方法参数传递,比如线程的 Runnable、排序的 Comparator、各种事件监听器等。
面试题 4:为什么局部内部类访问局部变量时,局部变量必须是 final 的?
回答话术:
这是因为生命周期不一致的问题。局部变量存储在栈内存中,当方法执行完毕后,栈帧销毁,局部变量也就消失了。但是内部类创建的对象存储在堆内存中,它的生命周期可能比方法更长。
为了解决这个问题,Java 会把局部变量的值拷贝一份到内部类中。如果允许局部变量被修改,那么内部类中拷贝的值和外部的值就会不一致,导致数据不同步的问题。所以 Java 要求这个变量必须是 final 或 effectively final(事实上不可变的),来保证两份数据的一致性。
面试题 5:内部类会导致内存泄漏吗?
回答话术:
非静态内部类(成员内部类、匿名内部类)会持有外部类实例的引用,这在某些场景下可能导致内存泄漏。
最典型的例子是在 Android 开发中,如果在 Activity 中定义了一个非静态的 Handler 匿名内部类,Handler 持有 Activity 的引用,如果消息队列中还有未处理的消息,Activity 想要销毁时就无法被垃圾回收,从而导致内存泄漏。
解决方案是使用静态内部类,静态内部类不持有外部类的引用,不会导致这个问题。如果静态内部类确实需要访问外部类的成员,可以通过**弱引用(WeakReference)**来持有外部类对象。