接上回之讲,谈到枚举单例模式没有定义readReslove()方法,但是在反序列化的时候返回的仍是同一个对象,下面我们来揭开它的什么面纱:
下载一个非常好用的反编译工具Jad(下载地址:https://varaneckas.com/jad/),解压后配置环境变量,就可以使用命令行调用了。
After thirty minutes.........
我电脑一直使用不了Jad反编译工具,又换了其他jd-gui也没给到我想要的效果,直接用javap -c进行反编译吧,代码如下:
Compiled from "Solution.java"
public final class net.zyy.leecode.Solution extends java.lang.Enum<net.zyy.leecode.Solution> {
public static final net.zyy.leecode.Solution INSTANCE;public static net.zyy.leecode.Solution[] values();
Code:
0: getstatic #1 // Field $VALUES:[Lnet/zyy/leecode/Solution;
3: invokevirtual #2 // Method "[Lnet/zyy/leecode/Solution;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[Lnet/zyy/leecode/Solution;"
9: areturnpublic static net.zyy.leecode.Solution valueOf(java.lang.String);
Code:
0: ldc #4 // class net/zyy/leecode/Solution
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class net/zyy/leecode/Solution
9: areturnpublic static net.zyy.leecode.Solution getInstance();
Code:
0: getstatic #7 // Field INSTANCE:Lnet/zyy/leecode/Solution;
3: areturnstatic {};
Code:
0: new #4 // class net/zyy/leecode/Solution
3: dup
4: ldc #8 // String INSTANCE
6: iconst_0
7: invokespecial #9 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #7 // Field INSTANCE:Lnet/zyy/leecode/Solution;
13: iconst_1
14: anewarray #4 // class net/zyy/leecode/Solution
17: dup
18: iconst_0
19: getstatic #7 // Field INSTANCE:Lnet/zyy/leecode/Solution;
22: aastore
23: putstatic #1 // Field $VALUES:[Lnet/zyy/leecode/Solution;
26: return
}
原来,枚举式单例模式在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例模式的实现,那为什么序列化破坏不了呢,我们回到JDK源码,还是ObjectInputStream的readObject0()方法:
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
...
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
...
我们看到,又调用了readEnum()方法,我们接下来来看readEnum()方法的实现:
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
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;
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;
}
我们发现,枚举类型其实通过类名和类对象找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。那么反射能否破坏呢?我们来看一段代码:
class Test {
public static void main(String[] args) {
Class<?> clazz = Solution.class;
try {
Object o = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果如下图所示:
居然没找到无参的构造方法,接下来我们去看Enum类的源码,因为我们在javap反编译的时候看到此时的类继承了java.lang.Enum类:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
我们发现父类只有一个有参构造方法,那我们利用反射来调用有参构造方法,代码如下:
class Test {
public static void main(String[] args) {
Class<?> clazz = Solution.class;
try {
Constructor<?> c = clazz.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
Object o = c.newInstance("zyy",21);
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果如下:
这时候错误已经非常明显了,不用利用反射来创建枚举类型,还是习惯的看看JDK源码,进入Constructor的newInstance()方法 :
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
从上述代码可以看出,方法里做了强制性的判断,如果是ENUM类型,直接抛出异常。当然,这种枚举单例模式也是Effective Java书中推荐的实现写法。
单例模式小结
最后我们来做一下总结,单例模式可以保证内存里只有一个实例,减少内存开销,单例模式看起来非常简单,实现起来也非常简单,但是在面试中是个高频考点,希望小伙伴们通过学习,遇到类似问题直接上源码分析,彰显技术深度,提高核心竞争力。