「源码分析」— 为什么枚举是单例模式的最佳方法
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了「源码分析」— 为什么枚举是单例模式的最佳方法相关的知识,希望对你有一定的参考价值。
1. 引言
枚举类型(enum type)是在 Java 1.5 中引入的一种新的引用类型,是由 Java 提供的一种语法糖,其本质是 int 值。关于其用法之一,便是单例模式,并且在《Effective Java》中有被提到:
单元素的枚举类型已经成为实现 Singleton 的最佳方法
本文便是探究 “为什么枚举是单例模式的最佳方法?”。
答案先写在前面,两个字:“简单”。
public
enum
EnumSingleton
{
INSTANCE
;
}
Java 在我们使用它的同时,解决了危害单例模式安全性的两个问题:“反射***” 和 “反序列化***”。
本文的内容概要如下:
- 回顾常见的单例模式方法;
- 探索 Java 中的枚举是如何防止两种***;
- 若不使用枚举,又如何防止两种***。
2. 常见单例模式方法
本小节将回顾下常见的单例模式方法,熟悉的同学可以直接跳过这节。
(1)懒汉式
特点:懒加载,线程不安全
public
class
Singleton
{
private
static
Singleton
instance
;
private
Singleton
()
{}
public
static
Singleton
getInstance
()
{
if
(
instance
==
null
)
{
instance
=
new
Singleton
();
}
return
instance
;
}
}
(2)饿汉式
特点:提前加载,线程安全
public
class
Singleton
{
private
static
Singleton
instance
=
new
Singleton
();
private
Singleton
()
{}
public
static
Singleton
getInstance
()
{
return
instance
;
}
}
(3)双重校验锁
特点:懒加载,线程安全
public
class
Singleton
{
private
volatile
static
Singleton
instance
;
private
Singleton
()
{}
public
static
Singleton
getInstance
()
{
if
(
instance
==
null
)
{
synchronized
(
Singleton
.
class
)
{
if
(
instance
==
null
)
{
instance
=
new
Singleton
();
}
}
}
return
instance
;
}
}
(4)静态内部类
特点:懒加载,线程安全
public
class
Singleton
{
private
static
class
SingletonHolder
{
private
static
final
Singleton
INSTANCE
=
new
Singleton
();
}
private
Singleton
()
{}
public
static
final
Singleton
getInstance
()
{
return
SingletonHolder
.
INSTANCE
;
}
}
3. 防止反射***
从第 2 节中列举的常用单例模式方法,可看出这些方法具有共同点之一是私有的构造函数。这是为了防止在该类的外部直接调用构建函数创建对象了。但是该做法却无法防御反射***:
public
class
ReflectAttack
{
public
static
void
main
(
String
[]
args
)
throws
Exception
{
Singleton
instance
=
Singleton
.
getInstance
();
// 获取无参的构造函数
Constructor
<
Singleton
>
constructor
=
Singleton
.
class
.
getDeclaredConstructor
();
// 使用构造函数创建对象
constructor
.
setAccessible
(
true
);
Singleton
reflectInstance
=
constructor
.
newInstance
();
System
.
out
.
println
(
instance
==
reflectInstance
);
}
}
// output:
// false
下面我们反射***枚举类型:
public
class
ReflectAttack
{
public
static
void
main
(
String
[]
args
)
throws
Exception
{
EnumSingleton
instance
=
EnumSingleton
.
INSTANCE
;
// 获取无参的构造函数
Constructor
<
EnumSingleton
>
constructor
=
EnumSingleton
.
class
.
getDeclaredConstructor
();
// 使用构造函数创建对象
constructor
.
setAccessible
(
true
);
EnumSingleton
reflectInstance
=
constructor
.
newInstance
();
System
.
out
.
println
(
instance
==
reflectInstance
);
}
}
// output:
// Exception in thread "main" java.lang.NoSuchMethodException: com.chaycao.java.EnumSingleton.<init>()
// at java.lang.Class.getConstructor0(Class.java:3082)
// at java.lang.Class.getDeclaredConstructor(Class.java:2178)
// at com.chaycao.java.ReflectAttack.main(ReflectAttack.java:14)
报了 NoSuchMethodException 异常,是由于 EnumSingleton 中没有无参构造器,那枚举类中的构造函数是怎么样的?
Java 生成的枚举类都会继承 Enum 抽象类,其只有一个构造函数:
public
abstract
class
Enum
<
E
extends
Enum
<
E
>>
implements
Comparable
<
E
>,
Serializable
{
// name: 常量的名称
// ordinal: 常量的序号(枚举声明中的位置,从0开始递增)
// 若以 EnumSingleton 的 INSTANCE 常量为例:
// name = “INSTANCE”;ordinal = 0
protected
Enum
(
String
name
,
int
ordinal
)
{
this
.
name
=
name
;
this
.
ordinal
=
ordinal
;
}
}
那我们修改下 getDeclaredConstructor方法的参数,重新获取构造函数试下:
public
class
ReflectAttack
{
public
static
void
main
(
String
[]
args
)
throws
Exception
{
EnumSingleton
instance
=
EnumSingleton
.
INSTANCE
;
Constructor
<
EnumSingleton
>
constructor
=
EnumSingleton
.
class
.
getDeclaredConstructor
(
String
.
class
,
int
.
class
);
constructor
.
setAccessible
(
true
);
EnumSingleton
reflectInstance
=
constructor
.
newInstance
(
"REFLECT_INSTANCE"
,
1
);
System
.
out
.
println
(
instance
==
reflectInstance
);
}
}
// output:
// Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
// at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
// at com.chaycao.java.ReflectAttack.main(ReflectAttack.java:16)
这次虽然成功获取到了构造函数,但是仍然报错,并提示我们不能反射创建枚举对象。
错误位于 Constructor的 newInstance方法,第 417 行,代码如下:
if
((
clazz
.
getModifiers
()
&
Modifier
.
ENUM
)
!=
0
)
throw
new
IllegalArgumentException
(
"Cannot reflectively create enum objects"
);
如果该类是 ENUM 类型,则会抛出 IllegalArgumentException异常,便也阻止了反射***。
4. 防止反序列化***
下面是对于常用方法的反序列化***:
public
class
DeserializeAttack
{
public
static
void
main
(
String
[]
args
)
throws
Exception
{
Singleton
instance
=
Singleton
.
getInstance
();
byte
[]
bytes
=
serialize
(
instance
);
Object
deserializeInstance
=
deserialize
(
bytes
);
System
.
out
.
println
(
instance
==
deserializeInstance
);
}
private
static
byte
[]
serialize
(
Object
object
)
throws
Exception
{
ByteArrayOutputStream
baos
=
new
ByteArrayOutputStream
();
ObjectOutputStream
oos
=
new
ObjectOutputStream
(
baos
);
oos
.
writeObject
(
object
);
byte
[]
bytes
=
baos
.
toByteArray
();
return
bytes
;
}
private
static
Object
deserialize
(
byte
[]
bytes
)
throws
Exception
{
ByteArrayInputStream
bais
=
new
ByteArrayInputStream
(
bytes
);
ObjectInputStream
ois
=
new
ObjectInputStream
(
bais
);
return
ois
.
readObject
();
}
}
// output:
// false
无法阻止反序列***,可以成功创建出两个对象。我们改成枚举类型试下:
public
class
DeserializeAttack
{
public
static
void
main
(
String
[]
args
)
throws
Exception
{
EnumSingleton
instance
=
EnumSingleton
.
INSTANCE
;
byte
[]
bytes
=
serialize
(
instance
);
Object
deserializeInstance
=
deserialize
(
bytes
);
System
.
out
.
println
(
instance
==
deserializeInstance
);
}
//....
}
// true
反序列得到的仍是同样的对象,这是为什么,下面深入 ObjectOutputStream 的序列化方法看下 Enum 类型的序列化内容,顺着 writeobject方法找到 writeObject0方法。
// ObjectOutputStream > writeobject0()
if
(
obj
instanceof
String
)
{
writeString
((
String
)
obj
,
unshared
);
}
else
if
(
cl
.
isArray
())
{
writeArray
(
obj
,
desc
,
unshared
);
}
else
if
(
obj
instanceof
Enum
)
{
writeEnum
((
Enum
<?>)
obj
,
desc
,
unshared
);
}
对于 Enum 类型将执行专门的 writeEnum方法进行序列化,该方法内容如下:
private
void
writeEnum
(
Enum
<?>
en
,
ObjectStreamClass
desc
,
boolean
unshared
)
throws
IOException
{
// 1. ENUM类型标志(常量):“126”
bout
.
writeByte
(
TC_ENUM
);
ObjectStreamClass
sdesc
=
desc
.
getSuperDesc
();
// 2. 完整类名:“com.chaycao.java.EnumSingleton: static final long serialVersionUID = 0L;”
writeClassDesc
((
sdesc
.
forClass
()
==
Enum
.
class
)
?
desc
:
sdesc
,
false
);
handles
.
assign
(
unshared
?
null
:
en
);
// 3. Enum对象的名称:“INSTANCE”
writeString
(
en
.
name
(),
false
);
}
从上述代码已经可以看出 EnumSingleton.INSTANCE 的反序列化内容。
接着我们再来观察 Enum 类型的反序列化, ObjectInputStream 与 ObjectOutputStream 类似,对于 Enum 类型也使用专用的 readEnum 方法:
private
Enum
<?>
readEnum
(
boolean
unshared
)
throws
IOException
{
// 1. 检查标志位
if
(
bin
.
readByte
()
!=
TC_ENUM
)
{
throw
new
InternalError
();
}
// 2. 检查类名是否是Enum类型
ObjectStreamClass
desc
=
readClassDesc
(
false
);
if
(!
desc
.
isEnum
())
{
throw
new
InvalidClassException
(
"non-enum class: "
+
desc
);
}
int
enumHandle
=
handles
.
assign
(
unshared
?
unsharedMarker
:
null
);
ClassNotFoundException
resolveEx
=
desc
.
getResolveException
();
if
(
resolveEx
!=
null
)
{
handles
.
markException
(
enumHandle
,
resolveEx
);
}
String
name
=
readString
(
false
);
Enum
<?>
result
=
null
;
// 3. 加载类,并使用类的valueOf方法获取Enum对象
Class
<?>
cl
=
desc
.
forClass
();
if
(
cl
!=
null
)
{
try
{
@SuppressWarnings
(
"unchecked"
)
Enum
<?>
en
=
Enum
.
valueOf
((
Class
)
cl
,
name
);
result
=
en
;
}
catch
(
IllegalArgumentException
ex
)
{
throw
(
IOException
)
new
InvalidObjectException
(
"enum constant "
+
name
+
" does not exist in "
+
cl
).
initCause
(
ex
);
}
if
(!
unshared
)
{
handles
.
setObject
(
enumHandle
,
result
);
}
}
handles
.
finish
(
enumHandle
);
passHandle
=
enumHandle
;
return
result
;
}
其过程对应了之前的序列化过程,而其中最重要的便是 Enum.valueOf方法:
public
static
<
T
extends
Enum
<
T
>>
T valueOf
(
Class
<
T
>
enumType
,
String
name
)
{
// name = "INSTANCE"
// 根据名称查找
T result
=
enumType
.
enumConstantDirectory
().
get
(
name
);
if
(
result
!=
null
)
return
result
;
if
(
name
==
null
)
throw
new
NullPointerException
(
"Name is null"
);
// 没有找到,抛出异常
throw
new
IllegalArgumentException
(
"No enum constant "
+
enumType
.
getCanonicalName
()
+
"."
+
name
);
}
根据名称查找对象,再返回,所以仍会返回 EnumSingleton中的 INSTANCE,不会存在反序列化的危险。
综上所述,可知枚举类型在 Java 中天生就不惧怕反射和反序列化的***,这是由 Java 自身提供的逻辑保证。那第 2 节中所提及的单例模式方法,是否也有办法能防止反射和反序列***?
5.非枚举的防守方法
本节以懒汉式为例,其他单例模式方法同样适用。
(1)防止反射
增加一个标志变量,在构造函数中检查是否已被调用过,若已被调用过,将抛出异常,保证构造函数只被调用一次:
public
class
Singleton
{
private
static
Singleton
instance
;
private
static
boolean
isInstance
=
false
;
private
Singleton
()
{
synchronized
(
Singleton
.
class
)
{
if
(!
isInstance
)
{
isInstance
=
true
;
}
else
{
throw
new
RuntimeException
(
"单例模式受到反射***!已成功阻止!"
);
}
}
}
public
static
Singleton
getInstance
()
{
if
(
instance
==
null
)
{
instance
=
new
Singleton
();
}
return
instance
;
}
}
(2)防止序列化
增加一个 readResolve 方法并返回 instance 对象。当 ObjectInputStream类反序列化时,如果对象存在 readResolve 方法,则会调用该方法返回对象。
public
class
Singleton
implements
Serializable
{
private
static
Singleton
instance
;
private
Singleton
()
{}
public
static
Singleton
getInstance
()
{
if
(
instance
==
null
)
{
instance
=
new
Singleton
();
}
return
instance
;
}
private
Object
readResolve
()
{
return
instance
;
}
}
6. 小结
由于 Java 的特殊处理,为枚举防止了反射、序列化***,我们可以直接使用枚举,不用担心单例模式的安全性,十分便利。但同时我们也需要记住反射***和序列化***的存在。
大家可以长按二维码,关注下~
你的订阅,是我写作路上最大的支持!
以上是关于「源码分析」— 为什么枚举是单例模式的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章