目录
声明
此文只针对我个人的目前的理解程度来记录的,有的我知道的或者觉得没有必要写的内容我就会省略掉。并且主要针对面试,介绍的可能不是很全面,甚至有的地方不是很正确,请包涵。
主要参考文章
设计模式的目的
- 可重用性 (即:相同功能的代码,不用多次编写)
- 可扩展性 (即:当需要增加新的功能时,非常的方便,称为可维护)
- 可靠性 (即:当我们增加新的功能后,对原来的功能没有影响)
- 可读性 (即:编程规范性, 便于其他程序员的阅读和理解)
- 使程序呈现高内聚,低耦合的特性。
设计模式的七大原则
固定记忆:单 开 里 依 接 合 迪。
设计模式的三大分类及关键点
1、创建型模式(用于解耦对象的实例化过程)
单例模式:某个类只能有一个实例,提供一个全局的访问点。
工厂模式:一个工厂类根据传入的参量决定创建出哪一种产品类的实例。
抽象工厂模式:创建相关或依赖对象的家族,而无需明确指定具体类。
建造者模式:封装一个复杂对象的创建过程,并可以按步骤构造。
原型模式:通过复制现有的实例来创建新的实例。
2、结构型模式
装饰器模式:动态的给对象添加新的功能。
代理模式:不修改原始对象,通过代理对象来间接访问原始对象,并在访问前后执行额外的操作。
桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。
适配器模式:将一个类的方法接口转换成客户希望的另一个接口。
组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构。
外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。
享元模式:通过共享技术来有效的支持大量细粒度的对象。
3、行为型模式
策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。
模板模式:抽象父类定义一个算法,子类在不改变该算法结构的情况下重定义该算法的某些步骤。
命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
观察者模式:对象间的一对多的依赖关系。
仲裁者模式:用一个中介对象来封装一系列的对象交互。
备忘录模式:在不破坏封装的前提下,保持对象的内部状态。
解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。
建造者模式:允许一个对象在其对象内部状态改变时改变它的行为。
责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
访问者模式:不改变数据结构的前提下,增加作用于一组对象元素的新功能。
23种设计模式(乱序--现学现写,不全面--应付面试为主)
单例模式--创建型
某个类只能有一个实例,提供一个全局的访问点。
单例模式要素:
- 私有构造方法;
- 私有静态引用指向自己实例 ;
- 以自己实例为返回值的公有静态方法;
单例模式好处:
- 单例模式只允许创建一个对象,因此节省内存;
- 避免了频繁的对象创建,可以加快对象访问速度。
单例模式场景:
- 需要频繁实例化然后销毁的对象;
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象;
- 有状态的工具类对象;
单例模式的实现方式分类:
饿汉式单例:
//线程安全的 public class Singleton { //单例模式,构造器要私有化。 private Singleton() {} //私有的静态的常量--私有静态引用指向自己实例。 //【因为是static修饰的属性,所以是在类加载的时候就被创建, //后期不会再改变,所以线程是安全的!】 private static final Singleton single = new Singleton(); //以自己实例为返回值的公有静态方法。 public static Singleton getInstance() { return single; } }
懒汉式单例:
//有线程安全的问题,不推荐使用 //就算是使用了[加锁+双重判断]的解救办法,性能还是被损耗了。 public class SingletonTest { public static SingletonTest singleton = null; public static SingletonTest getInstance(){ if(singleton == null){ singleton = new SingletonTest(); } return singleton; } //单例模式,构造器要私有化 private SingletonTest { } }
DCL(Double Check双重判断 + Lock加锁)懒汉式单例但线程安全,推荐使用:
//线程安全的 public class Singleton { private volatile static Singleton singleton; public static Singleton getSingleton(){ //双重判断之一 if(singleton==null){ //加锁 synchronized (Singleton.class) { //双重判断之二 if (singleton == null) { singleton = new Singleton(); } } } return singleton; } //构造函数私有化 private Singleton(){ } }
静态内部类(懒汉式单例但线程安全):
//线程安全的 //外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。 //第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类且初始化INSTANCE。 //在创建时是否有并发问题? => 没有没有,类加载时jvm会保证线程安全性! public class Singleton { //静态内部类 private static class SingleTonHoler{ private static Singleton INSTANCE = new Singleton(); } public static Singleton getInstance(){ return SingleTonHoler.INSTANCE; } //单例模式,构造器要私有化 private Singleton { } }
枚举类(饿汉式):
//枚举单例属于懒汉式还是饿汉式:饿汉式,内部枚举类相当于静态成员变量, //类加载时就会创建,因此也是线程安全的。 public class SingletonObject7 { private SingletonObject7(){ } /** * 枚举类型是线程安全的,并且只会装载一次 */ private enum Singleton{ INSTANCE; private final SingletonObject7 instance; Singleton(){ instance = new SingletonObject7(); } private SingletonObject7 getInstance(){ return instance; } } public static SingletonObject7 getInstance(){ //内部枚举类相当于静态成员变量 return Singleton.INSTANCE.getInstance(); } }
破坏单例模式的场景及解决办法:
1、反射是通过调用构造方法生成新对象的,除枚举方式外,其他方式都会被反射破坏单例,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例,则阻止生成新的实例,解决办法如下。
private SingletonObject(){ if (instance != null) { throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取"); } }
2、如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例(反序列化不会调用构造函数--反序列化会用到反射但不是通过反射调用构造函数而是ObjectInputStream类的私有的readObject()方法--使用ObjectInputStream.readObject()读取进来之后,如果是多次读取,就会创建多个实例),所以我们可以不实现序列化接口,如果非得实现序列化接口,可以在单例类中定义反序列化方法readResolve(),在反序列化时直接返回单例对象。
import java.io.Serializable; // Singleton Box: class Box implements Serializable { private static Box instance = new Box("TEST"); public static Box getInstance() { return Box.instance; } private Box(String name) { this.name = name; } private String name; @Override public String toString() { return "Box " + name; } //【readResolve()方法】,如果不写此方法反序列化时会破坏单例 private Object readResolve() { return Box.getInstance(); } }
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Foo { public static void main(String[] args) { Box box = Box.getInstance(); System.out.println(box.toString()); try { ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("e:/box.out")); o.writeObject(box); o.close(); } catch(Exception e) { e.printStackTrace(); } Box box1 = null, box2 = null; try { ObjectInputStream in =new ObjectInputStream( new FileInputStream("e:/box.out")); box1 = (Box)in.readObject(); in.close(); } catch(Exception e) { e.printStackTrace(); } try { ObjectInputStream in =new ObjectInputStream( new FileInputStream("e:/box.out")); box2 = (Box)in.readObject(); in.close(); } catch(Exception e) { e.printStackTrace(); } System.out.println("box1.equals(box2) : " + box1.equals(box2)); System.out.println(box1); System.out.println(box2); } }
模板模式--行为型
抽象父类定义一个算法,子类在不改变该算法结构的情况下重定义该算法的某些步骤。
算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中,提高了代码复用性。
自定义lamabda表达式也符合此设计模式。
案例:炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。
/**
* @ClassName: AbstractClass
* @Description: 抽象类(定义模板方法和基本方法)
* @Author: Sevenyear
*/
public abstract class AbstractClass {
//模板方法定义
//【为防止恶意操作,模板方法一般都加上 final 关键词】
public final void cookProcess() {
pourOil();
heatOil();
pourVegetable();
pourSauce();
fry();
}
public void pourOil() {
System.out.println("倒油");
}
//第二步:热油是一样的,所以直接实现
public void heatOil() {
System.out.println("热油");
}
//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
public abstract void pourVegetable();
//第四步:倒调味料是不一样
public abstract void pourSauce();
//第五步:翻炒是一样的,所以直接实现
public void fry(){
System.out.println("炒啊炒啊炒到熟啊");
}
}
/**
* @ClassName: ConcreteClass_BaoCai
* @Description: 炒包菜类
* @Author: Sevenyear
*/
public class ConcreteClass_BaoCai extends AbstractClass {
@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是包菜");
}
@Override
public void pourSauce() {
System.out.println("下锅的酱料是辣椒");
}
}
/**
* @ClassName: ConcreteClass_BaoCai
* @Description: 炒菜心类
* @Author: Sevenyear
*/
public class ConcreteClass_CaiXin extends AbstractClass {
@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是菜心");
}
@Override
public void pourSauce() {
System.out.println("下锅的酱料是蒜蓉");
}
}
public class Client {
public static void main(String[] args) {
//炒包菜
//创建对象
ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();
//调用炒菜的功能
baoCai.cookProcess();
}
}
jdk案例:InputStream抽象父类中已经定义好了读取字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个空闲索引位置,循环读取len个字节数据。具体如何读取一个字节数据?是由子类实现的。
public abstract class InputStream implements Closeable {
//抽象方法,要求子类必须重写
public abstract int read() throws IOException;
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read(); //调用了无参的read方法,该方法是每次读取一个字节数据
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
}
在InputStream类中定义了多个 read() 方法,从上面代码可以看到,无参的 read() 方法是抽象方法,要求子类必须实现。而 read(byte b[]) 方法调用了 read(byte b[], int off, int len) 方法,所以在此处重点看的方法是带三个参数的方法。 在该方法中可以看到调用了无参的抽象的 read() 方法。
享元模式--结构型
java常量池(字符串常量池、Integer常量池)的实现其实是基于享元模式(通过共享技术来有效的支持大量细粒度的对象)的思想,可以节省创建的时间,并且节省空间。
字符串常量池在java1.8中是在Java堆中的。在编译期就存在的字符串将会直接存入这个池中,在不同代码地方的字面量形式的相同的字符串将会直接引用同一个字符串,为什么能这样引用是因为字符串的不可变性。
策略模式--行为型
comparable和comparator