原理:单例模式的陷阱
作者:强官涛   类型:Java开发    类别:原理   日期:2017-12-08    阅读:1688 次   消耗积分:0 分

package com.woniuxy.sd.service;
public class Test {
private static Test t = new Test();
private Test(){
}
public static Test getInstance(){if(null == t)t = new Test();
return t;}}



上面是我们常见的一种单例模式写法,单例模式为保证对象的唯一性,私有化构造函数为保证其除自己之外不能被实例化,static的实例在压栈时将压往方法区静态空间,再由于静态空间唯一性来保证实例唯一,最后由一个公有方法返回该对象实例,那么在上述代码中只需要调用公有返回方法则可得到该实例,同时也确保了实例唯一。


Test.getInstance();


对于这样的方法,无论调用多少次得到的结果都是一样:


System.out.println(Test.getInstance());
System.out.println(Test.getInstance());


执行后得到:


com.woniuxy.sd.service.Test@9cab16
com.woniuxy.sd.service.Test@9cab16


那么,只要构造函数不能被调用,看来实例的确是唯一的,而构造函数本身为private的,的确不能被调用,看来这个单例模式是没有问题的。

但实际上这个单例模式有很大的问题,它并不能保证对象的唯一性,用户可以直接通过AccessibleObject.setAccessible方法来调问其私有构造函数。

javaAPI中对java.lang.reflect.AccessibleObject有如下描述

 AccessibleObject 类是 Field、Method 和 Constructor 对象的基类。它提供了将反射的对象标记为在使用时取消默认 Java 语言访问控制检查的能力。对于公共成员、默认(打包)访问成员、受保护成员和私有成员,在分别使用 Field、Method 或 Constructor 对象来设置或获得字段、调用方法,或者创建和初始化类的新实例的时候,会执行访问检查。在反射对象中设置 accessible 标志允许具有足够特权的复杂应用程序(比如 Java Object Serialization 或其他持久性机制)以某种通常禁止使用的方式来操作对象。

而对其setAccessible方法有两次重载,描述如下

static void

setAccessible(AccessibleObject[] array, boolean flag)
使用单一安全性检查(为了提高效率)为一组对象设置 accessible 标志的便捷方法。

void

setAccessible(boolean flag)
将此对象的 accessible 标志设置为指示的布尔值。

对第一种方法,要求给出一个AccessibleObject数组,和一个布尔值,当布尔值为true的时候表示当前不检查访问修饰符,而第二种方法是一个非静态的方法,在看AccessibleObject的构造函数

构造方法摘要
protected

AccessibleObject()
构造方法:仅供 Java 虚拟机使用。

那么很显然,想直接通过new AccessibleObject的方法得到AccessibleObject是不可行的,可以选择通过静态方法或者子类得到实例,但很遗憾的是该类没有返回该类实例的静态方法,对于AccessibleObject来说他拥有如下子类。

直接已知子类:Constructor, Field, Method

Constructor这个子类在API中描述如下

Constructor 提供关于类的单个构造方法的信息以及对它的访问权限。

Constructor 允许在将实参与带有基础构造方法的形参的 newInstance() 匹配时进行扩展转换,但是如果发生收缩转换,则抛出IllegalArgumentException同样该类没有返回实例的方法,没有可用的构造函数,但该类可以通过Class.getConstructor和Class.getDeclaredConstructors()来返回实例。

Constructor<T>

getConstructor(Class... parameterTypes)
返回一个Constructor对象,它反映此Class对象所表示的类的指定公共构造方法。

Constructor<T>

getDeclaredConstructor(Class... parameterTypes)
返回一个Constructor对象,该对象反映此Class对象所表示的类或接口的指定构造方法。

Constructor[]

getDeclaredConstructors()
返回Constructor 对象的一个数组,这些对象反映此Class对象表示的类声明的所有构造方法。

可变参数Class... parameterTypes表示构造函数的参数,第一种方法将返回公共构造函数,而第二种和第三种将返回全部构造函数

因此通过上述描述,可以完成调用私有构造函数

Class clzz = Class.forName("com.woniuxy.sd.service.Test");
Constructor[] c = clzz.getDeclaredConstructors();
AccessibleObject.setAccessible(c,true);
System.out.println(c[0].newInstance());
System.out.println(c[0].newInstance());

完成上述代码执行后你会发现原本的单例不在单例,它不在保证实例唯一

com.woniuxy.sd.service.Test@9cab16
com.woniuxy.sd.service.Test@1a46e30

为了解决这个问题,因此我们可以考虑在Test的构造函数上加上一个判断,当第二次实例则抛出一个自定义异常,为什么在构造函数上加判断?因为无论通过反射还是直接new都离不开对构造函数的调用。

private Test() throws InstanceException {
i++;
System.out.println(i);
if(i >= 2)
throw new InstanceException();
}

蜗牛学院,只为成就更好的你!这样创新的模式,值得你的选择!

还在等什么,赶快关注蜗牛学院官方微信,加入到蜗牛学院的大家庭中来吧!

20181009_153045_341.jpg

版权所有,转载本站文章请注明出处:蜗牛学院在线课堂, http://www.woniuxy.com/note/5
上一篇: 实验:JS+定时器实现在线时钟
下一篇: 实验:设计模式五大原则(1):单一职责原则
提示:登录后添加有效评论可享受积分哦!