这9个单例被破坏的事故现场,你遇到过几个? 评论区见 (4)

至此,自认为最优雅的单例模式写法便大功告成了。但是,上面看似完美的单例写法还是值得斟酌的。在构造方法中抛出异常,显然不够优雅。那么有没有比静态内部类更优雅的单例写法呢?

6 更加优雅的枚举式单例写法问世

枚举式单例写法可以解决上面的问题。首先来看枚举式单例的标准写法,创建EnumSingleton类。

public enum EnumSingleton { INSTANCE; private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance(){ return INSTANCE; } }

然后看客户端测试代码。

public class EnumSingletonTest { public static void main(String[] args) { try { EnumSingleton instance1 = null; EnumSingleton instance2 = EnumSingleton.getInstance(); instance2.setData(new Object()); FileOutputStream fos = new FileOutputStream("EnumSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); instance1 = (EnumSingleton) ois.readObject(); ois.close(); System.out.println(instance1.getData()); System.out.println(instance2.getData()); System.out.println(instance1.getData() == instance2.getData()); }catch (Exception e){ e.printStackTrace(); } } }

最后得到运行结果,如下图所示。

file

我们没有对代码逻辑做任何处理,但运行结果和预期一样。那么枚举式单例写法如此神奇,它的神秘之处体现在哪里呢?下面通过分析源码来揭开它的神秘面纱。
首先下载一个非常好用的Java反编译工具Jad,在解压后配置好环境变量(这里不做详细介绍),就可以使用命令行调用了。找到工程所在的Class目录,复制EnumSingleton.class所在的路径,如下图所示。

file

然后切换到命令行,切换到工程所在的Class目录,输入命令jad并输入复制好的路径,在Class目录下会多出一个EnumSingleton.jad文件。打开EnumSingleton.jad文件,我们惊奇地发现有如下代码。

static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); }

原来,枚举式单例写法在静态代码块中就对INSTANCE进行了赋值,是饿汉式单例写法的实现。至此,我们还可以试想,序列化能否破坏枚举式单例写法呢?不妨再来看一下JDK源码,还是回到ObjectInputStream的readObject0()方法。

private Object readObject0(boolean unshared) throws IOException { ... case TC_ENUM: return checkResolve(readEnum(unshared)); ... }

我们看到,在readObject0()中调用了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; }

由上可知,枚举类型其实通过类名和类对象找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。那么反射是否能破坏枚举式单例写法的单例对象呢?来看客户端测试代码。

public static void main(String[] args) { try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(); c.newInstance(); }catch (Exception e){ e.printStackTrace(); } }

运行结果如下图所示。

file

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwjzzw.html