1、七大软件设计原则
个人认为,结合到现实开发中,组件化和模块化编程的原理本质上就是七大设计原则,其目的就是可以根据不同的业务低开销的将组件(method、class、service等)组装在一起,将通用贯彻到底。
- 方法组件(Method:getUser(String idCardNo)):入参尽量满足迪米特法则,不必传用不到的参数,比如由idCardNo就已经能命中一个用户,就不必再传userName了。功能要满足单一职责原则,专人干专事,不要插足getUser以外的事情。
- 类(Class:User):功能上要满足开闭原则;还要满足单一职责,不必插足Order的事。NewUser、OldUser继承自User,也许新老用户特权方法不同,但本质上还是User,这是里氏替换原则。
- 基础业务模块(Interface:UserService):需要依赖倒置原则对可能存在的功能进行定义,不该管的不管,既是满足单一职责原则,又是满足接口隔离原则。在具体实例化类使用的时候使用依赖倒置;
- 复杂业务模块(集成多个Service):有些业务,比如消费特征分析业务(FeatureService),既要处理User,又要处理Order。那就使用合成复用原则整合UserService、OrderService。
- 微服务同样也适用最少知道原则,比如只有Order微服务能做订单的crud,其他调用微服务仅能查询订单数据。
综上,七大原则在组件化和模块化中的使用,保证了入参、方法、类、业务、模块的纯净,就像26个字母,彼此独立,但又能组合成不同的单词,可以提高代码的复用性,以及编程的灵活度,降低业务耦合,和更亲民的可读性。
开闭原则,扩展开放,修改关闭
依赖倒置原则
单一职责
接口隔离
迪米特法则(最少知道原则)
里实体换,杜绝继承泛滥,子类可以扩展父类的功能,但不能覆盖、修改父类的功能
合成复用
2、设计模式总览及工厂模式
2.1、简单java代码中使用
public interface IPay { // 定义支付的基本功能 void pay(); } public class AliPay implements IPay { // 不同支付渠道对于支付功能的具体实现 @Override public void pay() { System.out.println("阿里支付"); } } public class UnionPay implements IPay { @Override public void pay() { System.out.println("银联支付"); } } public class WeixinPay implements IPay { @Override public void pay() { System.out.println("微信支付"); } } public class PayFactory { // 支付渠道的工厂,根据类类型创建指定支付渠道的对象 public static IPay get(Class clazz) throws IllegalAccessException, InstantiationException { return (IPay) clazz.newInstance(); } } public class Test { public static void main(String[] args) throws InstantiationException, IllegalAccessException { PayFactory.get(AliPay.class).pay(); // 使用支付渠道 PayFactory.get(WeixinPay.class).pay(); PayFactory.get(UnionPay.class).pay(); } }
2.2、结合spring使用
public interface IPay { // 定义支付的基本功能 void pay(); } @Service public class AliPay implements IPay { @PostConstruct private void init() { // @PostConstruct在bean创建后执行 PayFactory.init(this); } @Override public void pay() { // 不同支付渠道对于支付功能的具体实现 System.out.println("阿里支付"); } } @Service public class UnionPay implements IPay { @PostConstruct private void init() { PayFactory.init(this); } @Override public void pay() { System.out.println("银联支付"); } } @Service public class WeixinPay implements IPay { @PostConstruct private void init() { PayFactory.init(this); } @Override public void pay() { System.out.println("微信支付"); } } public class PayFactory { private static final Map<String, IPay> PAYCHANNEL = new HashMap<>(); // 渠道池 public static void init(IPay iPay) { // 定义初始化方法,将渠道实例注入渠道池 PAYCHANNEL.put(iPay.getClass().getName(), iPay); } public static IPay get(String className) { // 获取渠道实例 return PAYCHANNEL.get(className); } } @SpringBootTest @RunWith(SpringJUnit4ClassRunner.class) public class IPayTest { @Test public void pay() { PayFactory.get(AliPay.class.getName()).pay(); // 应用 } }
3、单例模式(深度)
学习目标:
- 单例模式的应用场景
- Idea下的多线程调试方式
- 保证线程安全的单例模式策略
- 反射包里攻击单例的解决方案及原理
- 序列化破坏单例的原理及解决方案
- 常见的单例模式写法
单例模式Singleton Pattern是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点(getIntence)。
隐藏其所有的构造方法(私有化)。属于创建型模式。
3.1、单例的适用场景
确保任何情况下都绝对只有一个实例
ServletContext(servlet上下文)、ServletConfig、ApplicationContext、DBPool
3.2、饿汉式
类加载,字段声明时就赋值。
优点:执行效率高,性能高。简单,没有锁.
缺点:1、类加载时初始化,不管用不用都要初始化,可能会导致内存浪费。如果有大量需要单例的类,则不适合
3.2.1、普通式
public class HungarySingleton { // 只有一个实例 public static final HungarySingleton HUNGARY_SINGLETON = new HungarySingleton(); private HungarySingleton() { } // 私有化构造方法 public static HungarySingleton getInstance() { // 全局访问点 return HUNGARY_SINGLETON; } }
3.2.1、静态代码块赋值
public class HungaryStaticSingleton { // 只有一个实例 public static final HungaryStaticSingleton HUNGARY_SINGLETON; // 先静态后动态,先上后下,先属性后方法 static { // 静态的代码块,在类加载时初始化 HUNGARY_SINGLETON = new HungaryStaticSingleton(); } private HungaryStaticSingleton() { } // 私有化构造方法 public static HungaryStaticSingleton getInstance() { // 全局访问点 return HUNGARY_SINGLETON; } }
3.3、懒汉式
被外部类调用时才创建实例,不用的时候只声明,不创建。解决了饿汉浪费内存的问题。
优点:节省内存,
缺点:线程安全不稳定
同一个实例:
1、正常顺序执行
2、后者覆盖前者
不同的实例:
3.3.1、普通式
/** * 优点:解决内存浪费的问题 * 缺点:线程不安全 */ public class LazySimpleSingleton { // 只有一个实例 public static LazySimpleSingleton LAZY_SIMPLE_SINGLETON; private LazySimpleSingleton() { } // 私有化构造方法 public static LazySimpleSingleton getInstance() { // 全局访问点 if (null == LAZY_SIMPLE_SINGLETON) { // 5 LAZY_SIMPLE_SINGLETON = new LazySimpleSingleton(); // 6 } return LAZY_SIMPLE_SINGLETON; // 7 } } public class ExectorThread implements Runnable { @Override public void run() { LazySimpleSingleton instance = LazySimpleSingleton.getInstance(); // 3 System.out.println(Thread.currentThread().getName() + ":" + instance); // 4 } } public class LazyTest { public static void main(String[] args) { Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); // 有资格抢CPU资源了 t2.start(); System.out.println("End"); } }
运行结果
同一个实例
1、正常顺序执行:t1.3 -> t1.5 -> t1.6 -> t1.7 -> t1.4 -> t2.3 -> t2.5 -> t2.7 -> t2.4
2、创建了两个实例,但后者覆盖前者:t1.3 -> t1.5(实例为空,t1进if) -> t2.3 -> t2.5(实例为空,t2进if) -> t1.6 -> t2.6 -> t1.7 -> t2.7(t1、t2分别创建实例,但t1虽先创建,但没返回,导致t2覆盖了t1创建的实例) -> t2.4 -> t1.4
不同的实例
同时进入了条件,但按顺序返回:t1.3 -> t1.5(实例为空,t1进if) -> t2.3 -> t2.5(实例为空,t2进if) -> t1.6 -> t1.7 -> t1.4 -> t2.6(t1、t2分别创建实例) -> t2.7 -> t2.4
3.3.2、方法锁式
解决普通式线程不安全的问题
public class LazyMethodLockSingleton { // 只有一个实例 public static LazyMethodLockSingleton LAZY_SIMPLE_SINGLETON; private LazyMethodLockSingleton() { } // 私有化构造方法 public static synchronized LazyMethodLockSingleton getInstance() { // 加方法锁,同时只能有一个线程getInstance if (null == LAZY_SIMPLE_SINGLETON) { LAZY_SIMPLE_SINGLETON = new LazyMethodLockSingleton(); } return LAZY_SIMPLE_SINGLETON; } }
方法锁的粒度大,又导致了新问题:加锁后性能变低,同时只能有一个线程getInstance。
3.3.3、双重检查锁式
优点:解决了方法锁的性能问题。线程安全。
缺点:两个判断不优雅。
注意:
- 两次判断的作用
- 会有指令重排序的问题
双重检查锁的指令重排,使用volatile
关键字,volatile
关键字严格遵循happens-before
原则,即在读操作前,写操作必须全部完成。
public class LazyDoubleCheckSingleton { // 只有一个实例 public volatile static LazyDoubleCheckSingleton LAZY_SIMPLE_SINGLETON; private LazyDoubleCheckSingleton() { } // 私有化构造方法 public static LazyDoubleCheckSingleton getInstance() { // 全局访问点 if (null == LAZY_SIMPLE_SINGLETON) { // 第一次检查是否要阻塞 synchronized (LazyDoubleCheckSingleton.class) { if (null == LAZY_SIMPLE_SINGLETON) { // 第二次检查是否创建实例 LAZY_SIMPLE_SINGLETON = new LazyDoubleCheckSingleton(); // 可能会出现指令重排序的问题。线程环境要抢时间片,有一定的随机性,在这一步又要分配实例的内存地址, // 也要分配变量的内存地址,可能出现先后问题,导致线程紊乱。所以要加volatile } } } return LAZY_SIMPLE_SINGLETON; } }
3.3.4、静态内部类式
优点:写法优雅,利用了java的特点,线程安全,避免了内存浪费,性能高
缺点:反射可以破坏
public class LazyStaticInnerClassSingleton { private LazyStaticInnerClassSingleton() { // 私有化构造方法 } public static LazyStaticInnerClassSingleton getInstance() { // 全局访问点 return LazyHolder.LAZY_STATIC_INNER_CLASS_SINGLETON; } private static class LazyHolder { // 成员变量在加载的时候就要被分配空间,静态内部类在用的时候才分配内存。 private static final LazyStaticInnerClassSingleton LAZY_STATIC_INNER_CLASS_SINGLETON = new LazyStaticInnerClassSingleton(); } }
3.3.4.1、反射破坏
public class ReflectTest { public static void main(String[] args) { try { // 可以拿到类对象,并且反射到构造方法 Class clazz = LazyStaticInnerClassSingleton.class; Constructor c = clazz.getDeclaredConstructor(null); c.setAccessible(true); // c.newInstance()可以任意多次使用,破坏单例只有一个全局访问点的定义 Object instance1 = c.newInstance(); Object instance2 = c.newInstance(); // 可以创建多个实例,破坏单例任何情况下都绝对只有一个实例的原则 System.out.println(instance1); System.out.println(instance2); System.out.println(instance1 == instance2); } catch (Exception e) { e.printStackTrace(); } } } /* 结果: com.ykq.singleton.lazy.LazyStaticInnerClassSingleton@14ae5a5 com.ykq.singleton.lazy.LazyStaticInnerClassSingleton@7f31245a false */
3.3.4.2、防止反射破坏
在构造方法做一层实例非空的判断,如果实例存在,则抛出异常,强行阻止实例的多次创建。
缺点:不优雅
public class LazySafeStaticInnerClassSingleton { private LazySafeStaticInnerClassSingleton() { // 私有化构造方法 if (null != LazyHolder.LAZY_STATIC_INNER_CLASS_SINGLETON) { throw new RuntimeException("不允许非法访问"); } } public static LazySafeStaticInnerClassSingleton getInstance() { // 全局访问点 return LazyHolder.LAZY_STATIC_INNER_CLASS_SINGLETON; } private static class LazyHolder { // 成员变量在加载的时候就要被分配空间,静态内部类在用的时候才分配内存。 private static final LazySafeStaticInnerClassSingleton LAZY_STATIC_INNER_CLASS_SINGLETON = new LazySafeStaticInnerClassSingleton(); } }
3.4、注册式
将每一个实例都缓存到统一的容器中,使用唯一标识获取实例
3.4.1、枚举式单例
相当于继承Eunm。
因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式。
枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
虽然是单例,但set方法是public,会导致Instance的值可随意修改。
/** 创建 */ public enum EnumSingleton { INSTANCE; private Object data; public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance() { return INSTANCE; } } /** 使用 */ public class EnumTest { public static void main(String[] args) { EnumSingleton instance = EnumSingleton.getInstance(); instance.setData(new Object()); } }
3.4.1.1、枚举式单例为什么不会被反射破坏
反射代码,但会报错,如下。
public class ReflectTest { public static void main(String[] args) { try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(String.class, int.class); c.setAccessible(true); Object instance1 = c.newInstance(); System.out.println(instance1); } catch (Exception e) { e.printStackTrace(); } } }
jdk的底层已经规定,不能通过反射创建枚举对象
3.4.1.2、枚举式单例为什么是注册式的
当枚举声明的时候,就会当做enumConstantDirectory的常量存起来。所以,也有问题,可能会造成内存浪费,不适合大量单例使用的场景。
/** Class.class */ // 相当于定义了一个容器 private volatile transient Map<String, T> enumConstantDirectory = null; Map<String, T> enumConstantDirectory() { if (enumConstantDirectory == null) { T[] universe = getEnumConstantsShared(); if (universe == null) throw new IllegalArgumentException( getName() + " is not an enum type"); Map<String, T> m = new HashMap<>(2 * universe.length); for (T constant : universe) m.put(((Enum<?>)constant).name(), constant); enumConstantDirectory = m; } return enumConstantDirectory; } /** Eunm.class */ public static <T extends Enum<T>> T valueOf(Class<T> enumType, String 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); }
3.4.2、容器式单例(参考IoC容器思想)
由于枚举式单例不能大量的创建对象,所有有了容器式单例。枚举式的衍生,保证了实例是单例。
// Spring的bean也是单例的 public class ContainerSngleton { private static Map<String, Object> ioc = new ConcurrentHashMap<>(); public static Object getInstance(String className) { if (!ioc.containsKey(className)) { try { ioc.put(className, Class.forName(className)); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return ioc.get(className); } } public class ContainerTest { public static void main(String[] args) { Object instance1 = ContainerSngleton.getInstance("com.ykq.singleton.registe.Pojo"); Object instance2 = ContainerSngleton.getInstance("com.ykq.singleton.registe.Pojo"); System.out.println(instance1); System.out.println(instance2); } }
也有问题,容器下周末解决线程安全的问题。
3.5、ThreadLocal单例(线程局部变量)
基于线程的,在一个线程内是同一个对象,跨线程的是不同的对象。具有局限性的单例模式。
public class ThreadLocalSingleton { public static final ThreadLocal<ThreadLocalSingleton> THREAD_LOCAL_INSTANCE = new ThreadLocal<ThreadLocalSingleton>() { @Override protected ThreadLocalSingleton initialValue() { return new ThreadLocalSingleton(); } }; private ThreadLocalSingleton() {} public static ThreadLocalSingleton getInstance() { return THREAD_LOCAL_INSTANCE.get(); } } public class ExectorThread implements Runnable { @Override public void run() { ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance(); System.out.println(Thread.currentThread().getName() + ":" + instance); } } public class ThreadLocalTest { public static void main(String[] args) { System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); // 有资格抢CPU资源了 t2.start(); System.out.println("End"); } }
为什么说是线程内是同一个对象的原理:get中的map.getEntry()所传的this,相当于当前线程的信息。所以,可以理解为时每个线程有自己的一个实例。
3.6、序列化破坏单例
序列化破坏单例。通过序列化、反序列化就能创建出多个内容一样的实例,破坏单例任何情况下都绝对只有一个实例的原则。
代码如下
public class SerializableSingleton implements Serializable { // 序列化:把内存中的对象转为字节码,再把字节码通过IO输出流写到磁盘上,让它永久保存下来。 // 反序列化:将持久化的字节码内容,通过IO输入流读到内存,再把字节码转为对象 private final static SerializableSingleton INSTANCE = new SerializableSingleton(); private SerializableSingleton() { } public static SerializableSingleton getInstance() { return INSTANCE; } } public class SerializableTest { public static void main(String[] args) { SerializableSingleton s1 = null; SerializableSingleton s2 = SerializableSingleton.getInstance(); try { FileOutputStream fos = new FileOutputStream("SerializableSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SerializableSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (SerializableSingleton) ois.readObject(); ois.close(); System.out.println(s1); System.out.println(s2); System.out.println(s1 == s2); } catch (Exception e) { e.printStackTrace(); } } }
解决方案,在SerializableSingleton中增加readResolve(),如下
public class SerializableSingleton implements Serializable { // 序列化:把内存中的对象转为字节码,再把字节码通过IO输出流写到磁盘上,让它永久保存下来。 // 反序列化:将持久化的字节码内容,通过IO输入流读到内存,再把字节码转为对象 private final static SerializableSingleton INSTANCE = new SerializableSingleton(); private SerializableSingleton() { } public static SerializableSingleton getInstance() { return INSTANCE; } // 桥接模式 private Object readResolve() { return INSTANCE; } }
原理:如果被readObject的对象里有readResolve,就返回readResolve的值
ois.readObject(); --> readObject0(false); --> readOrdinaryObject(unshared) --> hasReadResolveMethod() --> invokeReadResolve(obj) --> return readResolveMethod.invoke(obj, (Object[]) null);
3.7、原码中的使用
Mybatis的ErrorContext
AbstractFactoryBean
3.8、总结
- 优点
在内存中只有一个实例,减少了内存的开销
可以避免对资源的多重占用
设置全局访问点,严格控制访问
- 缺点
没有接口,扩展困难。如果要扩展单例对象,只有修改代码,没有其他途径。(违背开闭原则)
- 特点
私有化构造器
保证线程安全
延迟加载
防止序列化和反序列化破坏单例
防御反射攻击单例
- 反射破坏的场景
1、简单懒汉单例模式下,且实例未创建,多个线程同时请求getInstance,可能会导致多个线程同时通过判断,导致多次创建实例。破坏单例任何情况下都绝对只有一个实例的原则。
2、静态内部类式前面的都可以被反射破坏,
因为都能通过反射拿到构造方法。破坏单例隐藏其所有的构造方法的原则;
c.newInstance()可以任意多次使用,破坏单例只有一个全局访问点的原则;
可以创建多个实例,破坏单例任何情况下都绝对只有一个实例的原则。
3、枚举序列化、反序列化就能创建出多个内容一样的实例,破坏单例任何情况下都绝对只有一个实例的原则。可以在类中写readResolve()方法避免。
4、ThreadLocal中,基于线程的实例是一个对象,但不同线程的实例是不同的对象。
- 单例的生命周期是怎么样的?单例会不会被GC回收?怎么样才能让单例被回收?
单例不回收
4、原型模式
目标:
原型模式应用场景
原型模式的浅克隆与深克隆的写法
了解克隆如何破坏单例
了解原型模式的优缺点
定义:
1、原型模式(Prototype Pattern)是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象(就是复制,且把复制的过程封装起来)。
2、调用者不需要知道如任何创建细节,不调用构造函数。
3、属于创建型模式。
适用场景:
1、类初始化消耗资源较多。
2、new产生一个对象需要非常繁琐的过程(数据准备-set,访问权限等)。
3、构造函数比较复杂。
4、循环体中产生大量对象时。
原型的目的:用一个可复用的方法,省略手动构造另一个对象的工作
复用某一对象的属性值时,创建对象很麻烦
可以这样,但是硬编码
BeanUtils。copy。跟上面没有本质的变化,底层也是set。
4.1、通用写法
接口,抽象原型,方法clone
具体原型实现clone。(set硬,可以用BeanUtil)
// 抽象原型 public interface IPrototype { Object clone(); } // 具体的原型 public class ConcretePrototype implements IPrototype { private Integer id; private Integer sort; @Override public ConcretePrototype clone() { ConcretePrototype prototype = new ConcretePrototype(); prototype.setId(id); prototype.setSort(sort); return prototype; } } public class GeneralTest { public static void main(String[] args) { ConcretePrototype c1 = new ConcretePrototype(); c1.setId(1); c1.setSort(1); System.out.println(c1); ConcretePrototype c2 = c1.clone(); System.out.println(c2); } }
类图:
4.2、浅克隆
实现Cloneable接口,由于Cloneable基类Object有native修饰的clone()方法,所以Cloneable接口不必再定义一个clone()方法,符合最少知道原则。native修饰后是底层字节码的克隆。
native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。
public class ConcretePrototype implements Cloneable { private Integer id; private String sort; private List<String> books; @Override public ConcretePrototype clone() { try { return (ConcretePrototype) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } } public class GeneralTest { public static void main(String[] args) { ConcretePrototype c1 = new ConcretePrototype(); c1.setId(1); c1.setSort("sort"); List<String> books = new ArrayList<>(); books.add("a"); books.add("b"); c1.setBooks(books); ConcretePrototype c2 = c1.clone(); c2.getBooks().add("c"); System.out.println("克隆对象:" + c2); System.out.println("原型对象:" + c1); System.out.println(c1 == c2); System.out.println("克隆对象book:" + c2.getBooks()); System.out.println("原型对象book:" + c1.getBooks()); System.out.println(c1.getBooks() == c2.getBooks()); } } 运行结果: 克隆对象:ConcretePrototype{id=1, sort='sort', books=[a, b, c]} 原型对象:ConcretePrototype{id=1, sort='sort', books=[a, b, c]} false 克隆对象book:[a, b, c] 原型对象book:[a, b, c] true
可见,原型对象和克隆对象的地址不一样,但公用了books的地址。因为,在原生对象中传递的是值,引用对象传递的是地址的值。可见浅克隆修改了原生对象,造成安全隐患,这种情况引出深克隆。
类图:
4.2、深克隆
在单例破坏中,序列化会导致创建不同的实例,所以需要在单例类里重写readResolve,才能防止重复创建对象。
基于这个原理,可以实现深克隆。
实现深克隆,可用序列化创建对象;也可用json;也可原生对象clone完成之后,再特殊处理一下引用传值的属性(此方式麻烦)。
public class ConcretePrototype implements Serializable { private Integer id; private String sort; private List<String> books; public ConcretePrototype deepClone() { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return (ConcretePrototype) ois.readObject(); } catch (Exception e) { e.printStackTrace(); } return null; } } public class GeneralTest { public static void main(String[] args) { ConcretePrototype c1 = new ConcretePrototype(); c1.setId(1); c1.setSort("sort"); List<String> books = new ArrayList<>(); books.add("a"); books.add("b"); c1.setBooks(books); ConcretePrototype c2 = c1.deepClone(); c2.getBooks().add("c"); System.out.println("克隆对象:" + c2); System.out.println("原型对象:" + c1); System.out.println(c1 == c2); System.out.println("克隆对象book:" + c2.getBooks()); System.out.println("原型对象book:" + c1.getBooks()); System.out.println(c1.getBooks() == c2.getBooks()); } } 运行结果: 克隆对象:ConcretePrototype{id=1, sort='sort', books=[a, b, c]} 原型对象:ConcretePrototype{id=1, sort='sort', books=[a, b]} false 克隆对象book:[a, b, c] 原型对象book:[a, b] false
经过验证,可以防止篡改原型对象。
类图:
4.3、克隆破坏单例
克隆本质是绕过了构造方法创建对象,当原型是单例时,克隆会破坏单例。
public class ConcretePrototype implements Cloneable { private static final ConcretePrototype INSTANCE = new ConcretePrototype(); private ConcretePrototype() {} public static ConcretePrototype getInstance() { return INSTANCE; } @Override public ConcretePrototype clone() { try { return (ConcretePrototype) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } } public class GeneralTest { public static void main(String[] args) { ConcretePrototype c1 = ConcretePrototype.getInstance(); ConcretePrototype c2 = c1.clone(); System.out.println("克隆对象:" + c2); System.out.println("原型对象:" + c1); System.out.println(c1 == c2); } } // 运行结果 克隆对象:com.ykq.prototype.singleton.ConcretePrototype@14ae5a5 原型对象:com.ykq.prototype.singleton.ConcretePrototype@7f31245a false
可见,单例被克隆了,违背了单例在任何情况下都只有一个实例的定义,所以破坏了单例。
怎么解决上述问题?
1、单例和原型原则上不能共同存在。如果是单例就不能让他成为原型
2、可以实现Cloneable,但实现的clone()方法return INSTANCE;
4.4、克隆在源码中的应用
JDK
- ArrayList
- HashMap
4.5、总结
优点:
性能优良,Java自带的原型模式(clone)是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。
可以使用深克隆的方式保存对象的状态(内存中的值),使用原型模式将对象复制一份并将其状态保存起来,简化了创建过程。
缺点:
必须配备克隆(或者可拷贝)的方法。
当对原型类进行改造时,属性、逻辑变化时,需要修改调整拷贝的代码,违反开闭原则。
深浅拷贝要运用得当。
原型模式和单例模式不要在一起用。
5、建造者模式(Builder Pattern)
目标:
建造者模式应用场景
建造者模式和工厂模式的区别
定义:
建造者模式是将一个复杂对象的构建和它的表示分类,使得同样的构建过程可以创建不同的表示。属于创建者模式。
特征:
用户只需要指定需要建造的类型,就可以获得对象,建造过程及细节不必了解。
适用的场景:
适用于创建对象需要很多步骤,但步骤的顺序不一定。(原型模式的创建步骤不是开放的,而建造者是开放的)
如果对象有非常复杂的内部结构(很多属性)
把复杂对象的创建和使用分离。
建造者模式的本质是,要创建一个完整的对象,不需要有固定顺序为其属性赋值。
可用于求职网站的简历,个人信息、技能信息、工作经历、项目经验,可以分别构建。
5.1、简单写法
public class PersonInfo { private String name; private String work; private String address; } public interface IBuilder { Object build(); } public class ConcreteBuilder implements IBuilder { private PersonInfo personInfo = new PersonInfo(); public void addName(String name) { personInfo.setName(name); } public void addWork(String work) { personInfo.setWork(work); } public void addAddress(String address) { personInfo.setAddress(address); } @Override public Object build() { return personInfo; } } public class Test { public static void main(String[] args) { IBuilder builder = new ConcreteBuilder(); ((ConcreteBuilder) builder).addName("张三"); ((ConcreteBuilder) builder).addWork("工程师"); System.out.println(builder.build()); } } 运行结果: PersonInfo{name='张三', work='工程师', address='null'}
5.2、链式编程
上述例子在构建中,builder使用太多,可以用链式编程。
改造方法:在构建方法return this;
public class ConcreteBuilder implements IBuilder { private PersonInfo personInfo = new PersonInfo(); public ConcreteBuilder addName(String name) { personInfo.setName(name); return this; } public ConcreteBuilder addWork(String work) { personInfo.setWork(work); return this; } public ConcreteBuilder addAddress(String address) { personInfo.setAddress(address); return this; } @Override public Object build() { return personInfo; } }
5.3、建造者模式在源码中的使用
StringBuilder的append追加字符时:
Mybatis、Spring等等
5.4、总结
优点:
封装性好,创建和使用分离。
扩展性好,建造类之间独立,一定程度上解耦。
缺点:
必须多创建一个Builder对象,没有Builder也能创建出实例。
产品内部发生变化,建造者要修改其建造方法,改动成本大。
5.5、建造者模式和工厂模式的区别
1、 建造者注重方法的调用顺序,工厂注重创建对象。
2、创建对象的力度不同,建造者创建复杂的对象,由各种复杂的部件组成;工厂创建的对象都一样。
3、工厂模式只需要关注创建出对象即可,建造者不仅要创建出对象,还要知道有哪些部件组成。
4、建造者根据建造过程中的顺序、组件不同,最终的对象部件也可能不一样。
6、代理模式(Proxy Pattern)
目标:
掌握代理模式的应用场景和实现原理
了解静态代理和动态代理的区别
了解CGLib和JDK Proxy的根本区别
手写实现动态代理
定义:
代理模式(Proxy Pattern)是指为其他对象提供一种代理,以控制对这个对象的访问。
代理对象在客户端和目标对象之间起到中介作用。
属于结构型设计模式。
适用场景:
保护目标对象
增强目标对象
6.1、静态代理
示例:面向抽象编程,先定义人。儿子有饿了的动作,母亲代理儿子,还需要买菜做饭、刷锅洗碗。
代理目标、抽象角色(主题,淘宝商家)
public interface IPerson { void hungary(); } public class SonA implements IPerson { @Override public void hungary() { System.out.println("A饿了,吃饭"); } } public class ProxyMom implements IPerson { private IPerson subject; public ProxyMom(IPerson subject) { this.subject = subject; } @Override public void hungary() { before(); subject.hungary(); after(); } private void before() { System.out.println("A妈先,买菜做饭"); } private void after() { System.out.println("A妈后,刷锅洗碗"); } } public class Test { public static void main(String[] args) { ProxyMom mom = new ProxyMom(new Son()); mom.hungary(); } } 运行结果: 先,买菜做饭 饿了,吃饭 后,刷锅洗碗
但静态有局限性,1、如果儿子是张三、李四,则需要有张三妈、李四妈,即有其他类需要代理时,需要为每一个类配一个代理类。2、如果功能有扩展,before、after则需要修改代理类代码。因此,需要动态代理解决这些问题。
6.2、动态代理
底层的逻辑通过字节码重组为被代理的类生成一个新的类,使用时通过反射调用目标类,并且在此之前、之后可动态增加动作。
显示声明被代理对象
有功能扩展,或者有多个不同的类需要代理时可使用。
6.2.1、JDK Proxy
原类和生成的代理类是兄弟关系,因为代理类要继承目标类实现的接口。
public interface IPerson { void Hungary(); } public class SonA implements IPerson { @Override public void hungary() { System.out.println("A饿了,吃饭"); } } public class SonB implements IPerson { @Override public void hungary() { System.out.println("B饿了,吃饭"); } } /** 代理类不要求是Person的实现了 */ public class Restaurant implements InvocationHandler { private IPerson person; public IPerson getProxyPerson(IPerson person) { this.person = person; Class clazz = person.getClass(); return (IPerson) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result = method.invoke(this.person, args); after(); return result; } private void before() { System.out.println("饭店,买菜做饭"); } private void after() { System.out.println("饭店,刷锅洗碗"); } } public class Test { public static void main(String[] args) { Restaurant restaurant = new Restaurant(); IPerson personA = restaurant.getProxyPerson(new SonA()); personA.hungary(); IPerson personB = restaurant.getProxyPerson(new SonB()); personB.hungary(); } } 运行结果: 饭店,买菜做饭 A饿了,吃饭 饭店,刷锅洗碗 饭店,买菜做饭 B饿了,吃饭 饭店,刷锅洗碗
如果IPerson新增动作,比如打包,那么动态代理相较于静态代理,饭馆不需要修改。
6.2.2、CGLib Proxy
CGLib只要求代理类是个class就好,不用在代理类中声明IPerson的属性。JDK Proxy要求拿到被代理类的接口。
原类和生成的代理类是父子关系,因为生成的代理类extend 目标类。
public class Restaurant implements MethodInterceptor { // 不需要声明IPerson的私有变量了,SonA也不用实现IPerson了,因为跟JDKProxy比较,invoke时不需要传接口类型 public Object getProxyPerson(Class clazz) { // cglib生成字节码的工具 Enhancer enhancer = new Enhancer(); // 相当于继承父类clazz enhancer.setSuperclass(clazz); enhancer.setCallback(this); return enhancer.create(); } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { before(); Object result = methodProxy.invokeSuper(o, objects); after(); return result; } private void before() { System.out.println("饭店,买菜做饭"); } private void after() { System.out.println("饭店,刷锅洗碗"); } } public class Test { public static void main(String[] args) { Restaurant restaurant = new Restaurant(); SonA personA = (SonA) restaurant.getProxyPerson(SonA.class); personA.hungary(); SonB personB = (SonB) restaurant.getProxyPerson(SonB.class); personB.hungary(); } }
6.3、动态代理的原理
源码的区别
动态配置和替换被代理对象
工具ProxyGenerator拿到字节码,再反编译工具jad
Jad:http://java-decompiler.github.io/
6.3.1、手写JDK Proxy
掌握动态代理的原理:代码模拟JDK代理的过程,跟手写代码一样,写代码,编译存磁盘,加载JVM,创建实例。
由下面示例可见:代理模式本身就是代码写代码(生成java文件,编译,加载,创建实例,使用)。
public interface MyInvocationHandler { Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } public class MyClassLoader extends ClassLoader { private File classPathFiles; public MyClassLoader() { String classPath = MyClassLoader.class.getResource("").getPath(); this.classPathFiles = new File(classPath); } @Override public Class<?> findClass(String name) { String className = MyClassLoader.class.getPackage().getName() + "." + name; if (classPathFiles != null) { File classFile = new File(classPathFiles, name.replace("\\.", "/") + ".class"); if (classFile.exists()) { FileInputStream in = null; ByteArrayOutputStream out = null; try { in = new FileInputStream(classFile); out = new ByteArrayOutputStream(); byte[] buff = new byte[1024]; int len; while ((len = in.read(buff)) != -1) { out.write(buff, 0, len); } return defineClass(className, out.toByteArray(), 0, out.size()); } catch (Exception e) { e.printStackTrace(); } } } return null; } } public class MyProxy { public static final String LN = "\r\n"; public static Object newProxyInstance(MyClassLoader loader, Class<?>[] interfaces, MyInvocationHandler h) { try { // 1、动态生成源码.java文件。拼字符串 String src = generateSrc(interfaces); System.out.println("================================"); System.out.println(src); // 2、java文件输出到磁盘,保存为$Proxy0.java String filePath = MyProxy.class.getResource("").getPath(); File f = new File(filePath + "$Proxy0.java"); FileWriter fileWriter = new FileWriter(f); fileWriter.write(src); fileWriter.flush(); fileWriter.close(); // 3、把java文件编译成$Proxy0.class文件 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null); Iterable iterable = manager.getJavaFileObjects(f); JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, iterable); task.call(); manager.close(); // 4、把生成的class文件加载到JVM中 Class proxyClass = loader.findClass("$Proxy0"); Constructor c = proxyClass.getConstructor(MyInvocationHandler.class); // 5、返回新的代理对象 return c.newInstance(h); } catch (Exception e) { e.printStackTrace(); } return null; } private static String generateSrc(Class<?>[] interfaces) { StringBuilder sb = new StringBuilder(); sb.append("package com.ykq.proxy.dynamicproxy.myproxy.proxy;" + LN); sb.append("import com.ykq.proxy.dynamicproxy.myproxy.IPerson;" + LN); sb.append("import java.lang.reflect.*;" + LN); sb.append("import java.lang.reflect.*;" + LN); // sb.append("public final class $Proxy0 extends Proxy implements IPerson {") sb.append("public final class $Proxy0 implements " + interfaces[0].getName() +" {" + LN); sb.append("MyInvocationHandler h;" + LN); sb.append("public $Proxy0(MyInvocationHandler h) {" + LN); sb.append("this.h = h;" + LN); sb.append("}" + LN); for (Method m : interfaces[ 0].getMethods()) { Class<?>[] params = m.getParameterTypes(); StringBuilder paramNames = new StringBuilder(); StringBuilder paramValues = new StringBuilder(); StringBuilder paramClasses = new StringBuilder(); // for (int i = 0; i < params.length; i++) { // paramNames.append(params[i].getName()); // paramValues.append(params[i].get); // paramClasses.append(); // } sb.append("@Override" + LN); sb.append("public final " + m.getReturnType() + " " + m.getName() + "() {" + LN); sb.append(" try {" + LN); sb.append(" Method mn = " + interfaces[0].getName() + ".class.getMethod(\"" + m.getName() + "\", new Class[]{});" + LN); sb.append(" this.h.invoke(this, mn, new Class[]{});" + LN); sb.append(" return;" + LN); sb.append(" } catch (Error|RuntimeException error) {" + LN); sb.append(" throw null;" + LN); sb.append(" } catch (Throwable throwable) {" + LN); sb.append(" throw new UndeclaredThrowableException(throwable);" + LN); sb.append(" }" + LN); sb.append("}" + LN); } sb.append("}" + LN); return sb.toString(); } }
6.3.1、JDK Proxy与CGLib的异同
1、相同的思想:
通过生成字节码,重组一个新的类。
2、区别:
- CGLib采用继承的方式,覆盖父类的方法
JDK采用的实现的方式,要求代理的目标对象一定要实现一个接口
- CG 对目标类没有任何的要求
JDK,对于用户,依赖更强(必须要实现接口),调用跟复杂
- CG生成过程,效率更高,性能更高,底层没有用到反射
JDK生成的逻辑较为简单,但执行效率低,每次都要用反射(反射会消耗性能)
- CG的坑,目标类不能有final方法,因为会忽略final修饰的方法,因此不能被代理
6.4、总结
- 优点:
代理模式能将代理对象与真实被调用的目标对象分离。
一定程度上降低了系统的耦合,易于扩展。
代理可以起到保护目标对象的作用。
增强目标的职责。
- 缺点:
会造成系统设计中类的数目增加。
在客户端和目标对象之间增加了一个代理对象,请求速度处理变慢。
增加系统的复杂度。
- Spring中的代理选择的原则:
1、当Bean有实现接口时,Spring就会用JDK的动态代理。
2、当Bean有实现接口时,Spring选择CGLib
3、Spring可以通过配置强制使用CGLib,只需要在Spring的配置文件中加入如下代码:
<aop:aspectj-autoproxy proxy-target-class="true"/>
7、适配器模式
定义:适配器模式(Adapter Pattern)又叫做变压器模式,他的功能是将一个类的接口变成客户端锁期望的另一个接口,从而使原本因接口不匹配而导致无法在一起工作的两个类能够一起工作。解决兼容问题。
属于结构型设计模式
适用场景:
1、已经存在的类,他的方法和需求不匹配的情况(方法结果相同或相似)
2、适配器模式不是软件设计阶段考虑的设计模式,是随着软件维护,由于不同产品,不同厂家造成功能类似而接口不相同情况下的解决方案。
7.1、classAdapter——类适配器
基于一个已有功能的原始类做适配,定义若干接口设定要适配的方法(面向抽象编程、依赖倒置)
// 原始类、原始方法 public class AC220V { public int output220V() { System.out.println("交流电输出:220"); return 220; } } // 若干要适配接口 public interface DC5V { int output5V(); } public interface DC11V { int output11V(); } // 适配器 public class PowerAdapter extends AC220V implements DC5V, DC11V { @Override public int output5V() { int input = super.output220V(); int output = input / 44; System.out.println("交流电输入:" + input + "; 直流电输出:" + output); return output; } @Override public int output11V() { int input = super.output220V(); int output = input / 20; System.out.println("交流电输入:" + input + "; 直流电输出:" + output); return output; } } public class Test { public static void main(String[] args) { PowerAdapter adapter = new PowerAdapter(); adapter.output5V(); adapter.output11V(); adapter.output220V(); } } 运行结果: 交流电输出:220 交流电输入:220; 直流电输出:5 交流电输出:220 交流电输入:220; 直流电输出:11 交流电输出:220
类图:
7.2、objectAdapter——对象适配器
objectAdapter跟classAdapter的区别是,objectAdapter不必再继承一个已有功能的基础类,而是把该基础类作为适配器的私有成员属性。作用是通过构造器赋值,减少了继承,符合迪米特法则(最少知道原则)。
public class PowerAdapter implements DC5V, DC11V { // 要保证适配器的最少知道原则,可以不继承AC220V,而是作为私有成员属性,通过构造器初始化 private AC220V ac220V; public PowerAdapter(AC220V ac220V) { this.ac220V = ac220V; } @Override public int output5V() { int input = ac220V.output220V(); int output = input / 44; System.out.println("交流电输入:" + input + "; 直流电输出:" + output); return output; } @Override public int output11V() { int input = ac220V.output220V(); int output = input / 20; System.out.println("交流电输入:" + input + "; 直流电输出:" + output); return output; } } public class Test { public static void main(String[] args) { PowerAdapter adapter = new PowerAdapter(new AC220V()); adapter.output5V(); adapter.output11V(); } }
类图:
7.3、interfaceAdapter——接口适配器
上面两类适配器,发现每增加一种适配的target,首先要定义一个接口,目的是让适配器实现接口中声明的方法,这将导致target越多,接口也越多。为解决这种问题,可以将所有的target放到一个接口里(不过要预防万能适配器),即interfaceAdapter,但也违背接口隔离和单一职责原则。
public interface DCTarget { // 适配的目标在一个接口里声明 int output5V(); int output11V(); } public class PowerAdapter implements DCTarget { // 接口适配器,减少了要实现的接口数量 private AC220V ac220V; public PowerAdapter(AC220V ac220V) { this.ac220V = ac220V; } @Override public int output5V() { int input = ac220V.output220V(); int output = input / 44; System.out.println("交流电输入:" + input + "; 直流电输出:" + output); return output; } @Override public int output11V() { int input = ac220V.output220V(); int output = input / 20; System.out.println("交流电输入:" + input + "; 直流电输出:" + output); return output; } }
类图:
7.4、实际场景举例
参考Spring的HandlerAdapter,入口是doDispatch()注support相当于一层条件校验,领会精神即可
// 声明三方适配器要实现的方法 public interface ILoginAdapter { boolean support(Object object); ResultMsg login(String id, Object adapter); } public abstract class AbstractAdapter extends PassportService implements ILoginAdapter { // extends PassportService是classAdapter,保留PassportService的原始功能 ResultMsg loginForRegister(String name, String password) { if (null == password) { password = "THIRD_EMPTY"; } super.register(name, password); return super.login(name, password); } } public class LoginForQQAdapter extends AbstractAdapter { @Override public boolean support(Object object) { return object instanceof LoginForQQAdapter; } @Override public ResultMsg login(String id, Object adapter) { if (!support(adapter)) { return null; } return super.loginForRegister(id, null); } } public class LoginForWechatAdapter extends AbstractAdapter { @Override public boolean support(Object object) { return object instanceof LoginForWechatAdapter; } @Override public ResultMsg login(String id, Object adapter) { if (!support(adapter)) { return null; } return super.loginForRegister(id, null); } } // 业务中注册的适配器 public interface IPassportForThird { ResultMsg loginForQQ(String openId); ResultMsg loginForWechat(String openId); } public class PassportForThirdAdapter implements IPassportForThird { @Override public ResultMsg loginForQQ(String openId) { return processLogin(openId, LoginForQQAdapter.class); } @Override public ResultMsg loginForWechat(String openId) { return processLogin(openId, LoginForQQAdapter.class); } private ResultMsg processLogin(String id, Class<? extends ILoginAdapter> clazz) { try { ILoginAdapter adapter = clazz.newInstance(); return adapter.login(id, adapter); } catch (Exception e) { e.printStackTrace(); } return null; } } // 基础的业务逻辑 public class PassportService { /** * 注册方法 */ public ResultMsg register(String name, String password) { return new ResultMsg(200, "注册成功", new Member()); } /** * 注册后登陆的方法 */ public ResultMsg login(String name, String password) { return null; } } public class Test { public static void main(String[] args) { IPassportForThird passport = new PassportForThirdAdapter(); passport.loginForWechat("1"); } }
类图:
7.5、总结
优点:
1、能提高类的透明性和复用,现有的类复用但不需要改变。
2、目标类和适配器类解耦,提高程序的扩展性。
3、在很多业务中符合开闭原则。
缺点:
1、适配器编写过程需要全面考虑,可能会增加系统的复杂性。
2、增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。
8、桥接模式
定义:
桥接模式(Bridge Pattern)也称为桥梁模式、接口模式(Interface)或者柄体模式(Handle and Body)。是将抽象部分与它的具体实现部分分离,使它们都可以独立的变化。
通过组合的方式建立两个类之间的联系,而不是继承。
属于结构型模型。
多层继承的替代方案。
适用场景:
1、在抽象和具体实现之间需要增加更多的灵活性场景。
2、一个类存在两个(或多个)独立变化的维度,而这两个维度都需要独立进行扩展。
3、不希望使用继承,或因为多层继承导致系统类的个数剧增。
用组合的方式建立连接
8.1、用法举例
消息分为短信和邮件,类型分为普通和紧急。先按照两个维度定义开发,再组合一起。
8.1.1、消息维度
public interface IMessage { void send(String msg); } public class EmailMessage implements IMessage { @Override public void send(String msg) { System.out.println("发送邮件:" + msg); } } public class MsgMessage implements IMessage { @Override public void send(String msg) { System.out.println("发送短信:" + msg); } }
8.1.2、类型维度
public class NormalTypeMsg extends AbstractTypeMsgBridge { public NormalTypeMsg(IMessage iMessage) { super(iMessage); } @Override public String send(String msg) { return "【普通消息】" + msg; } } public class UrgencyTypeMsg extends AbstractTypeMsgBridge { public UrgencyTypeMsg(IMessage iMessage) { super(iMessage); } @Override public String send(String msg) { return "【紧急消息】" + msg; } }
8.1.3、桥
桥里组合IMessage,作为成员变量,在构造方法中初始化。并且基于两个维度,用sendMsg( )方法进行关联。
public abstract class AbstractTypeMsgBridge { private IMessage iMessage; public AbstractTypeMsgBridge(IMessage iMessage) { this.iMessage = iMessage; } public abstract String send(String msg); public void sendMsg(String msg) { String msg1 = this.send(msg); iMessage.send(msg1); } }
8.1.4、类图
8.2、总结
优点:
1、分离抽象部分及其具体实现部分
2、提高了系统的扩展性
3、符合开闭原则
4、符合合成复用原则
缺点:
1、增加了系统的理解与设计难度
2、需要正确地识别系统中两个独立变换的维度
桥接模式相关的设计模式
1、桥接模式(算是一种特殊的组合模式,注重的是组合的形式为了避免继承)和组合模式(要求主线、共同点)
2、桥接模式(多个维度)和适配器模式(一个维度内,且强依赖)
桥接模式的目的是使用组合,使多个维度(抽象维度和实现维度)整合一起,替代为整合而使用继承的方式,因为继承破坏了单一职责原则和最少知道原则。
从开发角度上看,在关联之前,可先按彼此的维度顺序开发,然后在抽象维度上开发桥,将实现维度的一方或多方作为成员属性,另外声明一个方法用于整合两个维度的功能。
从设计模式上看,适配器(class要求实现接口,object要求声明成员属性,interface要求创建适配器接口)是指在一个已有的功能上拓展其他方向,亡羊补牢,实现兼容的目的。桥接是整合多个彼此无关的功能。
9、享元模式
抽象享元模式
配合工厂模式
10、组合模式
人在一起是聚合,心在一起是组合(具有相同的生命周期)