目录
前言
设计模式有很多,网上能搜到的教程也有很多,在学习了设计模式相关教程后,总是觉得每种设计模式都很类似,不能很好分辨出各设计模式的侧重点。在众多设计模式中,有7种设计模式在开发中应用频次较高,也非常重要,这里是对这7种设计模式进行总结,分析各设计模式的设计思想和侧重点。
一、单例模式(Singleton)
单例模式:就是采取一定措施使得整个程序中对于某个类,只存在一个对象实例。该类的所有元素和构造器对外界封闭,只提供一个静态方法来获取对象实例。
在设计单例模式时,需要考虑如何使得程序只存在一个对象实例,即当已经存在一个实例时如何避免创建新的实例,按照这一设计思想,联想到静态量、条件判断、线程安全,就可以按照不同方式实现单例模式,也能理解单例模式的八种方式。
1.饿汉式(静态常量)
为了避免重复创建新的对象实例,在类内部对声明静态常量的对象实例,在类装载时就完成了实例化,代码如下:
public class SingletonHungry1 {
// 1.构造器私有化,外部不能new
private SingletonHungry1() {}
// 2.本类内部定义属性时直接创建对象实例
private final static SingletonHungry1 instance = new SingletonHungry1();
// 3.对外提供一个公有的静态方法,返回实例对象
public static SingletonHungry1 getInstance() {
return instance;
}
}
优点
这种方式比较简单,在类装载的时候就完成了对象实例化操作,避免了线程安全问题。
缺点
静态常量的方式创建的实例对象会一直存在在内存中,如果整个程序从头到尾都未使用过这个实例,则会造成内存的浪费。
2.饿汉式(静态代码块)
可以在静态常量中实例化对象,也可以在静态代码块中实例化对象,这两种方式类似,代码如下:
public class SingletonHungry2 {
private static SingletonHungry2 instance;
// 1.构造器私有化,外部不能new
private SingletonHungry2() {}
// 2.在静态代码块中创建单例对象
static {
instance = new SingletonHungry2();
}
// 3.对外提供一个公有的静态方法,返回实例对象
public static SingletonHungry2 getInstance() {
return instance;
}
}
这种方式的优缺点与静态常量一致。
3.懒汉式(线程不安全)
除静态方式,还可以通过条件判断的方式来获取对象实例,且保证只存在一种实例,代码如下:
public class SingletonLazy1 {
private static SingletonLazy1 instance = null;
// 1.构造器私有化,外部不能new
private SingletonLazy1() {
}
// 2.提供一个静态的公有方法,当使用到该方法时,才去创建单例对象
public SingletonLazy1 getInstance() {
if (instance == null) {
instance = new SingletonLazy1();
}
return instance;
}
}
优点
这种方式起到了懒加载的效果,即只有当有需要对象实例时,才去创建。
缺点
在多线程下,都去获取对象实例时,在if判断语句块中,可能会产生多个实例对象,违背了只存在一个对象的原则,是线程不安全的。
4.懒汉式(同步方法,线程安全)
同步方法可以避免线程安全问题,因此只要在上面的基础上将获取对象实例的方法改为同步方法,就能避免线程安全问题,代码如下:
public class SingletonLazy2 {
private static SingletonLazy2 instance = null;
// 1.构造器私有化,外部不能new
private SingletonLazy2() {
}
// 2.提供一个静态的公有方法,当使用到该方法时,才去创建单例对象
// 同时利用Synchronized方法实现线程安全
public static synchronized SingletonLazy2 getInstance() {
if (instance == null) {
instance = new SingletonLazy2();
}
return instance;
}
}
优点
解决了线程安全问题。
缺点
每个线程获取实例时,都要进行同步,即便对象已经实例化了,还要在外进行等待,这大大降低了代码的执行效率。
5.懒汉式(同步代码块,线程安全)
同步代码块也可以实现线程安全问题,代码如下:
public class SingletonLazy3 {
private static SingletonLazy3 instance = null;
// 1.构造器私有化,外部不能new
private SingletonLazy3() {
}
// 2.提供一个静态的公有方法,当使用到该方法时,才去创建单例对象
// 同时利用同步代码块实现线程安全
public static SingletonLazy3 getInstance() {
synchronized (SingletonLazy3.class) {
if (instance == null) {
instance = new SingletonLazy3();
}
}
return instance;
}
}
值得注意的是,上述方式同样效率很低,和同步方法的方式没有区别,而为了提高效率将同步代码块放入if条件判断中,则会存在线程安全问题。
6.双重检查
方式5的弊端就是将同步代码块放入条件判断中就存在线程安全问题,而放在条件判断外效率低,因此可以多加一层条件判断,通过双重检查来解决这些问题,代码如下:
public class SingletonDoubleCheck {
private static SingletonDoubleCheck instance = null;
// 1.构造器私有化,外部不能new
private SingletonDoubleCheck() {}
// 2.提供一个静态的公有方法,当使用到该方法时,才去创建单例对象
// 利用同步代码块,同时通过两层判断语句实现线程安全
public static SingletonDoubleCheck getInstance() {
if (instance == null) {
synchronized (SingletonDoubleCheck.class) {
if (instance == null) {
instance = new SingletonDoubleCheck();
}
}
}
return instance;
}
}
这种方法是多线程开发中经常用到的,线程安全,效率较高。
7.静态内部类
当类被装载时是线程安全的,利用这一特性可以通过静态内部类实现单例模式,当外部类装载时,内部类不会装载,当调用获取对象方法时,内部类才会装载,代码如下:
public class SingletonInnerClass {
// 1.构造器私有化,外部不能new
private SingletonInnerClass() {}
// 2.写一个静态内部类,该类中有一个静态属性Singleton
private static class SingletonInstance {
private static final SingletonInnerClass INSTANCE = new SingletonInnerClass();
}
// 3.对外提供一个公有的静态方法,返回实例对象
public static SingletonInnerClass getInstance() {
return SingletonInstance.INSTANCE;
}
}
这种方式通过类转载的机制保证了线程安全,效率高,推荐使用,具体原因可以参照JVM相关内容。
8.枚举
通过JDK1.5中添加的枚举来实现单例模式,同样可以避免线程安全问题,还能防止反序列化重新创建新的对象,代码如下:
public enum SingletonEnum {
INSTANCE;
}
二、工厂模式(Factory)
工厂模式的设计思想:将产品对象的创建过程分离到具体工厂类中,即产品类内部自己写相应的属性和方法,而创建该产品对象由具体工厂来完成。
1.简单工厂
简单工厂是针对一类产品有与之对应的一个具体工厂类,用来创建具体产品对象,代码如下:
public class SimpleFactory {
// 创建Pizza产品具体的对象实例的方法
private static Pizza createPizza(String orderType) {
Pizza pizza = null;
if ("greek".equals(orderType)) {
pizza = new GreekPizza();
}else if ("cheese".equals(orderType)) {
pizza = new CheessPizza();
}else if ("pepper".equals(orderType)) {
pizza = new PepperPizza();
}
return pizza;
}
// 根据传进来的产品类型获取相应的产品对象实例
public static Pizza getPizza(String orderType) {
return createPizza(orderType);
}
}
上述代码就是一个简单工厂,而相应的产品代码如下:
public abstract class Pizza {
protected String name; // 名字
public void bake() { System.out.println(name + " baking"); }
public void cut() { System.out.println(name + " cutting"); }
public void box() { System.out.println(name + " boxing"); }
public void setName(String name) { this.name = name; }
}
public class CheesePizza extends Pizza {}
public class GreekPizza extends Pizza {}
public class PepperPizza extends Pizza {}
这里限于篇幅,只是简单介绍了产品类,为了方便工程获取具体子产品,这里定义了一个公共的抽象父类Pizza,也可以用接口来替换,效果一样。
在抽象父类或接口中可以定义公共的抽象方法,由子类或实现类根据情况具体实现。重点在于简单工厂的思想是通过工厂来创建相应产品的对象。
优点
客户端避免了直接创建产品对象的操作,由工厂来完成;客户端不需要知道具体产品的类名,只需要传入类型参数,由工厂来判断需要的产品类型,并创建对应的产品实例。
缺点
工厂类单一,负责具体某一类产品的创建工作(如Pizza抽象类的所有子类或者Pizza接口的所有实现类);当产品类过于复杂,存在几百上千个不同产品时,需要修改工厂的条件判断逻辑,容易造成逻辑混乱;当增加新的产品类型时(如增加Phone产品),需要专门针对这类产品建造新的工厂,系统扩展困难。
应用场景
当产品种类较少时,可以使用简单工厂模式。只需要传入工厂类的参数,不需要关注如何创建对象的逻辑。
2.工厂方法
工厂方法是对简单工厂的进一步抽象化,其定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化操作推迟到子类中。代码如下:
public abstract class OrderPizza {
// 抽象方法获取Pizza对象,让各子类具体实现
public abstract void getPizza();
// 获取客户希望订购的Pizza种类
public String getType() {
try {
BufferedReader string = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Input pizza type: ");
return string.readLine();
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
public class BJOrderPizza extends OrderPizza {
Pizza pizza = null;
@Override
public void getPizza() {
do {
String orderType = getType();
if ("cheess".equals(orderType)) {
this.pizza = new BJCheessPizza();
}else if ("pepper".equals(orderType)) {
this.pizza = new BJPepperPizza();
}
if (orderType != null) {
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}else {
break;
}
}while (true);
}
}
public class LDOrderPizza extends OrderPizza {
Pizza pizza = null;
@Override
public void getPizza() {
do {
String orderType = getType();
if ("cheess".equals(orderType)) {
this.pizza = new LDCheessPizza();
}else if ("pepper".equals(orderType)) {
this.pizza = new LDPepperPizza();
}
if (orderType != null) {
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}else {
break;
}
}while (true);
}
}
3.抽象工厂
抽象工厂定义了一个借口用于创建相关的或者无关的其他产品对象,相当于简单工厂和工厂方法的组合。抽象工厂分为两层,通用的AbsFactory借口用来定义获取产品对象的通用操作,具体的实现子类用来获取某一类产品的具体实例。代码如下:
// 通用的抽象工厂借口
public interface ABSFactory {
public Pizza getPizza(String orderType);
}
// 具体实现子工厂,实现对北京Pizza产品对象的创建
public class BJFactory implements ABSFactory {
@Override
public Pizza getPizza(String orderType) {
Pizza pizza = null;
if ("cheese".equals(orderType.toLowerCase())) {
pizza = new BJCheessPizza();
}else if ("pepper".equals(orderType.toLowerCase())) {
pizza = new BJPepperPizza();
}
return pizza;
}
}
// 具体实现子工厂,实现对伦敦Pizza产品对象的创建
public class LDFactory implements ABSFactory {
@Override
public Pizza getPizza(String orderType) {
Pizza pizza = null;
if ("cheese".equals(orderType.toLowerCase())) {
pizza = new LDCheessPizza();
}else if ("pepper".equals(orderType.toLowerCase())) {
pizza = new LDPepperPizza();
}
return pizza;
}
}
// 点Pizza类,通过某个工厂获取相应的产品对象实例
public class OrderPizza {
ABSFactory factory;
public OrderPizza(ABSFactory factory) { this.factory = factory; }
public void setFactory(ABSFactory factory) { this.factory = factory; }
public void getPizza() { getPizzaByFactory(); }
// 通过工厂获取产品实例
private void getPizzaByFactory() {
Pizza pizza = null;
do {
pizza = factory.getPizza(getType());
if (pizza != null) {
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}else {
break;
}
}while (true);
}
// 传入产品类型参数
private String getType() {
try {
BufferedReader string = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Input pizza type: ");
return string.readLine();
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
抽象工厂增强了程序的可拓展性,当新增一个产品簇时,不需要修改原代码,只需要增加相应的产品类和创建该产品对象的工厂即可。
三、原型模式(Prototype)
原型模式设计思想:用一个已经创建好的实例作为原型,通过复制该原型对象来创建一个和原型相同的新对象。根据拷贝方式的不同,原型模式分为浅拷贝和深拷贝。
1.浅拷贝
通俗来讲,浅拷贝就是拷贝地址,即拷贝对象的成员变量是某个数组、某个类的对象时,新得到的复制对象的这些成员变量只是获得了相同的地址,指向内存中一个同一个成员变量。代码如下:
public class Sheep implements Cloneable {
private String name;
private int age;
private String color;
// 构造器
public Sheep(String name, int age, String color) {
this.name = name;
this.age = age;
this.color = color;
}
// 浅拷贝,对于属性是基本数据类型或字符串,进行值传递;对于属性是引用数据类型,进行地址传递
@Override
protected Object clone() {
Sheep sheep = null;
try {
sheep = (Sheep) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return sheep;
}
}
通过实现Cloneable接口,使用默认的clone()方法来实现浅拷贝。
2.深拷贝
深拷贝是对拷贝对象的所有成员变量进行拷贝,对于数组、类这些引用数据类型进行复制,创建新的对象,而非拷贝地址。代码如下:
// 用于测试深拷贝的类对象
public class DeepCloneableTarget implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private String cloneName;
private String cloneClass;
public DeepCloneableTarget(String cloneName, String cloneClass) {
this.cloneName = cloneName;
this.cloneClass = cloneClass;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
// 拷贝原型
public class DeepPrototype implements Serializable, Cloneable {
public String name;
public DeepCloneableTarget deepCloneableTarget;
public DeepPrototype() {}
// 构造器
public DeepPrototype(String name, DeepCloneableTarget deepCloneableTarget) {
this.name = name;
this.deepCloneableTarget = deepCloneableTarget;
}
// 深拷贝:通过对象的序列化实现(推荐),
// 本质上是利用内存二进制流的复制,性能上比new一个对象更好。
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);
Object object = ois.readObject();
return object;
}catch (Exception e) {
e.printStackTrace();
}finally {
// 资源关闭,限于篇幅,这里简写了
ois.close();
bis.close();
oos.close();
bos.close();
}
return null;
}
}
优点:
利用原型模式拷贝对象时,不需要知道对象的内部细节,有哪些成员变量,可以直接将原有对象复制一份,简化了创建对象的过程。
缺点:
原型模式中拷贝对象的类需要实现Cloneable接口,配置一个clone方法,而如果采用深拷贝的方式,其成员变量中的类或数组成员变量,同样需要实现Cloneable接口,配置相应的clone方法,违反了开闭原则。