将对象的创建交予工厂类,下方是创建Video的简单工厂方法,上层通过传入类名(字符串)就可以获取到想要的类。
public class Videofactory { static Video getVideo(String name) { Video video = null; if (name.equalsIgnoreCase("java")) { video = new Javavideo(); } else if (name.equalsIgnoreCase("python")) { video = new Pythonvideo(); } return video; } }
这种方式违背了开闭原则,即如果要增加带创建的类,就需要修改工厂方法。可以通过使用反射解决该问题,代码如下。
static Video getVideo2(Class c) { Video video = null; try { video = (Video) Class.forName(c.getName()).newInstance(); } catch (Exception e) { e.printStackTrace(); } return video; }
如图是简单工厂的UML。
与简单工厂最大的不同之处在于,对象的创建不再是factory统一管理,而是进一步细分多个子类,由具体的实现类负责创建实例对象。
如图示工厂模式的UML,这样做的好处在于进一步提升了了factory的可扩展性,每个子类负责创建一个实习对象,子类内部可以更加灵活的使用多种构造器创建。
抽象工厂关注产品族,工厂模式关注产品等级,简言之就是如果factory中要创建的不止一个对象时,就要采用抽象工厂模式,
下图是抽象工厂的UML,可以看到具体的factory负责创建多个不同的等级但属同一族的对象。
也是屏蔽底层具体实现细节,不过更加注重创建过程,而工厂模式只关注最终对象。
可以看到,builder中有很多创建过程函数,诸如setName、setPrice、setPpt。
优化方面:
这种写法有线程安全问题,即多个线程同时执行getInstance时有可能导致创建多个实例。
通过添加synchronized,可以解决该问题,但是每次都会加解锁导致性能下降。
一旦创建完毕,以后在第一次if判断时就会返回false,减少加解锁开销。即使初始化时竞争激烈,也会在synchronized处隔离。由于对象的创建会分三步走。
如果指令发生重排序,假设Thread-0在内层if中执行3后,Thread-1在外层if就会判断为false,这样就会得到一个未经初始化的对象,因此需要给单例对象添加上volatile关键字,一是保证可见性,二是防止指令重排序。
类在加载过程中,jvm会自动分配一把锁,用于确保类不会加载两次。如图所示,可以借用该锁,在加载过程中完成类的初始化。
下图是具体实现步骤,由于调用getInstance方法后,JVM会加载Innerclass,该过程中只有一个线程会初始化,这样就保证了单例,同时又是线程安全的。
顾名思义,最大的不同就是类一旦加载就完成单例的初始化。
破坏单例模式有如下思路。
序列化/反序列化攻击,下方代码通过反序列出一个新的实例对象,二者hash值不同。
HungrySingleton hungrySingleton = HungrySingleton.getHungrySingleton(); System.out.println(hungrySingleton); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("obj.txt")); objectOutputStream.writeObject(hungrySingleton); File file = new File("obj.txt"); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file)); HungrySingleton newSingleton = (HungrySingleton) objectInputStream.readObject(); System.out.println(newSingleton);
深入objectInputStream.readObject()的底层,第一个方法是isInstantiable()
/** * Returns true if represented class is serializable/externalizable and can * be instantiated by the serialization runtime--i.e., if it is * externalizable and defines a public no-arg constructor, or if it is * non-externalizable and its first non-serializable superclass defines an * accessible no-arg constructor. Otherwise, returns false. */ boolean isInstantiable() { requireInitialized(); return (cons != null); } obj = desc.isInstantiable() ? desc.newInstance() : null;
意思是如果该类实现了Serializable接口,并且可以运行时实例化,则返回true,进而生成新的实例。
第二个方法是hasReadResolveMethod(),通过源码分析,可知这个readResolveMethod其实就是readResolve方法,如果单例类有该方法,则返回true,并会invokeReadResolve。
/** * Returns true if represented class is serializable or externalizable and * defines a conformant readResolve method. Otherwise, returns false. */ boolean hasReadResolveMethod() { requireInitialized(); return (readResolveMethod != null); }
因此,只需要在单例类中添加如下方法,就可以避免反序列化对单例模式的破坏。不过过程中还是会新生成一个对象。
private final Object readResolve() { return HUNGRY_SINGLETON; }
反射攻击,通过反射调用其构造方法,进而生成新的实例。
Class cls = HungrySingleton.class; Constructor constructor = cls.getDeclaredConstructor(); constructor.setAccessible(true);//获取到private修饰的方法 HungrySingleton newInstance = (HungrySingleton) constructor.newInstance(); System.out.println(newInstance);
防御机制很简单,即在构造函数添加如下判断。
private HungrySingleton() { if (HUNGRY_SINGLETON!=null){ throw new RuntimeException("禁止反射调用构造方法"); } }
反射攻击针对饿汉式和懒汉式有所不同,饿汉式一旦加载完毕就会初始化,因此使用反射攻击一定会遇到上面的判断。而懒汉式,如果先getInstance后反射构造方法,则也会遇到上面判断。但反过来先执行构造函数后getInstance,就会形成两个实例。
public class LazySingleton { private static LazySingleton lazySingleton =null; private LazySingleton() { if (lazySingleton != null) { throw new RuntimeException("禁止反射构造函数"); } } public static LazySingleton getInstance() { if(lazySingleton==null){ lazySingleton=new LazySingleton(); } return lazySingleton; } }
改进的做法是在构造函数中末尾添加
lazySingleton=this;
推荐使用枚举类创建单例对象,因为枚举成员本身只能存在一个。
在反序列化中,ObjectInputStream.java的readEnum方法中,通过Enum.valueOf获取到该唯一实例。
String name = readString(false); Enum<?> en = Enum.valueOf((Class)cl, name);
通过源码得知,Enum的构造函数只有一个,如下
protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; }
即使获取到了构造器,也无法通过反射创建
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance(); System.out.println(newInstance); 报错: Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
通过JAD反编译工具查看EnumSingleton.class,可以看出同样也是私有构造方法,静态代码块加载。
这种方式通常在初始化时,将单例对象放入map容器,且后续不再改动,方便统一管理。缺点是修改容器内部是线程不安全的。
每个线程拥有唯一的ThreadLocalMap,在执行getInstance时,检查当前map有没有该对象,有则返回,无就执行initialValue方法,这种实现方式只能在一个线程中单例。
public class ThreadlocalSingleton { public static ThreadLocal<ThreadlocalSingleton> threadLocal = new ThreadLocal<ThreadlocalSingleton>() { @Override protected ThreadlocalSingleton initialValue() { return new ThreadlocalSingleton(); } }; private ThreadlocalSingleton() { } public ThreadlocalSingleton getInstance() { return threadLocal.get(); } }
适用场景
总之原型模式比直接new一个对象性能更高,该方式必须实现Cloneable接口,clone()方法,
public class Prototypetest implements Cloneable { private String name; private int age; @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } }
值得注意的是implements Cloneable仅仅是一个标志,即要被克隆,必须实现该接口,clone方法调用到Object下的clone方法,这是一个native方法,会拷贝一份实例的二进制内存,因此效率较高。
默认的clone的实现是浅克隆,即如果实例对象内部含有引用对象,那么克隆的只是引用地址,即内部的引用对象是同一个,无形中会给项目留下bug。建议实现深克隆,具体实现是在clone方法中,对于需要克隆的引用对象,在单独的执行clone方法,并通过set设值。注意,string虽然是引用对象,但是其由于是常量赋值,因此在修改时不会影响到另一份。下面是实现深克隆的例子。
@Override public Object clone() throws CloneNotSupportedException { Prototypetest prototypetest = (Prototypetest) super.clone(); Date clone = (Date) date.clone(); prototypetest.date = clone; return prototypetest; }
提交评论