写点什么

深入学习单例模式和组合模式

发布于: 2020 年 06 月 24 日
深入学习单例模式和组合模式

单例模式

定义

指一个类只有一个实例,且该类能自行创建这个实例的一种模式。通俗的说就是一个类只产生一个实例

为什么要使用单例模式

使用单例模式主要是两方面的要求:性能的要求和功能的要求。

性能要求

减少实例频繁创建和消耗带来的资源消耗。

功能要求

多个用户访问实例是便于统一控制管理

类图



模式特点

  • 单例类只有一个实例对象

  • 该单例对象必须由单例类自行创建

  • 单例类对外提供一个访问该单例的全局访问点

模式常见应用

  • window系统中的回收站、显卡驱动程序对象、打印机后台处理服务等

  • 多线程中的线程池对象

  • 应用程序的日志对象

  • 数据库连接池对象

  • 应用程序中的对话框

  • ……

常见的实现方式

懒汉式



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

缺点:这种方式通过synchronized来保证线程安全的单例,效率较低,性能较差

优点:支持懒加载,内存利用率高

饿汉式



public class SingleHungry {
private static SingleHungry instance = new SingleHungry();
private SingleHungry(){}
public static SingleHungry getInstance(){
return instance;
}
}

缺点:类加载即初始化对象,浪费内存,容易产生垃圾对象

优点:无锁,效率较高

双重检测式



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

双重校验,其实就是加了两次锁,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题。



缺点:双锁机制,笨重且实现难度较大

优点:安全且在多线程情况下能保持高性能

枚举式



public enum SingleEnum {
//实例化对象
INSTANCE;
void getInstance(){}
}

因为枚举类隐藏了私有的构造器并且枚举类的域是相应类型的一个实例对象,所以能保证在任何情况下都是单例。



缺点:不为人所熟悉,让人感到生疏,实际工作中很少用到

优点:实现单例模式的最佳方法,高效、简洁、线程安全且不可破坏

静态内部类式



public class SingleInner {
private static class SingleInnerHolder{
private static SingleInner instance = new SingleInner();
}
private SingleInner(){}
public static SingleInner getInstance(){
return SingleInnerHolder.instance;
}
}

这种实现方式只有第一次调用getInstance方法时,虚拟机才加载 SingleInnerHolder 并初始化instance ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。



缺点:只适用于静态域的情况

优点:实际效果和双重检验一样,但是实现更简单

手写单例模式

懒汉式



饿汉式



枚举式



双重检验式



静态内部类式



破坏单例模式

单例模式真的能保证只有一个实例吗?

反射

单例模式中为了不让外部创建多个实例,使用了private关键字修饰构造方法,但是这样并不能完全阻止创建多个实例,我们可以通过反射去new 一个实例出来



Class<?> clazz = SingleHungry.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();

为了阻止反射破坏单例模式,我们需要在构造方法中加个判断,如果试图使用反射创建实例,直接抛异常



private SingleHungry() {
if (instance != null) {
throw new RuntimeException("请不要试图用反射破坏单例模式!");
}
}

序列化

单例实现Serializable接口后,反序列化也可以破坏单例模式。



SingleHungry singleton = SingleHungry.getSingleton();
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("singletonFile"));
oos.writeObject(singleton);
File file = new File("singletonFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
SingleHungry singletonBySerialize = (SingleHungry)ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}

为了避免序列化破坏单例模式,则需要在单例中添加readResolve方法



private Object readResolve() {
return getSingleton();
}

添加readResolve方法可以解决反序列化破坏单例模式的原因是因为反序列化过程中,在反序列化执行过程中会执行到ObjectInputStream#readOrdinaryObject方法,反序列化时判断对象是否包含readResolve方法,如果包含的话会直接调用这个方法获得对象实例,这样就能阻止反序列化创建多个单例实例了

总结

从线程安全、是否支持懒加载、性能和实现难易程度四个方向进行综合考量五种单例模式结果如下。



结论:一般不要使用懒汉模式,无特殊要求的情况下直接使用懒汉式即可。如果要求必须实现懒加载,建议是静态内部类式的单例模式。若在需要序列化的场景建议使用枚举式,确保序列化导致出现多个实例。

组合模式

定义

将对象组合成树形结构来表现“整体/部分”的层次结构

意图

使得用户对单个对象和组合对象的使用具有一致性,降低了元素内部结构的复杂度对程序的影响。

何时使用组合模式

  • 表示对象的部分-整体层次结构(树形结构)。

  • 忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象。

类图



  • Component接口描述了树中简单项目和复杂项目所共有的操作。

  • Leaf是树的基本结构, 不包含子项目。叶节点一般会完成大部分的实际工作, 因为它们无法将工作指派给其他部分。

  • Node是包含叶节点或其他节点等子项目的单位。 节点无法知道其包含的子项目所属的具体类,只通过通用的接口与其子项目交互。节点接收到请求后会将工作分配给自己的子项目,处理中间结果,然后将最终结果返回给客户端。

  • Client通过组件接口与所有项目交互。 因此客户端能以相同方式与树状结构中的简单或复杂项目交互。

实现方式

节点和叶子实现统一接口,节点内部组合该接口,并且含有内部属性 List,里面放Component

优缺点

优点

  • 高层模块调用简单

  • 可以利用多态和递归机制更方便地使用复杂树结构

  • 符合开闭原则,灵活自由增加节点

缺点

  • 违反了依赖倒置原则。因为叶子和节点的声明都是实现类,而不是接口

  • 有时候提供公共接口困难,有时候会导致过度组件化使人难以理解

实践

使用组合模式完成下述设计



窗口图:



结构图:



程序输出结果:



设计结果

代码层次



类图



client



public class Main {

public static void main(String[] args) {
WinForm winForm = new WinForm("WINDOWS窗口");
winForm.addComponent(new Picture("LOGO图片"));
winForm.addComponent(new Button("登录"));
winForm.addComponent(new Button("注册"));

IFrame frame = new IFrame("FRAME1");
frame.addComponent(new Label("用户名"));
frame.addComponent(new TextBox("文本框"));
frame.addComponent(new Label("密码"));
frame.addComponent(new PassWordBox("密码框"));
frame.addComponent(new CheckBox("复选框"));
frame.addComponent(new TextBox("T记住用户名"));
frame.addComponent(new LinkLabel("忘记密码"));
winForm.addComponent(frame);

winForm.print();
}
}

输出结果



具体代码



Component.java

public interface Component {
void print();
}



BaseComponent.java



public class BaseComponent implements Component {

private String name;

public BaseComponent(String name){
this.name = name;
}

@Override
public void print() {
System.out.println("print " + this.getClass().getSimpleName() + "(" + name + ")");
}
}



Container.java



public class Container implements Component {

private String name;

private List<Component> componentList = new ArrayList<Component>();

public Container(String name){
this.name = name;
}

public void addComponent(Component component) {
componentList.add(component);
}

@Override
public void print() {
System.out.println("print " + this.getClass().getSimpleName() + "(" + name + ")");
componentList.forEach(Component::print);
}
}



WinForm.java



public class WinForm extends Container {
public WinForm(String name) {
super(name);
}
}



IFrame.java



public class IFrame extends Container {
public IFrame(String name) {
super(name);
}
}



Picture.java



public class Picture extends BaseComponent {
public Picture(String name) {
super(name);
}
}



其它叶子类实现均同Picture一致



发布于: 2020 年 06 月 24 日阅读数: 61
用户头像

还未添加个人签名 2018.04.29 加入

还未添加个人简介

评论

发布
暂无评论
深入学习单例模式和组合模式