【JavaSE全面教学】Java内部类与枚举Day9(2026年)
写在前面:这是JavaSE系列的第9篇。说实话,内部类和枚举这两个知识点,我当时学的时候觉得特别"鸡肋"——感觉用不上,代码还变复杂了。但之后才发现,Android开发、Spring框架源码、各种设计模式里到处都是内部类和枚举的影子。今天我把这部分讲透,让你看懂框架源码不再懵。

一、为什么需要内部类?从一个Android按钮点击说起
假设你在写Android App,页面上有个按钮,点击后要弹个Toast提示。最简单的写法:
// Android开发中的典型场景
Button button = findViewById(R.id.button);
// 方式1:写一个单独的类(太麻烦,还要新建一个文件)
button.setOnClickListener(new MyClickListener());
// 方式2:匿名内部类(一行搞定,这才是Android开发的日常)
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "点击了按钮", Toast.LENGTH_SHORT).show();
}
});
内部类的核心价值:
- 代码组织:把相关的类放在一起,逻辑更清晰
- 访问控制:内部类可以访问外部类的私有成员
- 简化代码:匿名内部类让代码更简洁(尤其是回调场景)
二、四种内部类详解
2.1 成员内部类:类的"成员"
成员内部类就像类的属性或方法一样,定义在类里面、方法外面。
class Outer {
private String outerName = "外部类";
private int num = 10;
// 成员内部类
class Inner {
private String innerName = "内部类";
private int num = 20; // 和外部类同名变量
public void innerMethod() {
System.out.println("内部类的方法");
// 访问外部类的成员(包括private)
System.out.println("外部类的outerName:" + outerName);
System.out.println("外部类的num:" + Outer.this.num); // 10
System.out.println("内部类的num:" + this.num); // 20
}
}
}
如何创建成员内部类对象?
public class Test {
public static void main(String[] args) {
// 必须先有外部类对象,才能创建内部类对象
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.innerMethod();
}
}
踩坑提醒:
Outer.Inner这种写法只在Java中见过,看起来怪怪的。记住:成员内部类依赖于外部类实例,所以必须先new Outer()。
2.2 静态内部类:不依赖外部类实例
静态内部类用static修饰,它不持有外部类的引用,可以独立存在。
class Outer {
private static int staticNum = 100;
private int instanceNum = 200; // 非静态成员
// 静态内部类
static class StaticInner {
public void method() {
// 只能访问外部类的静态成员
System.out.println("可以访问staticNum:" + staticNum);
// System.out.println(instanceNum); // ❌ 编译错误!
}
}
}
创建静态内部类对象:
// 不需要外部类对象,直接创建
Outer.StaticInner inner = new Outer.StaticInner();
inner.method();
实际应用:HashMap中的Node类就是静态内部类
// HashMap源码节选
public class HashMap<K,V> extends AbstractMap<K,V> {
// Node是静态内部类,不依赖HashMap实例
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// ...
}
}
2.3 局部内部类:方法里的"临时工"
局部内部类定义在方法内部,只在方法内有效,出了方法就访问不到了。
class Outer {
private int num = 10;
public void method() {
final int localVar = 20; // 局部变量(JDK 8+自动加final)
// 局部内部类:只在method方法内有效
class LocalInner {
public void innerMethod() {
System.out.println("可以访问外部类成员:" + num);
System.out.println("可以访问局部变量:" + localVar);
}
}
// 只能在方法内部创建对象
LocalInner inner = new LocalInner();
inner.innerMethod();
}
// LocalInner在这里访问不到!
}
经验之谈:局部内部类在实际开发中用得很少,了解即可。重点掌握成员内部类和静态内部类。
2.4 匿名内部类:最常用,必须掌握
匿名内部类是没有名字的内部类,通常用于一次性使用的场景,比如事件监听、线程创建、回调函数等。
2.4.1 为什么要用匿名内部类?
看一个对比:
// 不用匿名内部类:需要单独写一个类(代码分散,文件多)
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("鸟在飞");
}
}
Flyable f = new Bird();
// 用匿名内部类:一步到位,代码紧凑
Flyable f2 = new Flyable() {
@Override
public void fly() {
System.out.println("鸟在飞");
}
};
2.4.2 匿名内部类的语法
// 语法:new 接口/抽象类() { 实现方法 }
// 实现接口
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("线程运行中");
}
};
new Thread(r).start();
// 继承抽象类
Animal a = new Animal() {
@Override
void cry() {
System.out.println("喵喵喵");
}
};
2.4.3 实际应用场景
场景1:Android按钮点击事件
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 处理点击
}
});
// JDK 8+ 用Lambda简化
button.setOnClickListener(v -> {
// 处理点击
});
场景2:创建线程
// 匿名内部类方式
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程运行");
}
}).start();
// Lambda简化
new Thread(() -> System.out.println("子线程运行")).start();
场景3:排序时的自定义比较器
List<Student> list = new ArrayList<>();
// 匿名内部类
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
return s1.getAge() - s2.getAge();
}
});
// Lambda简化
Collections.sort(list, (s1, s2) -> s1.getAge() - s2.getAge());
核心理解:匿名内部类本质上是一个没有名字、只用一次的类。它让代码更紧凑,但也会增加阅读难度。JDK 8引入的Lambda表达式就是匿名内部类的语法糖。
三、四种内部类对比表
| 类型 | 定义位置 | 是否需要外部类对象 | 能否访问外部类private | 典型应用场景 |
|---|---|---|---|---|
| 成员内部类 | 类中,方法外 | ✅ 需要 | ✅ 可以 | 和外部类紧密关联的对象 |
| 静态内部类 | 类中,方法外,带static | ❌ 不需要 | ⚠️ 只能访问静态成员 | 工具类、数据结构节点(如HashMap.Node) |
| 局部内部类 | 方法内部 | ✅ 需要 | ✅ 可以 | 几乎不用 |
| 匿名内部类 | 方法内部 | ✅ 需要 | ✅ 可以 | 事件监听、回调函数、线程创建 |
四、枚举(Enum):不只是定义常量
4.1 枚举的基本用法
枚举用于定义一组固定的、有限的常量。
// 定义枚举
enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}
// 使用
public class Test {
public static void main(String[] args) {
Season s = Season.SPRING;
System.out.println(s); // SPRING
// switch中使用枚举(最常用)
switch (s) {
case SPRING -> System.out.println("春暖花开");
case SUMMER -> System.out.println("夏日炎炎");
case AUTUMN -> System.out.println("秋高气爽");
case WINTER -> System.out.println("冬雪皑皑");
}
}
}
4.2 枚举的构造方法和属性
枚举可以像类一样定义属性和构造方法:
enum Color {
// 枚举常量,调用构造方法
RED("红色", "#FF0000"),
GREEN("绿色", "#00FF00"),
BLUE("蓝色", "#0000FF");
private final String chineseName;
private final String hexCode;
// 构造方法默认是private(不能改为public)
private Color(String chineseName, String hexCode) {
this.chineseName = chineseName;
this.hexCode = hexCode;
}
public String getChineseName() {
return chineseName;
}
public String getHexCode() {
return hexCode;
}
}
// 使用
Color c = Color.RED;
System.out.println(c.getChineseName()); // 红色
System.out.println(c.getHexCode()); // #FF0000
4.3 枚举的常用方法
// values():获取所有枚举值
Season[] seasons = Season.values();
for (Season s : seasons) {
System.out.println(s);
}
// valueOf():根据字符串获取枚举值
Season s = Season.valueOf("SPRING");
// ordinal():获取枚举值的序号(从0开始)
System.out.println(Season.SPRING.ordinal()); // 0
System.out.println(Season.SUMMER.ordinal()); // 1
// name():获取枚举值的名称
System.out.println(Season.SPRING.name()); // "SPRING"
4.4 枚举实现接口:策略模式
枚举可以实现接口,每个枚举常量提供不同的实现:
interface Operation {
double apply(double x, double y);
}
enum Calculator implements Operation {
ADD {
@Override
public double apply(double x, double y) {
return x + y;
}
},
SUBTRACT {
@Override
public double apply(double x, double y) {
return x - y;
}
},
MULTIPLY {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE {
@Override
public double apply(double x, double y) {
if (y == 0) throw new ArithmeticException("除数不能为0");
return x / y;
}
};
}
// 使用
public class Test {
public static void main(String[] args) {
double result = Calculator.ADD.apply(10, 5); // 15.0
double result2 = Calculator.DIVIDE.apply(10, 2); // 5.0
}
}
经验之谈:这种写法在策略模式中非常优雅,比传统的if-else或switch清晰多了。
4.5 单例模式的最佳实现:枚举
// 饿汉式(传统写法)
class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1() {}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
// 枚举式(《Effective Java》推荐,最简洁、线程安全、防反射攻击)
enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("单例方法");
}
}
// 使用
Singleton.INSTANCE.doSomething();
为什么枚举是单例的最佳实现?
- 线程安全:枚举的实例化由JVM保证线程安全
- 防止反射攻击:反射无法创建枚举实例
- 防止序列化问题:枚举天然支持序列化
- 代码简洁:一行代码搞定
五、面试高频考点
考点1:内部类可以访问外部类的哪些成员?
// 成员内部类、局部内部类、匿名内部类:可以访问外部类的所有成员(包括private)
// 静态内部类:只能访问外部类的静态成员
考点2:为什么成员内部类不能定义静态成员?
class Outer {
class Inner {
// static int num = 10; // ❌ 编译错误!
// 原因:成员内部类依赖于外部类实例,而静态成员不依赖实例,逻辑矛盾
static final int CONSTANT = 100; // ✅ 常量可以(编译期确定)
}
}
考点3:匿名内部类有什么限制?
// 1. 不能定义构造方法(因为没有类名)
// 2. 不能定义静态成员(除了常量)
// 3. 只能创建一个对象
// 4. 必须继承一个父类或实现一个接口
考点4:枚举的构造方法为什么是private?
// 枚举的实例是固定的、有限的,由JVM在类加载时创建
// 不允许外部通过new创建新的枚举实例
// 所以构造方法必须是private(其实不写也是private)
考点5:switch可以用哪些类型?
// JDK 5之前:byte、short、char、int
// JDK 5:增加枚举
// JDK 7:增加String
// JDK 17:增加模式匹配(预览特性)
六、总结
今天我们系统学习了内部类和枚举:
四种内部类:
- 成员内部类:依赖外部类实例,可访问所有成员
- 静态内部类:不依赖实例,只能访问静态成员(HashMap.Node)
- 局部内部类:方法内定义,几乎不用
- 匿名内部类:最常用,用于事件监听、回调、线程创建
枚举的高级用法:
- 定义属性和构造方法
- 实现接口(策略模式)
- 单例模式的最佳实现
核心选择原则:
- 代码需要复用 → 命名内部类
- 只用一次 → 匿名内部类
- 一组固定常量 → 枚举
下一步预告:
Day10我们将学习常用类和包装类——Object、String、Math、日期类、自动拆装箱等。这些是你每天写代码都会用到的工具类。
互动话题:你在看框架源码(比如Spring、Android)的时候,有没有遇到过看不懂的内部类用法?欢迎在评论区分享,我帮你分析!
如果这篇文章对你有帮助,欢迎点赞、收藏、关注三连支持!这是【JavaSE全面教学】系列的第9篇,关注我看完整套教程 👇
参考资料
本文为【JavaSE全面教学】系列第9篇,持续更新中…
更多推荐



所有评论(0)