JAVA设计模式第二讲:创建型设计模式

设计模式(design pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。本文以面试题作为切入点,介绍了设计模式的常见问题。我们需要掌握各种设计模式的原理、实现、设计意图和应用场景,搞清楚能解决什么问题。本文是第二篇:创建型设计模式

设计模式从入门到精通:创建型设计模式

    • 7、常用的设计模式(代表了最佳实践 共23种,常用的14种)
    • 8、创建型设计模式
      • 8.1、单例设计模式一共有几种实现方式?请分别用代码实现,并说明各个实现方式的优点和缺点?
        • 为什么说 Enum 实现单例模式是最佳实践
      • 8.2、什么时候该用工厂模式?相对于直接 new 来创建对象,用工厂模式来创建究竟有什么好处呢?
        • Demo3 工厂模式在JDK-Calendar的应用
        • 工厂模式最佳实践
        • Demo4 使用工厂模式实现 Spring BeanFactory?
      • 8.3、Builder 设计模式 将产品和产品建造过程解耦
        • Demo2 借助 Lombok 中的 @Builder 注解实现建造者模式
        • Demo3 Builder 模式在JDK的应用和源码分析
      • 补充 不可变模式
      • 8.4、原型设计模式的深拷贝和浅拷贝是什么,并写出深拷贝的两种方式的源码
      • 创建型设计模式总结:

7、常用的设计模式(代表了最佳实践 共23种,常用的14种)

掌握设计模式的五个层次
第一层:刚开始学编程不久,听说过什么是设计模式;
第二层:有长时间的编程经验,自己写了很多代码,其中用到了设计模式,但是自己却不知道;
第三层:学习过了设计模式,发现自己已经在使用了,并且发现一些新的模式挺好用的;✅
第四层:阅读了很多别人写的源码和框架, 在其中看到别人设计模式,并且能够领会设计模式的精妙和带来的好处
第五层: 代码写着写着,自己都没有意识到使用了设计模式,并且熟练的写了出来

总体来说设计模式分为三大类
创建型模式(对对象创建过程中各种问题和解决方案的总结)

  • 共5种:单例模式,工厂方法模式(抽象工厂模式),建造者模式,原型模式(不常用)

结构型模式(关注于类/对象 继承/组合方式)

  • 共7种:代理模式,桥接模式,适配器模式,装饰器模式,外观模式(不常用),组合模式(不常用),享元模式(不常用)

行为型模式(是从类或对象之间交互/职责划分等角度总结的模式)

  • 共11种:观察者模式,模板方法模式,策略模式,迭代器模式,责任链模式,状态模式,命令模式(不常用),备忘录模式(不常用),访问者模式(不常用),中介者模式(不常用),解释器模式(不常用)

  • 总结

    • 设计模式要干的事情就是解耦。
      创建型模式是将创建和使用代码解耦结构型模式是将不同功能代码解耦行为型模式是将不同的行为代码解耦

8、创建型设计模式

创建型设计模式包括:单例模式,工厂方法模式(抽象工厂模式),建造者模式,原型模式(不常用)。它主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码

8.1、单例设计模式一共有几种实现方式?请分别用代码实现,并说明各个实现方式的优点和缺点?

单例模式用来创建全局唯一的对象。
定义:一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。单例有几种经典的实现方式,它们分别是:饿汉式(两种)、懒汉式(三种)、双重检测、静态内部类、枚举(最佳实践)。
使用场景:①需要频繁的进行创建和销毁的对象、②创建对象时耗时过多或耗费资源过多(即:重量级对象), 但又经常用到的对象、③工具类对象、④频繁访问数据库或文件的对象(比如数据源、 session工厂等)

Demo1、饿汉式:(关键词:静态常量)

public Class singleton{
	 // 1、私有化构造函数 
     private Singleton() {}
     // 2、内部直接创建对象 
     public static singleton instance = new singleton();
     // 3、提供公有静态方法,返回对象实例 
     public static Singleton getInstance() {
       	return instance;
     }
}

优点:写法简单,避免线程同步问题
缺点:没有懒加载,可能造成内存浪费  不推荐. 
使用场景:耗时的初始化操作,提前到程序启动时完成

Demo2、饿汉式(静态代码块)

public Class singleton{
	 // 1、私有化构造函数 
     private Singleton() {}
    //2.本类内部创建对象实例
	private static Singleton instance;
	static { 
		// 在静态代码块中,创建单例对象
		instance = new Singleton();
	}
	//3. 提供一个公有的静态方法,返回实例对象
	public static Singleton getInstance() {
		return instance;
	}
}
优点:写法简单,避免线程同步问题
缺点:没有懒加载,可能造成内存浪费   不推荐

Demo3、懒汉式(线程不安全)

class Singleton {
	private static Singleton instance;
	private Singleton() {}
	//提供一个静态的公有方法,当使用到该方法时,才去创建 instance
	public static Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}
优点:起到了懒加载的效果
缺点:只能在单线程环境使用, 不要使用

Demo4、懒汉式(线程安全)

class Singleton {
	private static Singleton instance;
	private Singleton() {}
	//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
	public static synchronized Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}
优点:起到了懒加载的效果,解决了线程安全问题
缺点:效率低,不推荐

Demo5、懒汉式(线程安全,同步代码块)

class Singleton {
	private static Singleton singleton;
	private Singleton() {}
	//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
	public static Singleton getInstance() {
		if(singleton == null) {
		synchronized (Singleton.class) {
			singleton = new Singleton();
		}
		return singleton;
	}
}
不推荐使用,并不能起到线程同步的作用

Demo6、双重锁校验 可以应用于连接池的使用中

public Class singleton{
	private static volatile Singleton instance;
	private Singleton() {}
	//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
	//同时保证了效率, 推荐使用
	public static Singleton getInstance() {
		if(instance == null) {
			synchronized (Singleton.class) {
				if(instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}
优点:Double-Check机制保证线程安全,效率高   推荐使用

Demo7、静态内部类

class Singleton {
	private static Singleton instance;
	//构造器私有化
	private Singleton() {}
	//写一个静态内部类,该类中有一个静态属性 Singleton
	private static class SingletonInstance {
		private static final Singleton INSTANCE = new Singleton(); 
	}
	//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
	public static Singleton getInstance() {
		return SingletonInstance.INSTANCE;
	}
}
优点:JVM保证了线程安全,类的静态属性只会在第一次加载类的时候初始化,效率高,推荐

Demo8、枚举 – 单例实现的最佳实践

enum Singleton {
	INSTANCE; //属性
	public void sayOK() {
		System.out.println("ok");
	}
}
// main方法来测试
public static void main(String[] args) {
	Singleton instance = Singleton.INSTANCE;
	instance.sayOK();
}
优点:使用枚举,可以实现单例,避免了多线程同步问题,还能防止反序列化重新创建新的对象,推荐

在这里插入图片描述

补充Demo9:java核心类库 Runtime 的单例实现(饿汉式)

  • 每个 Java 应用在运行时会启动一个 JVM 进程,每个 JVM 进程都只对应一个 Runtime 实例,用于查看 JVM 状态以及控制 JVM 行为。进程内唯一,所以比较适合设计为单例。
//静态实例被声明为final,一定程度上保证了实例不被篡改
public class Runtime {
	private Runtime(){}
	private static final Runtime currentRuntime = new Runtime();
	private static Version version;
	public static Runtime getRuntime(){
	    return currentRuntime;
	}
	public void addShutdownHook(Thread hook) {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			sm.checkPermission(new RuntimePermission("shutdownHooks"));
		}
		ApplicationShutdownHooks.add(hook);
	}
}

Demo10: 获取 spi 单例

public class SpiProviderSelector {
    private static volatile SpiProviderSelector instance = null;
    private SpiProviderSelector(){}
    /* 获取单例*/
    public static SpiProviderSelector getInstance() {
        if(instance == null){
            synchronized (SpiProviderSelector.class){
                if(instance == null){
                    instance = new SpiProviderSelector();
                }
            }
        }
        return instance;
    }
}

单例模式缺点
1、单例对 OOP 特性的支持不友好

  • 对继承、多态特性支持不友好

2、单例对代码的可测试性不友好

  • 硬编码方式,无法实现 mock 替换

3、单例不支持有参数的构造函数

Demo11:怎么在单例模式中给对象初始化数据?

public class Singleton {
	private static Singleton instance = null;
	private final int paramA;
	private final int paramB;
	private Singleton(int paramA, int paramB) {
		this.paramA = paramA;
		this.paramB = paramB;
	} 
	public static Singleton getInstance() {
		if (instance == null) {
			throw new RuntimeException("Run init() first.");
		}
		return instance;
	} 
	public synchronized static Singleton init(int paramA, int paramB) {
		if (instance != null){
			throw new RuntimeException("Singleton has been created!");
		}
		instance = new Singleton(paramA, paramB);
		return instance;
	}
} 

// 先init,再使用 getInstance()
Singleton.init(10, 50); 
Singleton singleton = Singleton.getInstance()

单例模式的替代方案?

  • 通过工厂模式、IOC 容器来保证全局唯一性。

Action:请问 Spring下的 bean 单例模式与设计模式(GOF)中的单例模式区别? 可以作为面试题

  • 它们关联的环境不同,单例模式是指在一个JVM进程中仅有一个实例,不管在程序中的何处获取实例,始终都返回同一个对象。
    Spring单例是指一个Spring Bean容器(ApplicationContext)中仅有一个实例。

为什么说 Enum 实现单例模式是最佳实践

利用Enum实现单例模式

/* 使用枚举实现单例 */
public enum EnumSingleton {
	// 唯一的实例对象
    INSTANCE; 
    
    // 单例对象的属性对象
    private Object obj = new Object();
 
    public Object getObj() {
        return obj;
    }
 
    // 单例提供的对外服务。
    public Object getFactoryService() {
        return new Object();
    }
}

反编译代码

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumSingleton.java
public final class EnumSingleton extends Enum {
    public static EnumSingleton[] values() {
        return (EnumSingleton[])$VALUES.clone();
    }
 
    public static EnumSingleton valueOf(String name) {
        return (EnumSingleton)Enum.valueOf(EnumSingleton, name);
    }
    
    // 无法通过new来随意创建对象,构造函数为private.
    private EnumSingleton(String s, int i) {
        super(s, i);
        obj = new Object();
    }
 
    public Object getObj() {
        return obj;
    }
 
    public Object getFactoryService() {
        return new Object();
    }
 
    // 提供获取唯一实例对象的方法,通常是getInstance
    // 也可以直接获取到INSTANCE,但是获取到的都是一个对象
    public static final EnumSingleton INSTANCE;
    private Object obj;
    private static final EnumSingleton $VALUES[];
 
    // 静态代码中实例化对象,多线程并发的情况下保证唯一,属于饿汉模式
    static {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

从反编译代码中我们可以看到 Enum 的特点

  • 枚举本质上是个final类
  • 定义的枚举值实际上就是一个枚举类的不可变对象(比如这里的INSTANCE)
  • 在Enum类加载的时候,就已经实例化了这个对象
  • 无法通过 new 来创建枚举对象

Enum实现单例模式的几个关键点验证
1.避免反射创建单例对象(反射攻击)
枚举的私有构造函数如下所示:

private EnumSingleton(String s, int i)

尝试利用反射创建对象

/**
 * 反射攻击。
 * 由于Enum天然的不允许反射创建实例,所以可以完美的防范反射攻击。
 */
private static void reflectionAttack() {
    try {
        Constructor con = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
        con.setAccessible(true);
        // 反射新建对象以破坏单例
        Object obj = con.newInstance("INSTANCE", 0);
        System.out.println(obj);
        System.out.println(EnumSingleton.getInstance());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

会抛出异常"java.lang.IllegalArgumentException: Cannot reflectively create enum objects"。如下所示:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:416)
    at com.ws.pattern.singleton.EnumSingletonAppMain.reflectionAttack(EnumSingletonAppMain.java:43)
    at com.ws.pattern.singleton.EnumSingletonAppMain.main(EnumSingletonAppMain.java:9)

从异常可以看出来,newInstance抛出了异常。推测Java反射是不允许创建Enum对象的,看看源码Constructor.java中的newInstance方法,存在处理Enum类型实例化的一行判断代码 if ((clazz.getModifiers() & Modifier.ENUM) != 0),满足这个条件就抛出异常。newInstance的JDK代码如下:

public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

2.避免通过序列化创建单例对象(如果单例类实现了Serializable)(序列化攻击)

  • 序列化对象后,如果执行反序列化,也可以创建一个对象。利用此机制来尝试创建一个新的Enum对象。
/** 序列化攻击
 * 需要在单例类中增加read
 */
private static void serializableAttack() {
    EnumSingleton singleton = EnumSingleton.getInstance();
    System.out.println(singleton);
    try {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./EnumSingleton.out"));
        oos.writeObject(singleton);
        ObjectInputStream ois = new ObjectInputStream((new FileInputStream("./EnumSingleton.out")));
        Object obj = ois.readObject(); // 这里利用反序列化创建对象
        System.out.println(obj);
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

拿到了相同的对象。执行结果如下:

INSTANCE
INSTANCE

跟踪进入ois.readObject(),会进入ObjectInputStream.readObject0方法。其中会解析class的二进制,根据class的文件定义,分别解析不同类型的字段。重点关注case TC_ENUM:如下所示:

switch (tc) {
    case TC_NULL:
        return readNull();
 
    case TC_REFERENCE:
        return readHandle(unshared);
 
    case TC_CLASS:
        return readClass(unshared);
 
    case TC_CLASSDESC:
    case TC_PROXYCLASSDESC:
        return readClassDesc(unshared);
 
    case TC_STRING:
    case TC_LONGSTRING:
        return checkResolve(readString(unshared));
 
    case TC_ARRAY:
        return checkResolve(readArray(unshared));
 
    case TC_ENUM:
        return checkResolve(readEnum(unshared));
 
    case TC_OBJECT:
        return checkResolve(readOrdinaryObject(unshared));
 
    case TC_EXCEPTION:
        IOException ex = readFatalException();
        throw new WriteAbortedException("writing aborted", ex);
 
    case TC_BLOCKDATA:
    case TC_BLOCKDATALONG:
        if (oldMode) {
            bin.setBlockDataMode(true);
            bin.peek();             // force header read
            throw new OptionalDataException(
                bin.currentBlockRemaining());
        } else {
            throw new StreamCorruptedException(
                "unexpected block data");
        }
    case TC_ENDBLOCKDATA:
        if (oldMode) {
            throw new OptionalDataException(true);
        } else {
            throw new StreamCorruptedException(
                "unexpected end of block data");
        }
    default:
        throw new StreamCorruptedException(
            String.format("invalid type code: %02X", tc));
}

进入readEnum方法,重点关注Enum.valueOf方法。如下所示:

/**
 * Reads in and returns enum constant, or null if enum type is
 * unresolvable.  Sets passHandle to enum constant's assigned handle.
 */
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")
            // 这里根据 name 和 class拿到 Enum 实例
            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;
}

再跟进Enum.valueOf方法。代码如下:

public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
     // 从enumConstantDirectory()中根据name获取对象
     T result = enumType.enumConstantDirectory().get(name);
     if (result != null)
         return result;
     if (name == null)
         throw new NullPointerException("Name is null");
     throw new IllegalArgumentException(
         "No enum constant " + enumType.getCanonicalName() + "." + name);
}

enumConstantDirectory()是Class的方法,其本质是从Class.java的enumConstantDirectory属性中获取。代码如下:

private volatile transient Map<String, T> enumConstantDirectory = null;

也就是说,Enum中定义的Enum成员值都被缓存在了这个Map中,Key是成员名称(比如“INSTANCE”),Value就是Enum的成员对象。这样的机制天然保证了取到的Enum对象是唯一的。即使是反序列化,也是一样的。

结论:单例枚举的实现简化了单例模式的创建,利用了枚举特性,使得单例枚举成为最佳实践。


8.2、什么时候该用工厂模式?相对于直接 new 来创建对象,用工厂模式来创建究竟有什么好处呢?

概念:工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。

使用场景如果创建对象的逻辑并不复杂,那我们直接通过 new 来创建对象就可以了,不需要使用工厂模式用到大量的创建某种、某类或者某批对象时,考虑使用工厂模式

工厂模式的作用将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系解耦。从而提高项目的扩展和维护性

Demo1:简单工厂使用示例(规则校验器)

@Component
public class RuleValidatorFactory {
    @Autowired
    private ArRuleValidator aValidator;
    @Autowired
    private BRuleValidator bValidator;
    @Autowired
    private CRuleValidator cValidator;
    @Autowired
    private DRuleValidator dValidator;

    public IBaseValidator createRuleValidator(ValidatorModeEnum validatorModeEnum) {
        switch (validatorModeEnum) {
            case NAME:
                return aValidator;
            case DETAIL:
                return bValidator;
            case MAIN_IMG:
                return cValidator;
            case CATEGORY_ATTR:
                return dValidator;
        }
        throw new ServiceException("校验模式不存在");
    }
}

//调用该工厂模式的方法
IBaseValidator baseValidator = ruleValidatorFactory.ruleValidator(mode);
List<String> result = baseValidator.validateItemRule(validatorModeEnum);

工厂方法模式

  • 1、普通工厂:建立一个工厂类,对实现了同一接口的一些类进行实例的创建
  • 2、多个工厂方法模式 :在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
  • 3、静态工厂方法:上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
  • 4、抽象工厂模式:围绕一个超级工厂创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。
    • 防止工厂过多,对工厂类做了一层抽象

Demo2:使用是单例模式结合工厂模式
解决每次调用工厂类都会创建新对象的问题

public class RuleValidatorFactory {
	private static final Map<String, IBaseValidator> cachedParsers = new HashMap<>();

	// 定义一个静态代码块,存储对象
	static {
		cachedParsers.put(NAME, new ArRuleValidator());
		cachedParsers.put(DETAIL, new BRuleValidator());
		cachedParsers.put(MAIN_IMG, new CRuleValidator());
		cachedParsers.put(CATEGORY_ATTR, new DRuleValidator());
	}

    public IBaseValidator createRuleValidator(ValidatorModeEnum validatorModeEnum) {
           	return cachedParsers.get(validatorModeEnum); 
        }
        throw new ServiceException("校验模式不存在");
    }
}

Demo3 工厂模式在JDK-Calendar的应用

Calendar 类提供了大量跟日期相关的功能代码,同时,又提供了一个 getInstance() 工厂
方法,用来根据不同的 TimeZone 和 Locale 创建不同的 Calendar 子类对象

public abstract class Calendar implements Serializable, Cloneable, Comparable<> {
	// ...
	public static Calendar getInstance(){
		return createCalendar(TimeZone.getDefault(),Locale.getDefault(Locale.Category.FORMAT));
	}
	private static Calendar createCalendar(TimeZone zone,Locale aLocale) {
		if (provider != null) {
			try {
				return provider.getInstance(zone, aLocale);//默认方式获取
			} catch (IllegalArgumentException iae) {
				// fall back to the default instantiation
			}
		 }
		Calendar cal = null;
		if (aLocale.hasExtensions()) {
            String caltype = aLocale.getUnicodeLocaleType("ca");
            if (caltype != null) {
                switch (caltype) {
                case "buddhist":
                cal = new BuddhistCalendar(zone, aLocale);
                    break;
                case "japanese":
                    cal = new JapaneseImperialCalendar(zone, aLocale);
                    break;
                case "gregory":
                    cal = new GregorianCalendar(zone, aLocale);
                    break;
                }
            }
        }
    }
}
public class SimpleFactory {
	public static void main(String[] args) {
		Calendar cal = Calendar.getInstance();
		// 注意月份下标从0开始,所以取月份要+1
		System.out.println("年:" + cal.get(Calendar.YEAR));
		System.out.println("月:" + (cal.get(Calendar.MONTH) + 1));
		System.out.println("日:" + cal.get(Calendar.DAY_OF_MONTH));
		System.out.println("时:" + cal.get(Calendar.HOUR_OF_DAY));
		System.out.println("分:" + cal.get(Calendar.MINUTE));
		System.out.println("秒:" + cal.get(Calendar.SECOND));
	}
}

使用了工厂模式的组件:DateFormat String

工厂模式最佳实践

都使用抽象工厂模式,按照产品族维度来建立工厂,如果只有一个产品那么工厂中就一个方法,如果有多个产品就多个方法。
重温设计模式之 Factory–阿里对工厂模式的实践

工厂模式一个非常经典的应用场景:依赖注入框架
比如 SpringIOC、Google Guice它用来集中创建、组装、管理对象,跟具体业务代码解耦让程序员聚焦在业务代码的开发上

Demo4 使用工厂模式实现 Spring BeanFactory?

工厂类:负责某个类对象或者某一组相关类对象(继承自同一抽象类或者接口的子类)的创建;
DI 容器:负责的是整个应用中所有类对象的创建。

  • 它的功能:①配置解析 ②对象创建 ③对象生命周期管理

第一步,配置解析
DI 容器来创建的类对象和创建类对象的必要信息放到配置文件中。容器读取配置文件,根据配置文件提供的信息来创建对象。
Spring 容器的配置文件。Spring 容器读取这个配置文件,解析出要创建的两个对象:rateLimiterredisCounter,并且得到两者的依赖关系:rateLimiter 依赖 redisCounter。

public class RateLimiter {
	private RedisCounter redisCounter;
	public RateLimiter(RedisCounter redisCounter) {
		this.redisCounter = redisCounter;
	}
	public void test() {
		System.out.println("Hello World!");
	}
	//...
} 
public class RedisCounter {
	private String ipAddress;
	private int port;
	public RedisCounter(String ipAddress, int port) {
		this.ipAddress = ipAddress;
		this.port = port;
	}
	//...
}

Spring 配置文件beans.xml:

<beans>
	<bean id="rateLimiter" class="com.xzg.RateLimiter">
		<constructor-arg ref="redisCounter"/>
	bean>
	<bean id="redisCounter" class="com.xzg.redisCounter">
		<constructor-arg type="String" value="127.0.0.1">
		<constructor-arg type="int" value=1234>
	bean>
beans>

对象创建
将所有类对象的创建都放到一个BeansFactory工厂类中完成就可以了,通过“反射”机制,它能在程序运行的过程中,动态地加载类、创建对象,不需要事先在代码中写死要创建哪些对象

对象的生命周期管理
简单工厂模式有两种实现方式,一种是每次都返回新创建的对象,另一种是每次都返回同一个事先创建好的对象,也就是单例对象

在 Spring 框架中,①我们可以通过配置 scope 属性,来区分这两种不同类型的对象。scope=prototype 表示返回新创建的对象,scope=singleton 表示返回单例对象
配置对象是否支持懒加载。如果 lazy-init=true,对象在真正被使用到的时候才会被创建。
③配置对象的 init-method (初始化对象) 和 destroy-method (做清理工作)方法

如何使用 BeanFactory
从 classpath 中加载 XML 格式的配置文件,然后通过 BeanConfigParser 解析为统一的 BeanDefinition 格式,然后,BeansFactory 根据 BeanDefinition 来创建对象。

public class Demo {
	public static void main(String[] args) {
		ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
		RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rateLimiter");
		rateLimiter.test();
		//...
	}
}

public interface ApplicationContext {
	Object getBean(String beanId);
}

public class ClassPathXmlApplicationContext implements ApplicationContext {
	private BeansFactory beansFactory;
	private BeanConfigParser beanConfigParser;
	
	public ClassPathXmlApplicationContext(String configLocation) {
		this.beansFactory = new BeansFactory();
		this.beanConfigParser = new XmlBeanConfigParser();
		loadBeanDefinitions(configLocation);
	} 
	private void loadBeanDefinitions(String configLocation) {
		InputStream in = null;
		try {
			in = this.getClass().getResourceAsStream("/" + configLocation);
			if (in == null) {
				throw new RuntimeException("Can not find config file: " + configLocation);
			}
			// 推荐看 spring 源码
			List<BeanDefinition> beanDefinitions = beanConfigParser.parse(in);
			beansFactory.addBeanDefinitions(beanDefinitions);
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException e) {
					// TODO: log error
				}
			}
		}
	} 
	@Override
	public Object getBean(String beanId) {
		return beansFactory.getBean(beanId);
	}
}

public class BeanDefinition {
	private String id;
	private String className;
	private List<ConstructorArg> constructorArgs = new ArrayList<>();
	private Scope scope = Scope.SINGLETON;
	private boolean lazyInit = false;
	// 省略必要的getter/setter/constructors
	public boolean isSingleton() {
		return scope.equals(Scope.SINGLETON);
	} 
	public static enum Scope {
		SINGLETON,
		PROTOTYPE
	} 
	public static class ConstructorArg {
		private boolean isRef;
		private Class type;
		private Object arg;
		// 省略必要的getter/setter/constructors
	}
}

BeansFactory 的定义 负责根据从配置文件解析得到的 BeanDefinition 来创建对象

JVM 在启动的时候会根据代码自动地加载类、创建对象。至于都要加载哪些类、创建哪些对象,这些都是在代码中写死的,或者说提前写好的。但是,如果某个对象的创建并不是写死在代码中,而是放到配置文件中,我们需要在程序运行期间,动态地根据配置文件来加载类、创建对象(Java反射技术)

public class BeansFactory {
	// 用于保存单例对象  scope == singleton,下次直接从 Map 中取数据
	private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>();
	private ConcurrentHashMap<String, BeanDefinition> beanDefinitions = new ConcurrentHashMap<>();
	
	public void addBeanDefinitions(List<BeanDefinition> beanDefinitionList) {
		for (BeanDefinition beanDefinition : beanDefinitionList) {
			this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinition)
		} 
		for (BeanDefinition beanDefinition : beanDefinitionList) {
			// 非懒加载 且为单例  ---》 饿汉式单例
			if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton())
			createBean(beanDefinition);
		}
	}

	public Object getBean(String beanId) {
		BeanDefinition beanDefinition = beanDefinitions.get(beanId);
		if (beanDefinition == null) {
			throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);
		}
		return createBean(beanDefinition);
	}
	
	@VisibleForTesting
	protected Object createBean(BeanDefinition beanDefinition) {
		// 单例
		if (beanDefinition.isSingleton() && singletonObjects.containsKey(beanDefinition.getId())) {
			return singletonObjects.get(beanDefinition.getId());
		} 
		Object bean = null;
		try {
			// 非单例 or singletonObjects 不包含时,通过反射加载类,创建对象
			Class beanClass = Class.forName(beanDefinition.getClassName());
			List<BeanDefinition.ConstructorArg> args = beanDefinition.getConstructorArg();
			if (args.isEmpty()) {
				bean = beanClass.newInstance();
			} else {
				Class[] argClasses = new Class[args.size()];
				Object[] argObjects = new Object[args.size()];
				for (int i = 0; i < args.size(); ++i) {
					BeanDefinition.ConstructorArg arg = args.get(i);
					if (!arg.getIsRef()) {
						argClasses[i] = arg.getType();
						argObjects[i] = arg.getArg();
					} else {
						BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg());
						if (refBeanDefinition == null) {
							throw new NoSuchBeanDefinitionException("Bean is not defined: " +
						}
						argClasses[i] = Class.forName(refBeanDefinition.getClassName());
						// 递归调用
						argObjects[i] = createBean(refBeanDefinition);
					}
				}
				bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
			}
		} catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTarget
			throw new BeanCreationFailureException("", e);
		} 
		if (bean != null && beanDefinition.isSingleton()) {
			singletonObjects.putIfAbsent(beanDefinition.getId(), bean);
			return singletonObjects.get(beanDefinition.getId());
		}
		return bean;
	}
}

好处是:对象创建、组装、管理完全有 DI 容器来负责,跟具体业务代码解耦

Action:递归调用可能会导致了循环依赖,Spring 如何解决 A 和 B 对象的循环引用?
(1)只能处理单例的、setter 注入的循环依赖,其他的注入模式无法处理;
(2)依赖缓存处理循环依赖,关键思想是,将正在创建中的对象提前暴露一个单例工厂,让其他实例可以引用到。
可以参考这篇文章:Spring 源码学习(五)循环依赖


8.3、Builder 设计模式 将产品和产品建造过程解耦

定义Builder 模式用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。将复杂对象的建造过程抽象出来。

Builder模式的四个角色
①Product(产品角色):一个具体的产品对象
②Builder(抽象建造者):创建一个Product对象的各个部件指定的 接口/抽象类
③ConcreteBuilder(具体建造者): 实现接口,构建和装配各个部件。
④Director(指挥者): 构建一个使用Builder接口的对象。它主要是用于创建一个复杂的对象。它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。

使用场景
之前的做法:构建对象时必填项使用有参构造函数,非必填属性使用 set() 方法
现在
1、当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数;
2、类的属性之间有一定的依赖关系或者约束条件;
3、如果我们希望创建不可变对象,也就是说,不能在类中暴露 set() 方法。

Demo1:Builder 模式如何使用

public class ResourcePoolConfig {
	private String name;
	// 最大资源数
	private int maxTotal;
	// 最大空闲资源数
	private int maxIdle;
	// 最小空闲资源数
	private int minIdle;
	private ResourcePoolConfig(Builder builder) {
		this.name = builder.name;
		this.maxTotal = builder.maxTotal;
		this.maxIdle = builder.maxIdle;
		this.minIdle = builder.minIdle;
	}
	//...省略getter方法...
	//我们将Builder类设计成了ResourcePoolConfig的内部类。
	public static class Builder {
		private static final int DEFAULT_MAX_TOTAL = 8;
		private static final int DEFAULT_MAX_IDLE = 8;
		private static final int DEFAULT_MIN_IDLE = 0;
		private String name;
		private int maxTotal = DEFAULT_MAX_TOTAL;
		private int maxIdle = DEFAULT_MAX_IDLE;
		private int minIdle = DEFAULT_MIN_IDLE;
		public Builder() {}
		public ResourcePoolConfig build() {
			// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
			if (StringUtils.isBlank(name)) {
				throw new IllegalArgumentException("...");
			}
			if (maxIdle > maxTotal) {
				throw new IllegalArgumentException("...");
			}
			if (minIdle > maxTotal || minIdle > maxIdle) {
				throw new IllegalArgumentException("...");
			} 
			return new ResourcePoolConfig(this);
		} 
		public Builder setName(String name) {
			if (StringUtils.isBlank(name)) {
				throw new IllegalArgumentException("...");
			}
			this.name = name;
			return this;
		} 
		public Builder setMaxTotal(int maxTotal) {
			if (maxTotal <= 0) {
				throw new IllegalArgumentException("...");
			}
			this.maxTotal = maxTotal;
			return this;
		} 
		public Builder setMaxIdle(int maxIdle) {
			if (maxIdle < 0) {
				throw new IllegalArgumentException("...");
			}
			this.maxIdle = maxIdle;
			return this;
		}	 
		public Builder setMinIdle(int minIdle) {
			if (minIdle < 0) {
				throw new IllegalArgumentException("...");
			}
			this.minIdle = minIdle;
			return this;
		}
	} 
}
// Builder 模式的使用
// 符合面向对象的封装原则
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
		.setName("dbconnectionpool")
		.setMaxTotal(16)
		.setMaxIdle(10)
		.setMinIdle(12)
		.build();

Demo2 借助 Lombok 中的 @Builder 注解实现建造者模式

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ZcyStandardTransferLog implements Serializable {
    /** 自定义主键*/
    private Long id;
    /** 迁移相关id spuId*/
    private Long relatedId;
    /** 枚举值: */
    private Integer type;
    /**枚举值:0 offLine 下架;1 freeze 冻结;2 delete 删除 3 上架 online*/
    private Integer operateType;
    /** 日志详情*/
    private String detail;
    /** 创建人id*/
    private Long creatorId;
    /** 创建时间*/
    private Date createdAt;
}

创建了一个名为 ZcyStandardTransferLogBuilder 的静态内部类, 并且具有和实体类相同的属性(称为构建器).
1: 对于目标类中的所有的属性, 都会在构建器中创建对应的属性.
2: 创建一个无参的default构造方法.
3: 对于实体类中的每个参数, 都会对应创建类似于setter方法, 但是方法名是与该参数名是相同的, 并且返回值是构建器本身(便于链式调用).
4: 一个build方法, 调用此方法, 就会根据设置的值进行创建对象实例.
5: 同时也会生成一个toString() 方法.
6: 会创建一个builder()方法, 它的目的是用来创建构建器.

补充:builder中的常用注解

Demo3 Builder 模式在JDK的应用和源码分析

java.lang.StringBuilder中的建造者模式,将append()逻辑放在抽象父类中,然后返回this

@Override
public StringBuilder append(String str) {
    super.append(str);
    return this;
}
// StringBuilder 的父类
public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}
// AbstractStringBuilder 的接口
// 因此可以很方便地替换具体建造者或增加新的具体建造者, 用户使用不同的具体建造者即可得到不同的产品对象
public interface Appendable {
	Appendable append(CharSequence csq) throws IOException;
	Appendable append(CharSequence csq, int start, int end) throws IOException;
	Appendable append(char c) throws IOException;
}

// StringBuilder 的使用
stringBuilder.append("属性项【").append(entry.getKey()).append("】长度不允许超出 (").append(entry.getValue()).append(")字符,");

工厂模式和 Builder 模式的区别?

  • 工厂模式是用来创建不同但是相关类型的对象,由给定的参数决定创建哪种类型的对象;
  • Builder 模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象

补充 不可变模式

不可变模式定义
普通的不变模式指的是,对象中包含的引用对象是可以改变的
深度不变模式指的是,对象包含的引用对象也不可变

它们两个之间的关系,有点类似之前讲过的浅拷贝和深拷贝之间的关系

使用场景:如果一个对象符合创建之后就不会被修改这个特性,那我们就可以把它设计成不变类。
因为数据不变,所以不存在并发读写问题,因此不变模式常用在多线程环境下,来避免线程加锁。

// 普通不变模式
public class User {
	private String name;
	private int age;
	private Address addr;
	public User(String name, int age, Address addr) {
		this.name = name;
		this.age = age;
		this.addr = addr;
	}
	// 只有getter方法,无setter方法...
} 
public class Address {
	private String province;
	private String city;
	public Address(String province, String city) {
		this.province = province;
		this.city= city;
	}
	// 有getter方法,也有setter方法...
}


// 深度不变模式
public class User {
	private String name;
	private int age;
	private Address addr;
	public User(String name, int age, Address addr) {
		this.name = name;
		this.age = age;
		this.addr = addr;
	}
	// 只有getter方法,无setter方法...
} 
public class Address {
	private String province;
	private String city;
	public Address(String province, String city) {
		this.province = province;
		this.city= city;
	}
	// 只有getter方法,无setter方法..
}


```java
public class ImmutableDemo {
	public static void main(String[] args) {
		List<String> originalList = new ArrayList<>();
		originalList.add("a");
		originalList.add("b");
		originalList.add("c");
		List<String> jdkUnmodifiableList = Collections.unmodifiableList(originalList);
		List<String> guavaImmutableList = ImmutableList.copyOf(originalList);
		//jdkUnmodifiableList.add("d"); // 抛出UnsupportedOperationException
		// guavaImmutableList.add("d"); // 抛出UnsupportedOperationException
		originalList.add("d");
		print(originalList); // a b c d
		print(jdkUnmodifiableList); // a b c d
		print(guavaImmutableList); // a b c
	} 
	private static void print(List<String> list) {
      	String join = Joiner.on(",").skipNulls().join(list);
        System.out.println(join);
    }
}

集合中的对象不会增删,但是对象的成员变量(或叫属性值)是可以改变的
原因如下:JDK 的不变集合相当于对原集合采用装饰者模式,即通过组合方式限制掉原集合的写操作,所以在原始集合类发生改变的时候它也会改变,而 Google Guava 的不变集合,是重新创建了一个原始集合对象的副本,所以改变原始类并不能改变它的数据,也更加符合语义。在日常使用时,需要注意这一点。


8.4、原型设计模式的深拷贝和浅拷贝是什么,并写出深拷贝的两种方式的源码

概念如果对象的创建成本比较大(复杂的RPC/IO计算),而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的这种基于原型来创建对象的方式就叫作原型模式

Java中:Java中Object类是所有类的根类, Object类提供了一个clone()方法,该方法可以
将一个Java对象复制一份,但是需要实现clone的Java类必须要实现一个接口Cloneable,
该接口表示该类能够复制且具有复制的能力。 (浅拷贝,不实用

原型模式的实现方式–深拷贝和浅拷贝:浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。
深拷贝实现方案:1、使用Spring的BeanUtils工具类(原理是Java的反射语法) 2、使用Json序列化工具(推荐)

Demo1:重写Object的clone方法实现深拷贝,使用序列化来实现深拷贝

public class DeepProtoType implements Serializable, Cloneable {
	public String name; //String 属性
	public DeepCloneableTarget deepCloneableTarget;// 引用类型
	public DeepProtoType() {
		super();
	}

	//深拷贝 - 方式 1 使用clone 方法
	@Override
	protected Object clone() throws CloneNotSupportedException {
		Object deep = null;
		//这里完成对基本数据类型(属性)和String的克隆
		deep = super.clone(); 
		//对引用类型的属性,进行单独处理
		DeepProtoType deepProtoType = (DeepProtoType)deep;
		deepProtoType.deepCloneableTarget  = (DeepCloneableTarget)deepCloneableTarget.clone();
		return deepProtoType;
	}
	
	//深拷贝 - 方式2 通过对象的序列化实现 (推荐)
	public Object deepClone() {
		//创建流对象
		ByteArrayOutputStream bos = null;
		ObjectOutputStream oos = null;
		ByteArrayInputStream bis = null;
		ObjectInputStream ois = null;
		
		try {
			//序列化
			bos = new ByteArrayOutputStream();
			oos = new ObjectOutputStream(bos);
			oos.writeObject(this); //当前这个对象以对象流的方式输出
			//反序列化
			bis = new ByteArrayInputStream(bos.toByteArray());
			ois = new ObjectInputStream(bis);
			DeepProtoType copyObj = (DeepProtoType)ois.readObject();
			return copyObj;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		} finally {
			//关闭流
			try {
				bos.close();
				oos.close();
				bis.close();
				ois.close();
			} catch (Exception e2) {
				// TODO: handle exception
				System.out.println(e2.getMessage());
			}
		}
	}	
}

//深拷贝工具1 springframework BeanUtil  原理:反射
BeanUtils.copyProperties(source, target, "id", "updatedAt", "updatedId", "updatedName");

//深拷贝工具2 Dozer工具
List<AttachmentDTO> attachmentDtos = DozerBeanUtil.convertList(xxx.getAttachments(), attachmentDTO.class);

//深拷贝工具3 AnyBeanCopy工具  原理:json序列化   推荐
Person personCopy = AnyBeanCopyUtils.convert(person, Person.class);

2、请使用UML类图画出原型模型核心角色? 原理结构图
在这里插入图片描述
1)原型类,声明一个克隆自己的接口
2) ConcretePrototype: 具体的原型类, 实现一个克隆自己的操作
3) Client: 让一个原型对象克隆自己,从而创建一个新的对象(属性一样)

Action:原型设计模式和 Spring 原型区别在哪? 面试题

区别 Spring GOF
对象类型 根据Bean定义来创建对象 用原型实例指定创建对象类型
创建方式 根据Bean定义创建对象 通过拷贝原型创建对象
友好方式 非侵入式 侵入式

Demo2 Spring 框架哪些地方使用了原型模式,并对源码进行分析?
beans.xml

<bean id="id01" class="com.spring.bean.Monster" scope="prototype"/>
public void main (){
	ApplicationContext applicationContext = newClassPathXmlApplicationContext("beans.xml");
	//获取monster[通过id获取monster]
	Object bean = applicationContext.getBean("id01");
	System.out.println("bean" + bean);
}

// 在源码的 doGetBean 方法里面进行了判断
else if (mbd.isPrototype()) {
	// It's a prototype -> create a new instance.
	Object prototypeInstance = null;
	try {
		beforePrototypeCreation(beanName);
		// 进入了原型模式的对象创建
		prototypeInstance = createBean(beanName, mbd, args);
	}
	finally {
		afterPrototypeCreation(beanName);
	}
	bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

原型模式使用踩坑
1、不要使用Common包里面的BeanUtils工具类
2、在日常开发中,注意对象里面的字段被修改的情况,使用深拷贝避免该问题。

创建型设计模式总结:

Action:使用双重锁校验的单例模式时,使用需要在成员变量前加上 volatile 关键字?

  • 暂时在成员变量里加上 volatile,防止指令重排,确保没有问题。

你可能感兴趣的:(Java,设计模式详解,java,开发语言,后端,设计模式,面试)