创建型

由 笔尖 发布

工厂

简单工厂

将对象的创建交予工厂类,下方是创建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。

image-20200613175655723

工厂模式

与简单工厂最大的不同之处在于,对象的创建不再是factory统一管理,而是进一步细分多个子类,由具体的实现类负责创建实例对象。

如图示工厂模式的UML,这样做的好处在于进一步提升了了factory的可扩展性,每个子类负责创建一个实习对象,子类内部可以更加灵活的使用多种构造器创建。

image-20200613180046792

抽象工厂

抽象工厂关注产品族,工厂模式关注产品等级,简言之就是如果factory中要创建的不止一个对象时,就要采用抽象工厂模式,

下图是抽象工厂的UML,可以看到具体的factory负责创建多个不同的等级但属同一族的对象。

image-20200613200426192

建造者

建造者模式

也是屏蔽底层具体实现细节,不过更加注重创建过程,而工厂模式只关注最终对象。

可以看到,builder中有很多创建过程函数,诸如setName、setPrice、setPpt。

image-20200613204224028

单例模式

  • 内存中只有一个实例,减少开销
  • 控制全局访问点,严格控制访问
  • 没有接口,难以扩展

优化方面:

  • 私有构造器
  • 线程安全
  • 延迟加载
  • 序列化和反序列化
  • 反射

懒汉式(LazySingleton)

image-20200613213015761

这种写法有线程安全问题,即多个线程同时执行getInstance时有可能导致创建多个实例。

image-20200613213155647

通过添加synchronized,可以解决该问题,但是每次都会加解锁导致性能下降。

双重检查式(LazyDoubleCheckSingleton)

image-20200613213401869

一旦创建完毕,以后在第一次if判断时就会返回false,减少加解锁开销。即使初始化时竞争激烈,也会在synchronized处隔离。由于对象的创建会分三步走。

  1. 分配内存空间
  2. 内存空间初始化
  3. 变量指向该内存空间

如果指令发生重排序,假设Thread-0在内层if中执行3后,Thread-1在外层if就会判断为false,这样就会得到一个未经初始化的对象,因此需要给单例对象添加上volatile关键字,一是保证可见性,二是防止指令重排序。

基于类加载的懒汉式

类在加载过程中,jvm会自动分配一把锁,用于确保类不会加载两次。如图所示,可以借用该锁,在加载过程中完成类的初始化。

image-20200613221146280

下图是具体实现步骤,由于调用getInstance方法后,JVM会加载Innerclass,该过程中只有一个线程会初始化,这样就保证了单例,同时又是线程安全的。

image-20200613221053503

饿汉式(HungrySingleton)

顾名思义,最大的不同就是类一旦加载就完成单例的初始化。

image-20200614094022895

破坏单例模式有如下思路。

  • new instance(修饰为private,无法实现)
  • 实现Cloneable接口(单例模式不会去故意实现该接口)
  • 序列化/反序列化
  • 反射

序列化/反序列化攻击,下方代码通过反序列出一个新的实例对象,二者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;

Enum枚举类

推荐使用枚举类创建单例对象,因为枚举成员本身只能存在一个。

在反序列化中,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,可以看出同样也是私有构造方法,静态代码块加载。

image-20200614153008818

基于Map容器的单例模式

这种方式通常在初始化时,将单例对象放入map容器,且后续不再改动,方便统一管理。缺点是修改容器内部是线程不安全的。

基于ThreadLocal的单例模式

每个线程拥有唯一的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一个对象有频繁的调用过程
  • 构造函数较为复杂

总之原型模式比直接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;
}

暂无评论

发表评论