《Effective Java》第三条:用私有构造器或者枚举类型强化Singleton属性
一、为什么单例模式使得测试困难?
原因:
1. 模拟框架(如 Mockito、EasyMock)创建模拟对象的本质是:动态生成目标类的子类或代理对象,并覆盖其方法以模拟特定行为。但这依赖于一个前提 ——能够创建目标类的新实例。
2. 单例模式严格禁止外部创建新实例。
3. 单元测试的核心原则 —— 隔离性、可重复性和可控性。
结果:
1. 无法模拟实现。比如,我们在单测时,想要绕开真实调用而测试代码逻辑,但是单例模式使得必须用其实例,这就绕不开真实调用,测试结果收到真是调用结果影响。而这个调用有可能包含收费、网络调用等问题。
2. 各个单测之间存在污染风险。因为单例模式的内部属性是唯一的公用的,这使得其他单测的使用痕迹保留了下来,比如可能某个单测要记录调用次数。
二、为什么公有域式静态工厂方法的单例模式更显式?(错误理解下的错误问题)
原因:
1. Singleton.INSTANCE只能表明这是一个实例,而体现不了这是个唯一实例。
2. Singleton。getInstance()命名是行业惯例,明眼人直接就知道是单例了。
追问:那我把INSTANCE属性名称改为onlyINSTANCE不是比静态工厂方法更简单直接?
1. 无法阻止 “对变量本身的误解”
变量是 “数据载体”,即使叫onlyINSTANCE,开发者看到public static final修饰时,可能会产生额外疑问:
- “这个
final变量是引用不可变,那它指向的对象内部状态能改吗?”(虽然单例通常设计为不可变,但变量名无法传递这一点); - “既然是公开变量,我能直接通过
Singleton.onlyINSTANCE调用方法,那为什么不设计成工具类(全静态方法)?”(变量名无法区分 “单例实例” 和 “静态工具类” 的差异)。
而静态工厂方法getInstance()是 “行为入口”,天然传递 “获取实例” 的动作 —— 它暗示 “这个类是有状态的实例类,不是无状态的工具类”,避免了对 “变量性质” 的误解。
ps:家里有门,会暗示此空间外人不能太随便。没门直接取实例成啥了,还让不让人安静上厕所?既然你们家厕所能随便上?为啥不把你家客厅改成洗手台,厕所改成公厕?你直接化身为厕所管理员。
2. 无法引导 “正确的使用习惯”
单例的核心是 “唯一实例的访问入口”,而直接暴露变量,本质是 “把实例当工具类的静态变量用”,无法引导开发者形成 “通过统一入口获取实例” 的习惯。
举个例子:
- 直接暴露变量:开发者可能在代码中到处写
Singleton.onlyINSTANCE.doSomething(),代码分散; - 静态工厂方法:开发者会统一写
Singleton.getInstance().doSomething(),后续如果需要修改实例获取逻辑(比如加缓存、加权限校验),只需改getInstance()方法内部,无需修改所有调用处。
onlyINSTANCE虽然名字带 “唯一”,但没有 “入口感”—— 它只是一个可直接访问的变量,而非 “获取实例的唯一渠道”,这会导致后续维护的灵活性丧失。
ps:就好像去公司得走正门,不能翻墙翻窗户,不然到时候安检工作不好做。非法移民了!!!
3. 无法覆盖 “语义的完整性”
单例的 API 需要传递的语义是:“此类有且仅有一个实例,且只能通过指定方式获取”。onlyINSTANCE只覆盖了 “唯一”,却没覆盖 “只能通过指定方式获取”—— 开发者可能会想:“既然有onlyINSTANCE,那有没有可能通过其他方式创建实例?”(比如反射、序列化)。
而静态工厂方法getInstance()通过 “方法行为” 传递了完整语义:
- “get” 这个动词,暗示 “实例需要被获取,而不是直接存在”;
- 方法是获取实例的唯一公开入口,结合私有构造器,开发者会自然理解 “没有其他创建实例的方式”。
ps:家里有门就暗示,你只能从门进,从窗户不行。
总结:有门(有getInstance()方法)就是暗示:此地不随便、只能从门进、之后好收费。
二改、对于二中理解、问题及回答的纠正
1. 公有域方法和静态工厂方法是实现单例模式的两种方法。
公有域方法:
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 私有构造器防止外部实例化
}
public void doSomething() {
System.out.println("Singleton using public field");
}
}
静态工厂方法:
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 私有构造器防止外部实例化
}
public static Singleton getInstance() {
return INSTANCE;
}
public void doSomething() {
System.out.println("Singleton using static factory method");
}
}
| 你之前的理解 / 比喻 | 需修正的点 | 正确逻辑 |
|---|---|---|
| “公有域式静态工厂方法更显式” | 混淆了两种独立实现的优势归属 | 「公有域方法」靠final+ 私有构造器 “语法硬约束” 显式;「静态工厂」靠 “命名惯例” 显式,显式性更弱 |
| “直接暴露变量没有入口感” | 误解了公有域的 “入口感” 本质 | 公有域的 “入口” 是 “唯一变量”(静态入口),静态工厂的 “入口” 是 “方法”(动态入口),二者都有入口感 |
| “onlyINSTANCE 能更直接传递唯一” | 忽略了 “语义传递靠语法,而非变量名”,onlyINSTANCE 是冗余修饰 | 公有域的 “唯一” 靠final+ 私有构造器传递,INSTANCE已足够,only多余且破坏惯例 |
| 比喻 “家里有门对应静态工厂” | 比喻正确,但需补充 “公有域对应‘唯一水龙头’,不是‘没门’” |
公有域 =“唯一水龙头(只能从这接)”;静态工厂 =“正门(可加安检)”,二者都是 “有约束的入口” |
追问:谁说的语义传递靠语法,而非变量名?
确实,很多实现或者使用你不能单靠使用者自觉,而是在语法层面强制更有效。
三、静态工厂方法的优势
1. 可灵活调整 “单例语义”,不破坏 API 兼容性。
静态工厂方法将 “实例创建逻辑” 封装在方法内部,若未来需要修改 “是否返回单例”(如改为池化实例、按需创建),只需修改方法实现,无需改变对外暴露的 API(方法名、参数、返回值),现有调用代码无需调整。
// 初始版本:静态工厂实现单例
public class Config {
private static final Config INSTANCE = new Config();
private Config() {}
// 对外API:获取实例
public static Config getInstance() {
return INSTANCE; // 初始返回固定单例
}
}
// 后续需求变更:根据环境返回不同实例(开发/生产)
// 仅修改getInstance()内部,API不变,调用者无需改动
public class Config {
private static Config devInstance;
private static Config prodInstance;
private final String env;
private Config(String env) { this.env = env; }
// 对外API不变,内部逻辑调整
public static Config getInstance() {
String env = System.getenv("ENV");
if ("dev".equals(env)) {
if (devInstance == null) devInstance = new Config("dev");
return devInstance;
} else {
if (prodInstance == null) prodInstance = new Config("prod");
return prodInstance;
}
}
}
2. 泛型工厂,简化泛型类的使用,避免冗余类型转换
// 1. 定义泛型单例工厂(核心:通过Class对象创建实例,缓存单例)
public class SingletonFactory {
// 缓存:key=类型Class,value=该类型的单例实例
private static final Map<Class<?>, Object> SINGLETON_CACHE = new ConcurrentHashMap<>();
// 泛型静态工厂方法:创建指定类型的单例
@SuppressWarnings("unchecked")
public static <T> T getSingleton(Class<T> type) {
// 先查缓存,没有则创建并缓存
return (T) SINGLETON_CACHE.computeIfAbsent(type, clazz -> {
try {
// 通过私有构造器创建实例(反射)
return clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("创建单例失败:" + type.getName(), e);
}
});
}
}
// 2. 定义具体的单例类(私有构造器)
public class UserCache {
private UserCache() {} // 私有构造器,确保只能通过工厂创建
public void cacheUser(String userId) { /* 逻辑 */ }
}
public class OrderCache {
private OrderCache() {} // 私有构造器
public void cacheOrder(String orderId) { /* 逻辑 */ }
}
// 3. 客户端使用:用同一个工厂获取不同类型的单例
public class Client {
public static void main(String[] args) {
// 获取UserCache单例
UserCache userCache = SingletonFactory.getSingleton(UserCache.class);
// 获取OrderCache单例
OrderCache orderCache = SingletonFactory.getSingleton(OrderCache.class);
// 验证唯一性
UserCache userCache2 = SingletonFactory.getSingleton(UserCache.class);
System.out.println(userCache == userCache2); // true(单例)
}
}
- 优势:一个
SingletonFactory搞定所有单例类的创建,无需为UserCache、OrderCache分别写getInstance()方法; - 公有域的局限:若用公有域,每个单例类都要写
public static final Xxx INSTANCE = new Xxx(),代码冗余,且无法复用缓存、异常处理等逻辑。
3. 可作为方法引用,适配函数式接口
4. 单例与序列化的冲突本质
1.先明确:序列化 / 反序列化的基本逻辑
序列化(Serializable)的核心目的是:将对象的状态(实例域的值)转换成字节流,以便存储(如文件)或传输(如网络);反序列化则是将字节流恢复成原对象的副本。
但 Java 的默认反序列化机制有个关键特性:反序列化时,不会调用类的构造器(即使是私有构造器),而是直接通过字节流 “复刻” 一个新对象—— 这对单例是致命的:单例的核心是 “全局唯一实例”,但反序列化会 “偷偷创建新实例”,导致 “假冒的单例”。
import java.io.*;
// 仅加了Serializable,未处理transient和readResolve
public class Elvis implements Serializable {
// 单例实例
public static final Elvis INSTANCE = new Elvis();
// 实例域(非transient)
private String song = "Love Me Tender";
private Elvis() {} // 私有构造器,防止外部实例化
// 业务方法
public void sing() {
System.out.println("Sing: " + song);
}
// 测试:序列化+反序列化后是否还是原实例
public static void main(String[] args) throws Exception {
// 1. 序列化:将单例写入文件
Elvis original = Elvis.INSTANCE;
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("elvis.obj"));
oos.writeObject(original);
oos.close();
// 2. 反序列化:从文件读取对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("elvis.obj"));
Elvis deserialized = (Elvis) ois.readObject();
ois.close();
// 3. 验证是否为同一实例:结果为false!反序列化创建了新实例
System.out.println("Original == Deserialized? " + (original == deserialized));
// 4. 验证实例域:新实例的song字段也被恢复(默认序列化会保存实例域)
deserialized.sing(); // 输出:Sing: Love Me Tender
}
}

2. transient + readResolve
详解等到第89条
四、枚举单例

更多推荐



所有评论(0)