Java面向对象补充_单例_匿名内部类_枚举
Java 面向对象补充:单例模式 + 匿名内部类 + 枚举
本章位置:第二阶段 Java 核心框架
前置知识:类和对象、继承、接口、多态、JavaBean
本章内容:单例模式、匿名内部类、枚举
下一篇:反射机制、内省机制、注解
学习目标:理解对象创建控制、匿名实现、枚举类型的设计思想,并能在后续框架和项目中正确识别这些写法。
第一部分:单例模式
一、什么是单例模式
单例模式:
Singleton Pattern
核心目标:
一个类在整个程序运行期间,只提供一个共享实例。
普通类:
User user1 =
new User();
User user2 =
new User();
可以创建:
多个对象
单例类希望做到:
整个程序主要只使用同一个对象
二、为什么需要单例
有些对象没必要重复创建。
例如:
配置管理器
日志管理器
缓存管理器
线程池管理对象
全局工具服务
某些框架核心组件
如果每次使用都:
new ConfigManager();
可能导致:
重复占用资源
状态不一致
配置重复加载
对象管理混乱
所以希望:
全局共享一个对象
三、单例模式的三个核心步骤
经典单例通常需要:
1. 构造方法私有化
2. 类内部保存唯一实例
3. 对外提供获取实例的方法
四、为什么构造方法必须 private
普通类:
public class ConfigManager {
public ConfigManager() {
}
}
外部可以:
new ConfigManager();
如果要限制外部创建对象:
private ConfigManager() {
}
这样:
外部不能直接 new
五、饿汉式单例
最简单:
public class ConfigManager {
private static final ConfigManager
INSTANCE =
new ConfigManager();
private ConfigManager() {
}
public static ConfigManager
getInstance() {
return INSTANCE;
}
}
使用:
ConfigManager manager1 =
ConfigManager
.getInstance();
ConfigManager manager2 =
ConfigManager
.getInstance();
六、验证是不是同一个对象
System.out.println(
manager1
== manager2
);
结果:
true
这里用:
==
比较对象引用。
说明:
两个变量指向同一个对象
七、为什么叫饿汉式
因为:
类加载时
对象就已经创建
不管后面用不用:
先创建好
所以形象叫:
饿汉式
八、饿汉式优点
代码简单
线程安全
实现清晰
不容易写错
如果对象创建成本不高,
饿汉式通常非常可靠。
九、饿汉式缺点
如果对象:
创建成本很高
占用大量资源
程序可能永远不会使用
类加载时就创建:
可能造成资源浪费
十、懒汉式单例
希望:
真正第一次使用时
才创建对象
例如:
public class ConfigManager {
private static ConfigManager
instance;
private ConfigManager() {
}
public static ConfigManager
getInstance() {
if (
instance == null
) {
instance =
new ConfigManager();
}
return instance;
}
}
十一、为什么叫懒汉式
因为:
一开始不创建
真正需要时再创建
所以叫:
懒汉式
十二、普通懒汉式的线程安全问题
假设两个线程同时执行:
if (
instance == null
) {
instance =
new ConfigManager();
}
线程 A:
看到 instance == null
线程 B:
也看到 instance == null
结果:
两个线程都 new
可能创建:
两个实例
因此普通懒汉式:
线程不安全
十三、同步方法解决线程安全
可以:
public static synchronized
ConfigManager getInstance() {
if (
instance == null
) {
instance =
new ConfigManager();
}
return instance;
}
这样:
同一时刻只有一个线程进入
十四、同步懒汉式的问题
每次调用:
getInstance()
都要进入同步方法。
即使实例已经创建完成:
仍然需要同步
会增加一定开销。
十五、双重检查锁
常见:
public class ConfigManager {
private static volatile
ConfigManager instance;
private ConfigManager() {
}
public static ConfigManager
getInstance() {
if (
instance == null
) {
synchronized (
ConfigManager.class
) {
if (
instance == null
) {
instance =
new ConfigManager();
}
}
}
return instance;
}
}
十六、为什么检查两次
第一次:
if (instance == null)
目的是:
实例已经创建后
不再进入 synchronized
第二次:
if (instance == null)
目的是:
多个线程第一次竞争锁时
防止重复创建
十七、为什么需要 volatile
private static volatile
ConfigManager instance;
volatile 在这里主要用于:
保证可见性
防止与对象创建相关的危险指令重排序
双重检查锁正确写法中:
volatile 很重要
十八、静态内部类单例
推荐掌握:
public class ConfigManager {
private ConfigManager() {
}
private static class Holder {
private static final
ConfigManager INSTANCE =
new ConfigManager();
}
public static ConfigManager
getInstance() {
return Holder.INSTANCE;
}
}
十九、静态内部类方式的优点
它同时具备:
懒加载
线程安全
代码相对简洁
因为:
Holder
只有第一次真正使用时才加载
JVM 的类加载机制保证初始化安全。
二十、枚举单例
还可以:
public enum AppManager {
INSTANCE;
}
使用:
AppManager manager =
AppManager.INSTANCE;
二十一、为什么枚举单例很强
枚举天生可以保证:
实例数量固定
并且对:
序列化
反射破坏
有更好的防护能力。
很多资料会推荐:
枚举实现单例
二十二、单例模式不是“全局变量”
单例:
一个类只维护一个主要实例
但并不意味着:
所有字段都应该是 public static
单例仍然应该:
封装状态
提供方法
控制访问
二十三、单例和 static 工具类区别
工具类:
Math.max(...)
通常:
没有对象状态
主要是静态方法
单例:
ConfigManager
.getInstance()
虽然只有一个对象,
但:
仍然是对象
可以实现接口
可以参与多态
可以维护实例状态
二十四、单例适合什么场景
比较适合:
全局共享配置
共享管理器
资源访问入口
无必要重复创建的服务对象
二十五、什么时候不要滥用单例
不要因为:
“单例看起来高级”
就把所有类都写成单例。
过度单例会造成:
全局状态太多
代码耦合
测试困难
生命周期难管理
后面学习 Spring 后:
对象是否单例
通常交给 Spring 容器管理
不会到处自己手写 Singleton。
二十六、Spring 中的单例和设计模式单例
这是以后容易混淆的地方。
经典 Singleton:
一个 ClassLoader 中
类自己控制唯一实例
Spring 默认 singleton Bean:
一个 Spring 容器中
同一个 Bean 名称通常对应一个实例
它们:
思想相似
实现机制不同
二十七、综合案例:配置管理器
public class ConfigManager {
private static final
ConfigManager INSTANCE =
new ConfigManager();
private final Properties
properties =
new Properties();
private ConfigManager() {
try (
InputStream input =
ConfigManager.class
.getClassLoader()
.getResourceAsStream(
"app.properties"
)
) {
if (
input == null
) {
throw new IllegalStateException(
"配置文件不存在"
);
}
properties.load(
input
);
} catch (
IOException e
) {
throw new RuntimeException(
"配置加载失败",
e
);
}
}
public static ConfigManager
getInstance() {
return INSTANCE;
}
public String get(
String key
) {
return properties
.getProperty(
key
);
}
}
二十八、使用配置单例
String appName =
ConfigManager
.getInstance()
.get(
"app.name"
);
这样:
配置只加载一次
二十九、单例常见错误
错误:
public class Singleton {
public Singleton() {
}
}
构造方法 public:
外部仍然能随便 new
三十、单例常见错误:实例不是 static
错误:
private Singleton instance;
要想通过类直接访问唯一实例,
通常:
private static Singleton instance;
三十一、单例常见错误:普通懒汉式忽略多线程
if (
instance == null
) {
instance =
new Singleton();
}
多线程下:
可能重复创建
三十二、单例必须掌握
需要掌握:
构造方法 private
static 唯一实例
getInstance()
饿汉式
懒汉式
线程安全问题
静态内部类
枚举单例
第二部分:匿名内部类
三十三、什么是内部类
一个类定义在:
另一个类内部
可以叫:
内部类
例如:
public class Outer {
class Inner {
}
}
三十四、匿名内部类是什么
匿名内部类:
没有显式类名、创建后通常直接使用的一次性类实现。
例如:
Runnable task =
new Runnable() {
@Override
public void run() {
System.out.println(
"任务执行"
);
}
};
这里:
没有写 class Xxx
但实际上创建了:
Runnable 的匿名实现类对象
三十五、匿名内部类常见场景
常见:
实现接口
继承抽象类
临时重写方法
事件监听
线程任务
回调
三十六、匿名实现接口
接口:
public interface Flyable {
void fly();
}
普通写法:
public class Bird
implements Flyable {
@Override
public void fly() {
System.out.println(
"鸟在飞"
);
}
}
然后:
Flyable flyable =
new Bird();
三十七、如果只使用一次
如果这个实现:
只会用一次
专门创建:
Bird.java
可能比较麻烦。
可以:
Flyable flyable =
new Flyable() {
@Override
public void fly() {
System.out.println(
"匿名对象在飞"
);
}
};
三十八、匿名内部类的结构
接口类型 变量 =
new 接口类型() {
@Override
public void 方法() {
}
};
注意:
不是在 new 接口本身
而是:
创建接口匿名实现类的对象
三十九、接口本身不能直接 new
错误:
new Flyable();
因为接口:
不能实例化
匿名内部类写法:
new Flyable() {
@Override
public void fly() {
}
};
实际上 Java 帮你创建:
一个没有显式名字的实现类
四十、匿名继承抽象类
抽象类:
public abstract class Animal {
public abstract
void sound();
}
匿名内部类:
Animal animal =
new Animal() {
@Override
public void sound() {
System.out.println(
"汪汪"
);
}
};
四十一、匿名继承普通类
普通类:
public class Person {
public void say() {
System.out.println(
"Hello"
);
}
}
可以:
Person person =
new Person() {
@Override
public void say() {
System.out.println(
"匿名子类"
);
}
};
四十二、匿名内部类 + 线程
Thread thread =
new Thread(
new Runnable() {
@Override
public void run() {
System.out.println(
"子线程执行"
);
}
}
);
thread.start();
这是 Java 8 以前非常常见的写法。
四十三、Lambda 可以替代很多匿名内部类
上面:
new Runnable() {
@Override
public void run() {
System.out.println(
"子线程执行"
);
}
}
可以简化:
() -> {
System.out.println(
"子线程执行"
);
}
所以:
new Thread(
() -> {
System.out.println(
"子线程执行"
);
}
).start();
四十四、Lambda 不能替代所有匿名内部类
Lambda 主要用于:
函数式接口
也就是:
只有一个抽象方法的接口
如果:
需要继承普通类
需要继承抽象类
接口有多个抽象方法
Lambda:
不能直接替代
四十五、匿名内部类实现多方法接口
例如:
public interface Handler {
void start();
void stop();
}
匿名内部类:
Handler handler =
new Handler() {
@Override
public void start() {
System.out.println(
"开始"
);
}
@Override
public void stop() {
System.out.println(
"停止"
);
}
};
这种情况:
不能直接使用 Lambda
因为有:
两个抽象方法
四十六、匿名内部类可以访问外部变量
例如:
String message =
"Hello";
Runnable task =
new Runnable() {
@Override
public void run() {
System.out.println(
message
);
}
};
四十七、局部变量必须有效 final
Java 8 以后:
不一定显式写 final
但被匿名内部类引用的局部变量通常需要:
实际上不再修改
例如:
String message =
"Hello";
后面不要:
message =
"World";
否则匿名内部类引用时会报错。
这种情况叫:
effectively final
有效 final
四十八、匿名内部类中的 this
匿名内部类里面的:
this
通常代表:
当前匿名内部类对象
不是外部类对象。
如果要访问外部类:
OuterClass.this
四十九、匿名内部类的优点
代码紧凑
一次性实现方便
不用额外创建类文件
适合简单回调
五十、匿名内部类的缺点
如果逻辑太长:
可读性会明显下降
例如匿名类里写:
几百行代码
就不合适。
此时应该:
单独创建实现类
五十一、什么时候适合匿名内部类
适合:
逻辑短
只使用一次
实现很简单
回调型代码
五十二、什么时候不适合
不适合:
逻辑复杂
需要复用
需要测试
需要多个地方使用
内部状态很多
五十三、综合案例:排序比较器
例如:
List<Student> students =
new ArrayList<>();
匿名 Comparator:
students.sort(
new Comparator<Student>() {
@Override
public int compare(
Student a,
Student b
) {
return b.getScore()
.compareTo(
a.getScore()
);
}
}
);
后面 Lambda 可以简化:
students.sort(
(
a,
b
) ->
b.getScore()
.compareTo(
a.getScore()
)
);
五十四、匿名内部类必须掌握
需要理解:
匿名实现接口
匿名继承抽象类
匿名继承普通类
一次性对象
this
effectively final
和 Lambda 的关系
第三部分:枚举
五十五、为什么需要枚举
假设订单状态:
1
待支付
2
已支付
3
已取消
代码中:
if (
status == 1
) {
}
问题:
1 到底是什么意思?
可以传 99 吗?
不同开发者会不会写错?
五十六、字符串状态也有问题
例如:
String status =
"PAID";
可能有人写:
paid
Payed
PAY
PAIDD
字符串没有:
类型约束
容易出错。
五十七、枚举是什么
枚举:
enum
用于表示:
一组固定、有限、明确的常量对象。
例如:
public enum OrderStatus {
PENDING,
PAID,
CANCELLED
}
五十八、使用枚举
OrderStatus status =
OrderStatus.PAID;
判断:
if (
status
== OrderStatus.PAID
) {
System.out.println(
"订单已支付"
);
}
五十九、为什么枚举比 int 常量更安全
如果方法:
public void updateStatus(
OrderStatus status
) {
}
调用:
updateStatus(
OrderStatus.PAID
);
不能随便:
updateStatus(100);
类型已经限制:
只能传 OrderStatus
六十、枚举本质上是类
枚举并不是简单:
常量语法糖
它本质上是一种特殊的类。
枚举可以拥有:
字段
构造方法
普通方法
抽象方法
实现接口
六十一、枚举常量本质上是对象
例如:
OrderStatus.PAID
可以理解:
OrderStatus 类型的一个固定对象
六十二、带字段的枚举
例如:
public enum OrderStatus {
PENDING(
0,
"待支付"
),
PAID(
1,
"已支付"
),
CANCELLED(
2,
"已取消"
);
private final int code;
private final String description;
OrderStatus(
int code,
String description
) {
this.code =
code;
this.description =
description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
}
六十三、枚举构造方法为什么不写 public
枚举构造方法:
不能让外部随便 new
枚举实例数量必须由:
枚举定义本身控制
所以构造方法不能是:
public
六十四、使用带字段枚举
OrderStatus status =
OrderStatus.PAID;
System.out.println(
status.getCode()
);
System.out.println(
status.getDescription()
);
结果:
1
已支付
六十五、values()
每个枚举都会自动提供:
values()
例如:
OrderStatus[] values =
OrderStatus
.values();
遍历:
for (
OrderStatus status :
OrderStatus.values()
) {
System.out.println(
status
);
}
六十六、valueOf()
OrderStatus status =
OrderStatus
.valueOf(
"PAID"
);
得到:
OrderStatus.PAID
六十七、valueOf() 大小写敏感
如果:
OrderStatus.valueOf(
"paid"
);
可能抛:
IllegalArgumentException
因为:
枚举常量名严格匹配
六十八、name()
OrderStatus.PAID
.name();
结果:
PAID
六十九、ordinal()
OrderStatus.PAID
.ordinal();
返回:
枚举常量定义顺序
例如:
PENDING
ordinal = 0
PAID
ordinal = 1
七十、不要把 ordinal 当数据库业务值
非常重要。
不推荐:
数据库保存 ordinal
因为以后枚举调整顺序:
PENDING,
CANCELLED,
PAID
ordinal 就会变化。
更安全:
自己定义稳定 code
例如:
PAID(
20,
"已支付"
)
七十一、枚举和 switch
可以:
switch (status) {
case PENDING ->
System.out.println(
"等待支付"
);
case PAID ->
System.out.println(
"已经支付"
);
case CANCELLED ->
System.out.println(
"已经取消"
);
}
非常清晰。
七十二、枚举实现接口
接口:
public interface Displayable {
String getDisplayName();
}
枚举:
public enum Gender
implements Displayable {
MALE(
"男"
),
FEMALE(
"女"
);
private final String name;
Gender(
String name
) {
this.name =
name;
}
@Override
public String getDisplayName() {
return name;
}
}
七十三、枚举方法
例如:
public boolean isFinished() {
return this
== PAID
|| this
== CANCELLED;
}
调用:
OrderStatus.PAID
.isFinished();
七十四、枚举可以覆盖方法
例如:
public enum Operation {
ADD {
@Override
public int calculate(
int a,
int b
) {
return a + b;
}
},
SUBTRACT {
@Override
public int calculate(
int a,
int b
) {
return a - b;
}
};
public abstract int calculate(
int a,
int b
);
}
七十五、枚举和常量类对比
普通常量:
public static final int
STATUS_PAID =
1;
枚举:
OrderStatus.PAID
枚举优点:
类型安全
可读性高
可以带字段
可以写方法
可遍历
可用于 switch
七十六、常见业务枚举
项目中经常:
订单状态
用户状态
审核状态
性别
角色类型
支付方式
消息类型
文件类型
启用/禁用状态
七十七、用户状态枚举
public enum UserStatus {
DISABLED(
0,
"禁用"
),
ENABLED(
1,
"启用"
);
private final int code;
private final String text;
UserStatus(
int code,
String text
) {
this.code =
code;
this.text =
text;
}
public int getCode() {
return code;
}
public String getText() {
return text;
}
}
七十八、根据 code 找枚举
可以:
public static UserStatus
fromCode(
int code
) {
for (
UserStatus status :
values()
) {
if (
status.code
== code
) {
return status;
}
}
throw new IllegalArgumentException(
"未知用户状态:"
+ code
);
}
七十九、调用 fromCode
UserStatus status =
UserStatus
.fromCode(
1
);
得到:
ENABLED
八十、枚举为什么适合状态机
例如订单:
待支付
↓
已支付
↓
已完成
或者:
待支付
↓
已取消
枚举可以结合:
状态
行为
转换规则
形成更清晰的业务模型。
八十一、枚举和数据库
数据库通常保存:
code
例如:
0
1
2
Java:
使用 enum
查询后:
code → enum
写入前:
enum → code
八十二、枚举和 JSON
SpringBoot 后面可能:
自动把枚举转换为字符串
例如:
{
"status": "PAID"
}
也可以通过配置或注解:
使用 code
后面再深入。
八十三、枚举常见错误:用魔法数字
不推荐:
if (
user.getStatus()
== 1
) {
}
更清晰:
if (
user.getStatus()
== UserStatus.ENABLED
) {
}
八十四、什么是魔法值
例如:
if (type == 3) {
}
这里:
3 是什么意思?
看代码的人不知道。
这种不解释含义的:
数字
字符串
常叫:
魔法值
Magic Value
枚举可以减少魔法值。
八十五、枚举常见错误:数据库直接保存 ordinal
不要依赖:
ordinal()
作为永久业务数据。
推荐:
自己定义 code
八十六、枚举常见错误:所有常量都用 enum
例如:
π
服务器端口
最大长度
这些只是:
单个常量
不一定需要 enum。
枚举更适合:
一组固定选项
八十七、枚举必须掌握
需要掌握:
enum 定义
枚举常量
字段
构造方法
Getter
values()
valueOf()
name()
ordinal()
switch
code 映射
第四部分:三个知识点综合理解
八十八、单例解决什么问题
对象创建数量
核心:
整个程序共享同一个实例
八十九、匿名内部类解决什么问题
临时创建一个实现
核心:
一次性实现接口或继承类
九十、枚举解决什么问题
固定值集合
核心:
把魔法数字 / 字符串
变成类型安全对象
九十一、三个知识点对比
| 知识点 | 解决的问题 | 典型场景 |
|---|---|---|
| 单例 | 控制对象数量 | 配置管理器 |
| 匿名内部类 | 临时实现 | Runnable、Comparator |
| 枚举 | 固定状态集合 | 订单状态、用户状态 |
九十二、综合案例:任务执行器
定义任务状态:
public enum TaskStatus {
WAITING,
RUNNING,
SUCCESS,
FAILED
}
定义执行接口:
public interface Task {
void execute();
}
匿名实现:
Task task =
new Task() {
@Override
public void execute() {
System.out.println(
"执行任务"
);
}
};
单例管理器:
public class TaskManager {
private static final
TaskManager INSTANCE =
new TaskManager();
private TaskManager() {
}
public static TaskManager
getInstance() {
return INSTANCE;
}
public void run(
Task task
) {
task.execute();
}
}
使用:
TaskManager
.getInstance()
.run(task);
这里同时使用:
单例
匿名内部类
枚举
九十三、IDEA 创建枚举
可以:
右键包
→ New
→ Java Class
然后选择:
Enum
或者创建后:
public enum OrderStatus {
}
九十四、IDEA 快速重写方法
在匿名内部类中:
Ctrl + O
可以:
Override Methods
选择需要重写的方法。
九十五、实现接口方法
快捷键:
Ctrl + I
用于:
Implement Methods
在匿名类或普通实现类中都很有用。
九十六、练习题 1:饿汉式单例
创建:
AppConfig
要求:
构造 private
唯一 static 实例
getInstance()
验证:
a == b
结果为:
true
九十七、练习题 2:懒汉式
自己实现:
普通懒汉式
然后思考:
为什么多线程不安全
九十八、练习题 3:静态内部类单例
使用:
Holder
完成单例。
九十九、练习题 4:匿名 Runnable
使用匿名内部类创建:
Runnable
线程输出:
正在执行任务
一百、练习题 5:匿名 Comparator
定义:
Student
字段:
name
score
使用匿名 Comparator:
按成绩降序
排序。
一百零一、练习题 6:订单状态枚举
创建:
OrderStatus
包含:
WAIT_PAY
PAID
FINISHED
CANCELLED
一百零二、练习题 7:带 code 的枚举
每个状态:
code
description
并实现:
fromCode()
一百零三、练习题 8:枚举 switch
根据:
OrderStatus
输出不同提示。
一百零四、练习题 9:单例配置管理器
使用上一章:
properties
创建一个:
ConfigManager
要求:
单例
配置只加载一次
提供 get(key)
一百零五、练习题 10:综合任务系统
要求:
TaskStatus 使用 enum
Task 使用 interface
任务实现使用匿名内部类
TaskManager 使用单例
一百零六、常见面试基础题
需要能够回答:
1. 什么是单例模式?
2. 饿汉式和懒汉式区别?
3. 懒汉式为什么有线程安全问题?
4. 双重检查为什么需要 volatile?
5. 什么是匿名内部类?
6. 匿名内部类和 Lambda 有什么关系?
7. 什么是 effectively final?
8. enum 和 static final 常量有什么区别?
9. values() 和 valueOf() 有什么作用?
10. 为什么不建议把 ordinal 存数据库?
一百零七、本章总结
这一篇补齐了三个非常常见的 Java 语言能力。
单例:
控制对象创建
匿名内部类:
快速提供一次性实现
枚举:
表示固定状态和固定选项
它们在后面都会继续出现。
例如:
线程
Comparator
监听器
Spring
MyBatis
状态设计
业务常量
设计模式
尤其枚举在真实项目里非常常见。
以后遇到:
用户状态
订单状态
审核状态
角色类型
支付方式
不要第一反应全部写:
int
String
而应该判断:
是否适合 enum
到这里,第二阶段第一项:
单元测试
配置文件
JavaBean
单例
匿名内部类
枚举
已经完整整理结束。
下一项进入:
反射机制
内省机制
注解
这三个知识点会直接帮助理解:
MyBatis
Spring
SpringBoot
为什么可以自动创建对象、读取属性、扫描注解、完成依赖注入。