码迷,mamicode.com
首页 > 其他好文 > 详细

「源码分析」— 为什么枚举是单例模式的最佳方法

时间:2020-11-01 09:33:05      阅读:16      评论:0      收藏:0      [点我收藏+]

标签:rabl   bst   ica   检查   之一   chm   adb   odi   single   

1. 引言

枚举类型(enum type)是在 Java 1.5 中引入的一种新的引用类型,是由 Java 提供的一种语法糖,其本质是 int 值。关于其用法之一,便是单例模式,并且在《Effective Java》中有被提到:

单元素的枚举类型已经成为实现 Singleton 的最佳方法
本文便是探究 “为什么枚举是单例模式的最佳方法?”。

答案先写在前面,两个字:“简单”。

public

enum

EnumSingleton

{
    INSTANCE
;
}

Java 在我们使用它的同时,解决了危害单例模式安全性的两个问题:“反射***” “反序列化***”。

本文的内容概要如下:

  1. 回顾常见的单例模式方法;
  2. 探索 Java 中的枚举是如何防止两种***;
  3. 若不使用枚举,又如何防止两种***。

    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 的特殊处理,为枚举防止了反射、序列化***,我们可以直接使用枚举,不用担心单例模式的安全性,十分便利。但同时我们也需要记住反射***和序列化***的存在。

大家可以长按二维码,关注下~

你的订阅,是我写作路上最大的支持!
技术图片

「源码分析」— 为什么枚举是单例模式的最佳方法

标签:rabl   bst   ica   检查   之一   chm   adb   odi   single   

原文地址:https://blog.51cto.com/9167833/2544298

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!