目录
1. 设计模式
1.1 什么是设计模式
软件设计模式(Software Design Pattern),又称设计模式,是一套被==反复使用==、==多数人知晓的==、==代码设计经验的总结==。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是==前辈们的代码设计经验的总结==,具有一定的普遍性,可以反复使用。
1.2 设计模式的种类23种
创建型模式
用于描述“怎样创建对象”,它的主要特点是“==将对象的创建与使用分离==”。GoF(四人组)书中提供了==单例、原型、工厂方法、抽象工厂、建造者==等 5 种创建型模式。
结构型模式
用于描述如何将类或对象按某种布局组成更大的结构,GoF(四人组)书中提供了==代理、适配器、桥接、装饰、外观、享元、组合==等 7 种结构型模式。
行为型模式
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF(四人组)书中提供了==模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器==等 11 种行为型模式。
1.3 单例模式
-
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于==创建型模式==,它提供了一种==创建对象的最佳方式==。
这种模式涉及到一个单一的类,==该类负责创建自己的对象==,同时==确保只有单个对象被创建==。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
-
单例模式的创建方式
第一种:饿汉式--->不管是否使用该对象,该类会直接创建。
第二种: 懒汉式--->当使用到该对象在创建
饿汉式-方式1(静态变量方式)
package com.wjk;
public class 饿汉式 {
public static void main(String[] args) {
Singleton s1=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
Singleton s3=Singleton.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
}
}
//创建一个单例类
class Singleton{
//构造方法私有化---
private Singleton(){}
//创建一个本类的对象---static:随着类的加在而被加在到内存而且只会加载一次
private static Singleton singleton=new Singleton();
//静态方法 返回本类创建的类对象
public static Singleton getInstance(){
return singleton;
}
}
<font color='red'>说明:</font>
该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
2.懒汉式-方式1(线程不安全)
package com.wjk;
public class 懒汉式01 {
public static void main(String[] args) {
Singleton01 s1 = Singleton01.getInstance();
Singleton01 s2 = Singleton01.getInstance();
System.out.println(s1);
System.out.println(s2);
}
}
//创建一个单例类
class Singleton01{
//构造方法私有化---
private Singleton01(){}
//创建一个本类的对象---static:随着类的加在而被加在到内存而且只会加载一次
private static Singleton01 singleton;
//静态方法 返回本类创建的类对象
public static Singleton01 getInstance(){
if(singleton==null){
singleton=new Singleton01();
}
return singleton;
}
}
<font color='red'>说明:</font>
从上面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。
3.懒汉式-方式2(线程安全)
package com.wjk;
public class 懒汉式02 {
public static void main(String[] args) {
Singleton01 s1 = Singleton01.getInstance();
Singleton01 s2 = Singleton01.getInstance();
System.out.println(s1);
System.out.println(s2);
}
}
//创建一个单例类
class Singleton02{
//构造方法私有化---
private Singleton02(){}
//创建一个本类的对象---static:随着类的加在而被加在到内存而且只会加载一次
private static Singleton02 singleton;
//静态方法 返回本类创建的类对象
public synchronized static Singleton02 getInstance(){
if(singleton==null){
singleton=new Singleton02();
}
return singleton;
}
}
<font color='red'>说明:</font>
该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
4.懒汉式-方式3(双重检查锁)
再来讨论一下懒汉模式中加锁的问题,对于 getInstance()
方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为null
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
总结: 单例模式: 饿汉式【资源的浪费】 懒汉式【线程安全问题--双重校验锁】
应用: spring中的bean默认就是单例。
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
-
要解决双重检查锁模式带来空指针异常的问题,只需要使用
volatile
关键字,volatile
关键字可以保证可见性和有序性。
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
-
<font color="red">小结:</font>
添加
volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
还有,这里的private static volatile Singleton singleton = null;中的volatile也必不可少,volatile关键字可以防止jvm指令重排优化,
在java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。这里是因为 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:
-
第一步是给 singleton 分配内存空间;
-
第二步开始调用 Singleton 的构造函数等,来初始化 singleton;
-
第三步,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。
这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。
如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错,详细流程如下图所示:
这里还说一下volatile关键字的第二个作用,保证变量在多线程运行时的可见性:
package com.wjk;
public class Test01 {
public static void main(String[] args) throws Exception{
T t=new T();
t.start();
Thread.sleep(2000);
System.out.println("主线程设置t线程的参数来止损失");
t.setFlag(false);
}
}
class T extends Thread{
private volatile boolean flag=true;
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
System.out.println("进入run方法");
while(flag){
}
}
}
在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前 的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就 可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数 据的不一致。 要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行 读取。
单例模式优点和缺点
优点:单例类只有一个实例,节省了内存资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能;单例模式可以在系统设置全局的访问点,优化和共享数据,例如前面说的Web应用的页面计数器就可以用单例模式实现计数值的保存。
缺点:单例模式一般没有接口,扩展的话除了修改代码基本上没有其他途径。
1.4 工厂模式
1.4.1 背景
在面向对象编程中,创建对象实例最常用的方式就是通过 new 操作符构造一个对象实例,但在某些情况下,new 操作符直接生成对象会存在一些问题。举例来说,对象的创建需要一系列的步骤:可能需要计算或取得对象的初始位置、选择生成哪个子对象实例、或在生成之前必须先生成一些辅助对象。 在这些情况,新对象的建立就是一个 “过程”,而不仅仅是一个操作,就像一部大机器中的一个齿轮传动。
针对上面这种情况,我们如何轻松方便地构造对象实例,而不必关心构造对象示例的细节和复杂过程?解决方案就是使用一个工厂类来创建对象。
1.4.2 什么是工厂模式
工厂模式将目的将创建对象的具体过程屏蔽隔离起来,从而达到更高的灵活性,工厂模式可以分为三类:
-
简单工厂模式(Simple Factory)
-
工厂方法模式(Factory Method)
-
抽象工厂模式(Abstract Factory)
我们在了解工厂模式时,先从现实生活说起:
(1)在没有工厂的时代,如果客户需要一款宝马车,那么就需要客户去创建一款宝马车,然后拿来用。
(2)简单工厂模式:后来出现了工厂,用户不再需要去创建宝马车,由工厂进行创建,想要什么车,直接通过工厂创建就可以了。比如想要320i系列车,工厂就创建这个系列的车。
(3)工厂方法模式:为了满足客户,宝马车系列越来越多,如320i、523i等等系列,一个工厂无法创建所有的宝马系列,于是又单独分出来多个具体的工厂,每个具体工厂创建一种系列,即具体工厂类只能创建一个具体产品。但是宝马工厂还是个抽象,你需要指定某个具体的工厂才能生产车出来。
(4)抽象工厂模式:随着客户要求越来越高,宝马车必须配置空调,于是这个工厂开始生产宝马车和需要的空调。最终是客户只要对宝马的销售员说:我要523i空调车,销售员就直接给他523i空调车了。而不用自己去创建523i空调车宝马车。
下面我们就针对几种不同的工厂模式进行详细的说明:
3.简单工厂模式:
简单工厂模式的核心是定义一个创建对象的接口,将对象的创建和本身的业务逻辑分离,降低系统的耦合度,使得两个修改起来相对容易些,当以后实现改变时,只需要修改工厂类即可。
如果不使用工厂,用户将自己创建宝马车,具体UML图和代码如下:
package com.wjk.factory.demo01;
public class Test01 {
public static void main(String[] args) {
BMW instance = SimpleFactory.getInstance("523i");
instance.move();
}
}
class SimpleFactory{
public static BMW getInstance(String type){
if("320i".equals(type)){
return new BMW320();
}else if("523i".equals(type)){
return new BMW523();
} else{
throw new RuntimeException("该型号不存在");
}
}
}
// 开闭原则---对扩充开放 对修改关闭。
abstract class BMW{
public abstract void move();
}
class BMW320 extends BMW{
public void move() {
System.out.println("BMW320正在路上奔跑");
}
}
class BMW523 extends BMW{
public void move() {
System.out.println("BMW523正在路上奔跑");
}
}
简单工厂模式的优缺点:**
简单工厂模式提供专门的工厂类用于创建对象,实现了对象创建和使用的职责分离,客户端不需知道所创建的具体产品类的类名以及创建过程,只需知道具体产品类所对应的参数即可,通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
但缺点在于不符合“开闭原则”,每次添加新产品就需要修改工厂类。在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展维护,并且工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
为了解决简单工厂模式的问题,出现了工厂方法模式。
1.4.3 工厂方法模式:
==工厂方法模式将工厂抽象化,并定义一个创建对象的接口。每增加新产品,只需增加该产品以及对应的具体实现工厂类,由具体工厂类决定要实例化的产品是哪个,将对象的创建与实例化延迟到子类,这样工厂的设计就符合“开闭原则”了,扩展时不必去修改原来的代码==。在使用时,用于只需知道产品对应的具体工厂,关注具体的创建过程,甚至不需要知道具体产品类的类名,当我们选择哪个具体工厂时,就已经决定了实际创建的产品是哪个了。
但缺点在于,每增加一个产品都需要增加一个具体产品类和实现工厂类,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
工厂方法的 UML 结构图如下:
-
抽象工厂 AbstractFactory: 工厂方法模式的核心,是具体工厂角色必须实现的接口或者必须继承的父类,在 Java 中它由抽象类或者接口来实现。
-
具体工厂 Factory:被应用程序调用以创建具体产品的对象,含有和具体业务逻辑有关的代码抽象产品 AbstractProduct:是具体产品继承的父类或实现的接口,在 Java 中一般有抽象类或者接口来实现。
-
具体产品 Product:具体工厂角色所创建的对象就是此角色的实例。
package com.wjk.factory.demo02;
public class Test02 {
public static void main(String[] args) {
BMW bmw = BMW320Factory.createBMW();
bmw.move();
}
}
//产品的抽象
abstract class BMW{
public abstract void move();
}
//具体产品
class BMW320i extends BMW{
public void move() {
System.out.println("320i移动");
}
}
class BMW523i extends BMW{
public void move() {
System.out.println("523i移动");
}
}
class BMW330i extends BMW{
public void move() {
System.out.println("330i移动");
}
}
//抽象工厂类
abstract class Factory{
public static BMW createBMW(){
return null;
}
}
//具体工厂类
class BMW320Factory extends Factory{
public static BMW createBMW() {
return new BMW320i();
}
}
//具体工厂类
class BMW523Factory extends Factory{
public static BMW createBMW() {
return new BMW523i();
}
}
class BMW330Factory extends Factory{
public static BMW createBMW() {
return new BMW330i();
}
}
但缺点在于,每增加一个产品都需要增加一个具体产品类和实现工厂类,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
作用:
把创建对象和使用对象进行分离。降低的耦合性。
工厂模式的实现方式: [1]简单工厂模式 [2]工厂方法模式。
1.4.4 抽象工厂模式:了解
在工厂方法模式中,我们使用一个工厂创建一个产品,一个具体工厂对应一个具体产品,但有时候我们需要一个工厂能够提供多个产品对象,而不是单一的对象,这个时候我们就需要使用抽象工厂模式。
在介绍抽象工厂模式前,我们先理清两个概念:
-
产品等级结构:产品等级结构指的是产品的继承结构,例如一个空调抽象类,它有海尔空调、格力空调、美的空调等一系列的子类,那么这个空调抽象类和他的子类就构成了一个产品等级结构。
-
产品族:产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品。比如,海尔工厂生产海尔空调、海尔冰箱,那么海尔空调则位于海尔族中。
产品等级结构和产品族结构示意图如下:
什么是抽象工厂模式:
抽象工厂模式主要用于创建相关对象的家族。当一个产品族中需要被设计在一起工作时,通过抽象工厂模式,能够保证客户端始终只使用同一个产品族中的对象;并且通过隔离具体类的生成,使得客户端不需要明确指定具体生成类;所有的具体工厂都实现了抽象工厂中定义的公共接口,因此只需要改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
但该模式的缺点在于添加新的行为时比较麻烦,如果需要添加一个新产品族对象时,需要更改接口及其下所有子类,这必然会带来很大的麻烦。
UML结构图:
抽象工厂 AbstractFactory:定义了一个接口,这个接口包含了一组方法用来生产产品,所有的具体工厂都必须实现此接口。
具体工厂 ConcreteFactory:用于生产不同产品族,要创建一个产品,用户只需使用其中一个工厂进行获取,完全不需要实例化任何产品对象。
抽象产品 AbstractProduct:这是一个产品家族,每一个具体工厂都能够生产一整组产品。
具体产品 Product
3、代码实现:
通过抽象工厂模式,我们可以实现以下的效果:比如宝马320系列使用空调型号A和发动机型号A,而宝马230系列使用空调型号B和发动机型号B,在为320系列生产相关配件时,就无需制定配件的型号,它会自动根据车型生产对应的配件型号A。
也就是说,当每个抽象产品都有多于一个的具体子类的时候(空调有型号A和B两种,发动机也有型号A和B两种),工厂角色怎么知道实例化哪一个子类呢?抽象工厂模式提供两个具体工厂角色(宝马320系列工厂和宝马230系列工厂),分别对应于这两个具体产品角色,每一个具体工厂角色只负责某一个产品角色的实例化,每一个具体工厂类只负责创建抽象产品的某一个具体子类的实例。
产品类:
//发动机以及型号
public interface Engine {}
public class EngineA implements Engine{
public EngineA(){
System.out.println("制造-->EngineA");
}
}
public class EngineB implements Engine{
public EngineB(){
System.out.println("制造-->EngineB");
}
}
//空调以及型号
public interface Aircondition {}
public class AirconditionA implements Aircondition{
public AirconditionA(){
System.out.println("制造-->AirconditionA");
}
}
public class AirconditionB implements Aircondition{
public AirconditionB(){
System.out.println("制造-->AirconditionB");
}
}
创建工厂类:
//创建工厂的接口
public interface AbstractFactory {
//制造发动机
public Engine createEngine();
//制造空调
public Aircondition createAircondition();
}
//为宝马320系列生产配件
public class FactoryBMW320 implements AbstractFactory{
@Override
public Engine createEngine() {
return new EngineA();
}
@Override
public Aircondition createAircondition() {
return new AirconditionA();
}
}
//宝马523系列
public class FactoryBMW523 implements AbstractFactory {
@Override
public Engine createEngine() {
return new EngineB();
}
@Override
public Aircondition createAircondition() {
return new AirconditionB();
}
}
客户:
public class Customer {
public static void main(String[] args){
//生产宝马320系列配件
FactoryBMW320 factoryBMW320 = new FactoryBMW320();
factoryBMW320.createEngine();
factoryBMW320.createAircondition();
//生产宝马523系列配件
FactoryBMW523 factoryBMW523 = new FactoryBMW523();
factoryBMW523.createEngine();
factoryBMW523.createAircondition();
}
}
工厂模式小结:
1、工厂方法模式与抽象工厂模式的区别在于:
(1)工厂方法只有一个抽象产品类和一个抽象工厂类,但可以派生出多个具体产品类和具体工厂类,每个具体工厂类只能创建一个具体产品类的实例。
(2)抽象工厂模式拥有多个抽象产品类(产品族)和一个抽象工厂类,每个抽象产品类可以派生出多个具体产品类;抽象工厂类也可以派生出多个具体工厂类,同时每个具体工厂类可以创建多个具体产品类的实例
1.5. 适配器模式
-
定义:适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
三种适配器:类的适配器模式、对象的适配器模式
1.5.1 适配器模式之类适配器模式
实现方式:让Adapter继承Adaptee类,然后再实现Target接口,来实现适配器功能。
- Adaptee:适配者类,它是需要被访问的、需要被适配的组件. AC220V
- Target:目标接口,当前系统业务所使用的接口,可以是抽象类或接口. DC5V
- Adapter:适配器类,通过继承和实现目标接口,让客户端按照目标接口的方法访问适配者
- Client:客户端,适配器的使用者
实例:手机充电需要将220V的交流电转化为手机锂电池需要的5V直流电。使用电源适配器,将 AC220v ——> DC5V。
类图
**代码:**
/**
* 源角色(Adaptee):现在需要适配的接口。
*/
public class AC220 {
/**
* 输出220V交流电
*
* @return
*/
public int output220V() {
int output = 220;
return output;
}
}
/**
* 目标角色(Target):这就是所期待得到的接口。
* 注意:由于这里讨论的是类适配器模式,因此目标不可以是类。
*/
public interface DC5 {
/**
* 输出5V直流电(期待得到的接口)
*
* @return
*/
int output5V();
}
/**
* 类适配器
* 适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。
*/
public class PowerAdapter extends AC220 implements DC5 {
/**
* 输出5V直流电
*
* @return
*/
@Override
public int output5V() {
int output = output220V();
return (output / 44);
}
}
/**
* 测试类适配器
*/
public class TestClassAdapter {
public static void main(String[] args) {
DC5 dc5 = new PowerAdapter();
System.out.println("输出电流:" + dc5.output5V() + "V");
}
}
-
优点:由于Adapter继承了Adaptee类,所以它可以根据需求重写Adaptee类的方法,使得Adapter的灵活性增强了。
缺点:因为java单继承的缘故,Target类==必须是接口==,以便于Adapter去继承Adaptee并实现Target,完成适配的功能,但这样就导致了Adapter里暴露了Adaptee类的方法,使用起来的成本就增加了。
1.5.2 适配器模式之对象适配器模式
-
实现方式:让Adapter持有Adaptee类的实例,然后再实现Target接口,以这种持有对象的方式来实现适配器功能。
- `Adaptee`:适配者类,它是需要被访问的、需要被适配的组件
- `Target`:目标接口,当前系统业务所使用的接口,可以是抽象类或接口
- `Adapter`:适配器类,通过聚合和实现目标接口,让客户端按照目标接口的方法访问适配者
- `Client`:客户端,适配器的使用者
-
实例:手机充电需要将220V的交流电转化为手机锂电池需要的5V直流电。使用电源适配器,将 AC220v ——> DC5V。
类图:
**代码**
/**
* 源角色(Adaptee):现在需要适配的接口。
*/
public class AC220 {
/**
* 输出220V交流电
*
* @return
*/
public int output220V() {
int output = 220;
return output;
}
}
/**
* 目标角色(Target):这就是所期待得到的接口。
*/
public interface DC5 {
/**
* 输出5V直流电(期待得到的接口)
*
* @return
*/
int output5V();
}
/**
* 对象适配器
*/
public class PowerAdapter implements DC5 {
private AC220 ac220;
public PowerAdapter(AC220 ac220) {
this.ac220 = ac220;
}
/**
* 输出5V直流电
*
* @return
*/
@Override
public int output5V() {
int output = this.ac220.output220V();
return (output / 44);
}
}
/**
* 测试对象适配器
*/
public class TestObjectAdapter {
public static void main(String[] args) {
AC220 ac220 = new AC220();
PowerAdapter powerAdapter = new PowerAdapter(ac220);
System.out.println("输出电流:" + powerAdapter.output5V() + "V");
}
}
-
优点:根据合成复用原则,使用组合替代继承, 所以它解决了类适配器必须继承Adaptee的局限性问题,也不再要求Target必须是接口。使用成本更低,更灵活。