设计模式 创建型

Java设计模式学习

创建型模式

简单工厂模式

简单工厂模式严格意义上来说,并不属于设计模式中的一种,不过这里还是简单记录下。

定义:由一个工厂对象决定创建出哪一种类型实例。客户端只需传入工厂类的参数,无心关心创建过程。

优点:具体产品从客户端代码中抽离出来,解耦。

缺点:工厂类职责过重,增加新的类型时,得修改工程类得代码,违背开闭原则。

举例:新建Fruit水果抽象类,包含eat抽象方法:

public abstract class Fruit {

    public abstract void eat();
}

其实现类Apple:

public class Apple extends Fruit{
    @Override
    public void eat() {
        System.out.println("吃🍎");
    }
}

新建创建Fruit的工厂类:

public class FruitFactory {

    public Fruit produce(String name) {
        if ("apple".equals(name)) {
            return new Apple();
        } else {
            return null;
        }
    }
}

新建个客户端测试一下:

public class Application {

    public static void main(String[] args) {
        FruitFactory factory = new FruitFactory();
        Fruit fruit = factory.produce("apple");
        fruit.eat();
    }
}

运行main方法,输出:

吃🍎

可以看到,客户端Application并未依赖具体的水果类型,只关心FruitFactory的入参,这就是客户端和具体产品解耦的体现,UML图如下:

QQ截图20191216103019.png

工厂方法模式

为了解决简单工厂模式的缺点,诞生了工厂方法模式(Factory method pattern)。

定义:定义创建对象的接口,让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到了子类进行。

优点

  1. 具体产品从客户端代码中抽离出来,解耦。
  2. 加入新的类型时,只需添加新的工厂方法(无需修改旧的工厂方法代码),符合开闭原则。

缺点:类的个数容易过多,增加复杂度。

举例:新建Fruit抽象类,包含eat抽象方法:

public abstract class Fruit {

    public abstract void eat();
}

新建FruitFactory抽象工厂,定义produceFruit抽象方法:

public abstract class FruitFactory {

    public abstract Fruit produceFruit();
}

新建Fruit的实现类,Apple:

public class Apple extends Fruit {
    @Override
    public void eat() {
        System.out.println("吃🍎");
    }
}

新建FruitFactory的实现类AppleFruitFactory,用于生产具体类型的水果 —— 苹果:

public class AppleFruitFactory extends FruitFactory{
    @Override
    public Fruit produceFruit() {
        return new Apple();
    }
}

新建客户端Application测试一波:

public class Application {

    public static void main(String[] args) {
        FruitFactory factory = new AppleFruitFactory();
        Fruit fruit = factory.produceFruit();
        fruit.eat();
    }
}

运行main方法,输出如下:

吃🍎

现在要新增Banana类型的水果,只需要新增Banana类型的工厂类即可,无需修改现有的AppleFruitFactory代码,符合开闭原则。但是这种模式的缺点也显而易见,就是类的个数容易过多,增加复杂度。

上面例子UML图如下所示:

QQ截图20191216105317.png

抽象工厂模式

抽象工厂模式(Abstract factory pattern)提供了一系列相关或者相互依赖的对象的接口,关键字是“一系列”。

优点

  1. 具体产品从客户端代码中抽离出来,解耦。
  2. 将一个系列的产品族统一到一起创建。

缺点:拓展新的功能困难,需要修改抽象工厂的接口;

综上所述,抽象工厂模式适合那些功能相对固定的产品族的创建。

举例:新建水果抽象类Fruit,包含buy抽象方法:

public abstract class Fruit {

    public abstract void buy();
}

新建价格抽象类Price,包含pay抽象方法:

public abstract class Price {

    public abstract void pay();
}

新建水果创建工厂接口FruitFactory,包含获取水果和价格抽象方法(产品族的体现是,一组产品包含水果和对应的价格):

public interface FruitFactory {

    Fruit getFruit();
    Price getPrice();
}

接下来开始创建🍎这个“产品族”。新建Fruit实现类AppleFruit:

public class AppleFruit extends Fruit{
    @Override
    public void buy() {
        System.out.println("购买🍎");
    }
}

新建对应的苹果价格实现ApplePrice:

public class ApplePrice extends Price{
    @Override
    public void pay() {
        System.out.println("🍎单价2元");
    }
}

创建客户端Application,测试一波:

public class Application {
    public static void main(String[] args) {
        FruitFactory factory = new AppleFruitFactory();
        factory.getFruit().buy();
        factory.getPrice().pay();
    }
}

输出如下:

购买🍎
🍎单价2元

客户端只需要通过创建AppleFruitFactory就可以获得苹果这个产品族的所有内容,包括苹果对象,苹果价格。要新建🍌的产品族,只需要实现FruitFactory、Price和Fruit接口即可。这种模式的缺点和工厂方法差不多,就是类的个数容易过多,增加复杂度。

上面例子UML图如下所示:

QQ截图20191216112922.png

建造者模式

建造者模式也称为生成器模式(Builder Pattern),将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。简单来说就是,相同的过程可以创建不同的产品。

将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。

简单来说就是,相同的过程可以创建不同的产品。

适用于:

  1. 一个对象有非常复杂的内部结构(很多属性)
  2. 想将复杂对象的创建和使用分离。

优点

  1. 封装性好,创建和使用分离
  2. 拓展性好,建造类之间独立,一定程度上解耦。

缺点

  1. 产生多余的Builder对象;
  2. 产品内部发生变化,建造者需要更改,成本较大。

举个例子:

新增商铺类Shop,包含名称,地点和类型属性:

public class Shop {

    private String name;
    private String location;
    private String type;

    @Override
    public String toString() {
        return "Shop{" +
                "name='" + name + '\'' +
                ", location='" + location + '\'' +
                ", type='" + type + '\'' +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

接着创建Shop抽象生成器ShopBuilder:

public abstract class ShopBuilder {

    private String name;
    private String location;
    private String type;

    public abstract void name(String name);
    public abstract void location(String location);
    public abstract void type(String type);

    public abstract Shop build();
}

包含和Shop相同的属性及对应的抽象构建方法。

继续创建ShopBuilder的实现,水果店构造器FruitShopBuilder:

public class FruitShopBuilder extends ShopBuilder{

    private Shop shop = new Shop();

    @Override
    public void name(String name) {
        this.shop.setName(name);
    }

    @Override
    public void location(String location) {
        this.shop.setLocation(location);
    }

    @Override
    public void type(String type) {
        this.shop.setType(type);
    }

    @Override
    public Shop build() {
        return shop;
    }
}

创建个经销商类Dealer,用于通过ShopBuilder构建具体的商店:

public class Dealer {

    private ShopBuilder builder;

    public void setBuilder(ShopBuilder builder) {
        this.builder = builder;
    }

    public Shop build(String name, String location, String type) {
        this.builder.name(name);
        this.builder.location(location);
        this.builder.type(type);
        return builder.build();
    }
}

创建个客户端Application测试一波:

public class Application {

    public static void main(String[] args) {
        ShopBuilder builder = new FruitShopBuilder();
        Dealer dealer = new Dealer();
        dealer.setBuilder(builder);

        Shop shop = dealer.build("XX水果店", "福州市XX区XX街XX号", "水果经营");
        System.out.println(shop);
    }
}

输出如下:

Shop{name='XX水果店', location='福州市XX区XX街XX号', type='水果经营'}

这个例子是典型的建造者模式,UML图如下所示:

QQ截图20191216162813.png

其实建造者模式更为常用的例子是下面这个:

创建一个店铺类Shop,Shop里包含构造该Shop的内部类:

public class Shop {

    private String name;
    private String location;
    private String type;

    public Shop(ShopBuilder builder) {
        this.name = builder.name;
        this.location = builder.location;
        this.type = builder.type;
    }

    @Override
    public String toString() {
        return "Shop{" +
                "name='" + name + '\'' +
                ", location='" + location + '\'' +
                ", type='" + type + '\'' +
                '}';
    }

    public static class ShopBuilder {
        private String name;
        private String location;
        private String type;

        public ShopBuilder name(String name) {
            this.name = name;
            return this;
        }

        public ShopBuilder location(String location) {
            this.location = location;
            return this;
        }

        public ShopBuilder type(String type) {
            this.type = type;
            return this;
        }

        public Shop build() {
            return new Shop(this);
        }
    }
}

在客户端构建Shop只需:

public class Application {

    public static void main(String[] args) {
        Shop shop = new Shop.ShopBuilder()
                .name("XX水果店")
                .location("福州市XX区XX街XX号")
                .type("水果经营")
                .build();
        System.out.println(shop);
    }
}

这种用法和Lombok的@Builder注解效果是一样的。

这个例子的UML图:

QQ截图20191216163308.png

单例模式

单例模式目的是为了一个类只有一个实例。

优点:

  1. 内存中只有一个实例,减少了内存开销;
  2. 可以避免对资源的多重占用;
  3. 设置全局访问点,严格控制访问。

缺点:

  1. 没有接口,拓展困难。

懒汉模式

懒汉模式下的单例写法是最简单的,但它是线程不安全的:

public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    private LazySingleton() {

    }

    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

可加同步锁解决线程安全问题:

public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    private LazySingleton() {

    }

    public static LazySingleton getInstance() {
        synchronized (LazySingleton.class) {
            if (lazySingleton == null) {
                lazySingleton = new LazySingleton();
            }
        }
        return lazySingleton;
    }
}

但是同步锁锁的是整个类,比较消耗资源,并且即使运行内存中已经存在LazySingleton,调用其getInstance还是会上锁,所以这种写法也不是很好。

双重同步锁单例模式

public class LazyDoubleCheckSingleton {

    private static LazyDoubleCheckSingleton instance = null;

    private LazyDoubleCheckSingleton() {

    }

    public static LazyDoubleCheckSingleton getInstance() {
        if (instance == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

上面例子虽然加了同步锁,但它还是线程不安全的。虽然上面的例子不会出现多次初始化LazyDoubleCheckSingleton实例的情况,但是由于指令重排的原因,某些线程可能会获取到空对象,后续对该对象的操作将触发空指针异常。

要修复这个问题,只需要阻止指令重排即可,所以可以给instance属性加上volatile关键字:

public class LazyDoubleCheckSingleton {

    private volatile static LazyDoubleCheckSingleton instance = null;

    private LazyDoubleCheckSingleton() {

    }

    public static LazyDoubleCheckSingleton getInstance() {
        if (instance == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

相关博文:深入理解volatile关键字

上面这种写法是不但确保了线程安全,并且当LazyDoubleCheckSingleton实例创建好后,后续再调用其getInstance方法不会上锁。

静态内部类单例模式

看例子:

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {

    }

    private static class InnerClass {
        private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.instance;
    }
}

为什么这个例子是可行的呢?主要有两个原因:

  1. JVM在类的初始化阶段会加Class对象初始化同步锁,同步多个线程对该类的初始化操作;
  2. 静态内部类InnerClass的静态成员变量instance在方法区中只会有一个实例。

在Java规范中,当以下这些情况首次发生时,A类将会立刻被初始化:

  1. A类型实例被创建;
  2. A类中声明的静态方法被调用;
  3. A类中的静态成员变量被赋值;
  4. A类中的静态成员被使用(非常量);

饿汉单例模式

“饿汉”意指在类加载的时候就初始化:

public class HungrySingleton {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}

这种模式在类加载的时候就完成了初始化,所以并不存在线程安全性问题;但由于不是懒加载,饿汉模式不管需不需要用到实例都要去创建实例,如果创建了不使用,则会造成内存浪费。

序列化破坏单例模式

前面的单例例子在实现序列化接口后都能被序列化的方式破坏,比如HungrySingleton,让其实现序列化接口:

public class HungrySingleton implements Serializable {

    private static final long serialVersionUID = -8073288969651806838L;

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}

然后创建Application测试一下如何破坏:

public class Application {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 演示序列化破坏单例
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file"));
        outputStream.writeObject(instance);

        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("file"));
        HungrySingleton newInstance = (HungrySingleton) inputStream.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

输出如下所示:

cc.mrbird.design.pattern.creational.singleton.HungrySingleton@7f31245a
cc.mrbird.design.pattern.creational.singleton.HungrySingleton@6d03e736
false

可以看到,虽然是单例模式,但却成功创建出了两个不一样的实例,单例遭到了破坏。

要让反序列化后的对象和序列化前的对象是同一个对象的话,可以在HungrySingleton里加上readResolve方法:

public class HungrySingleton implements Serializable {

    private static final long serialVersionUID = -8073288969651806838L;

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return instance;
    }
    // 新增
    private Object readResolve() {
        return instance;
    }
}

再次运行Application的main方法后:

cc.mrbird.design.pattern.creational.singleton.HungrySingleton@7f31245a
cc.mrbird.design.pattern.creational.singleton.HungrySingleton@7f31245a
true

可以看到,这种方式最终反序列化出来的对象和序列化对象是同一个对象。但这种方式反序列化过程内部还是会重新创建HungrySingleton实例,只不过因为HungrySingleton类定义了readResolve方法(方法内部返回instance引用),反序列化过程会判断目标类是否定义了readResolve该方法,是的话则通过反射调用该方法。

反射破坏单例模式

除了序列化能破坏单例外,反射也可以,举个反射破坏HungrySingleton的例子:

public class Application {

    public static void main(String[] args) throws Exception {
        HungrySingleton instance = HungrySingleton.getInstance();
        // 反射创建实例
        Class<HungrySingleton> c = HungrySingleton.class;
        // 获取构造器
        Constructor<HungrySingleton> constructor = c.getDeclaredConstructor();
        // 打开构造器权限
        constructor.setAccessible(true);
        HungrySingleton newInstance = constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

输出如下所示:

cc.mrbird.design.pattern.creational.singleton.HungrySingleton@1b6d3586
cc.mrbird.design.pattern.creational.singleton.HungrySingleton@4554617c
false

可以看到,我们通过反射破坏了私有构造器权限,成功创建了新的实例。

对于这种情况,饿汉模式下的例子可以在构造器中添加判断逻辑来防御(懒汉模式的就没有办法了),比如修改HungrySingleton的代码如下所示:

public class HungrySingleton {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
        if (instance != null) {
            throw new RuntimeException("forbidden");
        }
    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}

再次运行Application的main方法:

Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at cc.mrbird.design.pattern.creational.singleton.Application.main(Application.java:33)
Caused by: java.lang.RuntimeException: forbidden
    at cc.mrbird.design.pattern.creational.singleton.HungrySingleton.<init>(HungrySingleton.java:16)
    ... 5 more

枚举单例模式

枚举单例模式是推荐的单例模式,它不仅可以防御序列化攻击,也可以防御反射攻击。举个枚举单例模式的代码:

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 Application {

    public static void main(String[] args) throws Exception {

        EnumSingleton instance = EnumSingleton.getInstance();
        instance.setData(new Object());
        EnumSingleton newInstance = EnumSingleton.getInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
    }
}

输出如下所示:

INSTANCE
INSTANCE
java.lang.Object@1b6d3586
java.lang.Object@1b6d3586

测试下序列化攻击:

public class Application {

    public static void main(String[] args) throws Exception {
        EnumSingleton instance = EnumSingleton.getInstance();
        instance.setData(new Object());
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file"));
        outputStream.writeObject(instance);
       
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("file"));
        EnumSingleton newInstance = (EnumSingleton) inputStream.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);

        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
        System.out.println(instance.getData() == newInstance.getData());
    }
}

输出如下所示:

INSTANCE
INSTANCE
true
java.lang.Object@568db2f2
java.lang.Object@568db2f2
true

可以看到序列化和反序列化后的对象是同一个。

原理:跟踪ObjectInputStream#readObject源码,其中当反编译对象为枚举类型时,将调用readEnum方法:

QQ截图20191217155448.png

name为枚举类里的枚举常量,对于线程来说它是唯一的,存在方法区,所以通过Enum.valueOf((Class)cl, name)方法得到的枚举对象都是同一个。

再测试一下反射攻击:

public class Application {

    public static void main(String[] args) throws Exception {
        EnumSingleton instance = EnumSingleton.getInstance();

        Class<EnumSingleton> c = EnumSingleton.class;
        // 枚举类只包含一个(String,int)类型构造器
        Constructor<EnumSingleton> constructor = c.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        EnumSingleton newInstance = constructor.newInstance("hello", 1);

        System.out.println(instance == newInstance);
    }
}

运行输出如下:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at cc.mrbird.design.pattern.creational.singleton.Application.main(Application.java:71)

可以看到抛异常了,查看Constructor类的417行代码可以发现原因:QQ截图20191217160647.png

Java禁止通过反射创建枚举对象。

正是因为枚举类型拥有这些天然的优势,所以用它创建单例是不错的选择,这也是Effective Java推荐的方式。

原型模式

原型实例指定创建对象的种类,通过拷贝这些原型创建新的对象。

适用于:

  1. 类初始化消耗较多资源;
  2. 循环体中生产大量对象的时候。

优点:

  1. 原型模式性能比直接new一个对象性能好;
  2. 简化创建对象过程。

缺点:

  1. 对象必须重写Object克隆方法;
  2. 复杂对象的克隆方法写起来较麻烦(深克隆、浅克隆)

举例:新建一个学生类Student,实现克隆接口,并重写Object的克隆方法(因为都是简单属性,所以浅克隆即可):

public class Student implements Cloneable {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

在Application中测试一波:

public class Application {

    public static void main(String[] args) throws CloneNotSupportedException {
        Student student = new Student();
        ArrayList<Student> list = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            Student s = (Student) student.clone();
            s.setName("学生" + i);
            s.setAge(20 + i);
            list.add(s);
        }
        System.out.println(list);
    }
}

输出如下所示:

[Student{name='学生0', age=20}, Student{name='学生1', age=21}, Student{name='学生2', age=22}]

这种方式会比直接在循环中创建Student性能好。

当对象包含引用类型属性时,需要使用深克隆,比如Student包含Date属性时:

public class Student implements Cloneable {

    private String name;
    private int age;
    private Date birthday;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        // 引用类型深克隆
        Date birthday = (Date) student.getBirthday().clone();
        student.setBirthday(birthday);
        return student;
    }
}

值得注意的是,克隆会破坏实现了Cloneable接口的单例对象。

posted @ 2022-10-06 16:44  Cassie_Lee  阅读(44)  评论(0)    收藏  举报