设计模式
一、设计模式的的6大原则
总原则-开闭原则
对扩展开放,对修改封闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。
想要达到这样的效果,我们需要使用接口和抽象类等,后面的具体设计中我们会提到这点。
(1)单一职责原则
一个类最好只有一个功能,不要一个类有2个功能,除非逻辑很简单
(2)接口隔离原则
每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。
(3)依赖倒置原则
面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
(4)里氏替换原则
任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
里氏代换原则是对“开-闭”原则的补充。实现“开闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。
当子类继承父类时,最好不要修改父类的方法,继承意味着耦合,可以通过组合、聚合解耦
(5)迪米特法则 (最少知道原则)
一个类对自己依赖的类知道的越少越好。无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。
(6)合成复用原则
尽量使用聚合和组合代替继承
二、设计模式的分类
设计模式分为三大类:
创建型模式,共五种:
单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式
结构型模式,共七种:
适配器模式、装饰者模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:
策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
PS:其实还有两类:并发型模式和线程池模式(多线程相关)。
学习建议:
设计模式需要几个阶段的学习,没有大量项目经验的时候学习,可能只是了解,当有了一些项目场景的时候,才会深刻体会到其中的奥妙。
后面我会一个一个详细讲解每种设计模式,每讲解一种会把它设置成链接,请收藏
三、设计模式精讲
一、单例模式
单例模式有以下特点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头
话不多说,先上一个流程图看看单例模式的进化史:
一、饿汉式单例
public class HungrySingleton {
public static void main(String[] args) {
//测试
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2); // true
}
}
//饿汉式
class Singleton {
//1. 构造器私有化, 外部不能new
private Singleton() {
}
//2.一开始创建对象实例
private final static Singleton instance = new Singleton();
//3. 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
return instance;
}
}
ps:Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)
优点: 饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的
缺点:优点正是其缺点,由于一开始就加载,如果没有使用就会白白占用内存,于是有了下面的懒汉式
二、线程不安全懒汉式
public class NotSafeLazySingleton {
public static void main(String[] args) {
System.out.println("懒汉式1 , 线程不安全~");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2); // true
}
}
class Singleton {
private static Singleton instance;
private Singleton() {}
//提供一个静态的公有方法,当使用到该方法时,才去创建 instance,即懒汉式
//由于没有加锁,大量线程同时访问可能出现实例化多个对象,所以线程不安全
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
当大量线程同时访问getInstance()时,就可能创建多个实例,所以不可取
三、线程安全同步方法(锁方法锁粒度较大)
public class SynchronizedMethodSingleton {
public static void main(String[] args) {
System.out.println("懒汉式2 , 线程安全~");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2); // true
}
}
// 懒汉式(线程安全,同步方法)
class Singleton {
private static Singleton instance;
private Singleton() {}
//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
//即懒汉式
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
由于锁加在了方法上,高并发下会降低性能,所以应该减小锁的粒度
四、线程安全同步代码块(双重检验锁降低了锁粒度,推荐使用)
public class SynchronizedBlockSingleton {
public static void main(String[] args) {
System.out.println("双重检查");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2); // true
}
}
// 懒汉式(线程安全,同步代码块)
class Singleton {
//这里加volatile关键字禁止指令重排
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;
}
}
ps:这里instance之所以要用volatile修饰,是为了禁止指令重排,防止后面线程访问到前面线程未初始化的对象,详情参考以下链接:
https://blog.csdn.net/OrangeRawNorthland/article/details/83788412
双重检验锁尽管降低了锁粒度,但毕竟还是有锁,有锁就会降低并发性能,那么可不可以不加锁呢?答案是肯定的,那就是使用静态内部类,静态内部类不会在一开始就加载,会在第一次使用时才加载,所以它既解决了饿汉式可能白白占用资源的问题,又解决了懒汉式为解决线程安全问题加锁导致的性能问题,可谓是一举两得
五、使用静态内部类(推荐使用)
public class StaticInnerClassSingleton {
public static void main(String[] args) {
System.out.println("使用静态内部类完成单例模式");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2); // true
}
}
// 静态内部类完成, 推荐使用
class Singleton {
private static volatile Singleton instance;
//构造器私有化
private Singleton() {}
//写一个静态内部类,该类中有一个静态属性 Singleton
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
public static synchronized Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
六、枚举实现单例
public class EnumSingleton {
public static void main(String[] args) {
Singleton instance = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance == instance2);//true
instance.sayOK();
}
}
//使用枚举,可以实现单例, 推荐
enum Singleton {
INSTANCE; //属性
public void sayOK() {
System.out.println("ok~");
}
}
枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。( 其他方法都会通过反射的方式破坏单例 )
单例模式总结:
单例模式为一个面向对象的应用程序提供了对象唯一的访问点,不管它实现何种功能,整个应用程序都会同享一个实例对象 。
对于单例模式的几种实现方式,知道饿汉式和懒汉式的区别,线程安全,资源加载的时机,还有懒汉式为了实现线程安全的几种方式的细微差别。
原创不易,觉得写得不错的话就点点赞关注关注呗,我的微信公众号:java时光