2.5 万字详解:23 种设计模式
一、设计模式的认识
二、设计模式的分类
根据其目的 根据范围
三、设计模式的优点
四、设计模式中关键点
五、创建型模式
简单(静态)工厂模式 工厂方法模式 抽象工厂模式 单例模式 原型模式 建造者模式
六、个人体会
一、设计模式的认识
设计模式(Design Pattern)是前辈们经过相当长的一段时间的试验和错误总结出来的,是软件开发过程中面临的通用问题的解决方案。这些解决方案使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
二、设计模式的分类
(1)根据其目的
即模式是用来做什么的,可分为创建型(Creational),结构型(Structural)和行为型(Behavioral)三种:①创建型模式主要用于创建对象。②结构型模式主要用于处理类或对象的组合。③行为型模式主要用于描述对类或对象怎样交互和怎样分配职责。
(2) 根据范围
即模式主要是用于处理类之间关系还是处理对象之间的关系,可分为类模式和对象模式两种:类模式处理类和子类之间的关系,这些关系通过继承建立,在编译时刻就被确定下来,是属于静态的。对象模式处理对象间的关系,这些关系在运行时刻变化,更具动态性。
三、设计模式的优点
①可以提高程序员的思维能力、编程能力和设计能力。②使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。③使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。现在这样说肯定有些懵逼,需要在实际开发中才能体会得到真正的好处。
四、设计模式中关键点
(1)创建型模式:简单工厂:一个工厂类根据传入的参量决定创建出那一种产品类的实例。工厂方法:定义一个创建对象的接口,让子类决定实例化那个类。
抽象工厂:创建相关或依赖对象的家族,而无需明确指定具体类。
建造者模式:封装一个复杂对象的构建过程,并可以按步骤构造。
单例模式:某个类只能有一个实例,提供一个全局的访问点。
原型模式:通过复制现有的实例来创建新的实例。
(2)结构型模式
外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。
桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。
组合模式:将对象组合成树形结构以表示“”部分-整体“”的层次结构。
装饰模式:动态的给对象添加新的功能。
代理模式:为其他对象提供一个代理以便控制这个对象的访问。
适配器模式:将一个类的方法接口转换成客户希望的另外一个接口。
亨元(蝇量)模式:通过共享技术来有效的支持大量细粒度的对象。
(3)行为型模式模板模式:定义一个算法结构,而将一些步骤延迟到子类实现。
解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。
策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。
状态模式:允许一个对象在其对象内部状态改变时改变它的行为。
观察者模式:对象间的一对多的依赖关系。
备忘录模式:在不破坏封装的前提下,保持对象的内部状态。
中介者模式:用一个中介对象来封装一系列的对象交互。
命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能。
责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
五、创建型模式
(1)简单(静态)工厂模式
1.认识
①一句话来说就是,一个工厂类根据传入的参量决定创建出那一种产品类的实例。因为逻辑实现简单,所以称为简单工厂模式,也因为工厂中的方法一般设置为静态,所以也称为静态工厂,它不属于23种模式。
②简单工厂模式专门定义一个工厂类来负责创建其他类的实例,被创建的实例通常都具有共同的父类,在工厂类中,可以根据参数的不同返回不同类的实例。升级版本简单工厂模式,通过反射根据类的全路径名生成对象。
③简单工厂模式就是将这部分创建对象语句分离出来,由工厂类来封装实例化对象的行为,修改时只需要修改类中的操作代码,使用时调用该类不需要考虑实例化对象的行为,使得后期代码维护升级更简单方便,有利于代码的可修改性与可读性。
④但是如果增加新的产品的话,需要修改工厂类的判断逻辑,违背开闭原则。
2.UML图解
简单介绍一下UML:泛化:继承 带三角箭头的实线,箭头指向类
实现:实现 带三角箭头的虚线,箭头指向接口
依赖:new A的对象当作方法参数传递进来作为B类的局部变量 带箭头的虚线,指向被使用者
关联:一个类作为另一个类的成员变量 带普通箭头的实心线,指向被拥有者
聚合:new A的对象当作方法参数传递进来作为B类的成部变量 带空心菱形的实心线,菱形指向整体
组合:new A的对象当作构造方法参数传递进来作为B类的成部变量或者A类作为B类成 员变量并已经new A(A类和B类具有相同的生命周期) 带实心菱形的实线,菱形指向整体 总结:各种关系的强弱顺序:泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖
区分:①如果B类作为了A类的成员变量(has的关系),则一般是A类与B类是关联(A类与B类平级)、聚合(A类是整体,B类是部分)、组合的关系(A类是整体,B类是部分,且A类B类有相同的生命周期,)根据上下文语意区分:
聚合(B类即便不在A类中也可以单独存在),组合(B类不在A类中就无法单独存在)。②如果B类作为了A类的局部变量(use的关系),方法的形参,或者对静态方法的调用一般是依赖关系。UML类图如下:
UML说明:苹果手机和红米手机继承了手机这个抽象类,工厂类里根据客户端传入的参数生成相应的对象,如,客户说要红米,工厂给客户一个红米手机,客户说要苹果,工厂给客户一个苹果手机。
简单工厂有三个对象:①抽象产品类:提供抽象方法供具体产品类实现 ②具体产品类:提供具体的产品 ③工厂:根据内部逻辑返回相应的产品
3.代码实现
(1)抽象产品类Phone 这里可以是类,也可以是接口或者抽象类,千万不要思维定式。我比较喜欢面向接口编程,所以我这里用了接口。
public interface Phone {
void produce();
}
(2)具体产品类
在这里插入代码片public class ApplePhoneImpl implements Phone{
@Override
public void produce() {
System.out.println("生产苹果手机");
}
}
public class RedmiPhoneImpl implements Phone{
@Override
public void produce() {
System.out.println("生产了红米手机");
}
}
(3)工厂类
public class Factory {
public Phone getPhone(String type){
Phone phone = null;
if("红米".equals(type)){
phone = new RedmiPhoneImpl();
}else if("苹果".equals(type)){
phone = new ApplePhoneImpl();
}//.....
return phone;
}
}
(4)客户端使用
@Test
public void test1(){
Factory factory = new Factory();
Phone redmiPhone = factory.getPhone("红米");
System.out.println(redmiPhone);
redmiPhone.produce();
Phone applePhone = factory.getPhone("苹果");
System.out.println(applePhone);
applePhone.produce();
}
运行结果如下:
4.总结
优点:只需要传入一个正确的参数,就可以获取你所需要的对象而无需知道其创建对象的细节
缺点:扩展性差,当增加新的产品需要修改工厂类的判断逻辑,违背开闭原则,如我想要买一个华为手机的话,除了新增华为手机这个产品类,还需要修改工厂中的逻辑
5.升级版本
通过反射创建对象,以改进了之前提到的缺点(增加新的产品需要修改工厂类的判断逻辑),现在增加新的具体产品的时候不需要修改工厂中的代码。满足了开闭原则。(1)工厂类代码如下:
public class FactoryPlus {
public Phone getPhone(Class clazz) throws Exception {
return (Phone) Class.forName(clazz.getName()).newInstance();
}
}
(2)客户端代码如下:
@Test
public void test2() throws Exception {
FactoryPlus factory = new FactoryPlus();
Phone redmiPhone = factory.getPhone(RedmiPhoneImpl.class);
System.out.println(redmiPhone);
redmiPhone.produce();
Phone applePhone = factory.getPhone(ApplePhoneImpl.class);
System.out.println(applePhone);
applePhone.produce();
}
运行结果如下:
6.再升级(重要)
(1)工厂类:
public class FactoryPlusPlus {
/**<bean id="applePhone" class="com.wander.design.simplefactory.product.ApplePhoneImpl"/>
* 熟悉吧!!!spring ioc 就是通过将下面的这句话配置在配置文件中,再利用反射创建对象,
* 这就是spring ioc的原理:工厂+配置文件+反射!!以达到彻底解耦的目的**/
private static String className="com.wander.design.simplefactory.product.ApplePhoneImpl";
public static Phone getPhone() throws Exception {
return (Phone) Class.forName(className).newInstance();
}
}
(2) 客户端:
@Test
public void test3() throws Exception {
Phone phone = FactoryPlusPlus.getPhone();
phone.produce();
}
(3)说明:spring ioc容器的原理就是这种方式:工厂+配置文件+反射,spring通过读取配置文件(<bean id="applePhone" class="com.wander.design.simplefactory.product.ApplePhoneImpl"/>
),获取到className再利用反射机制Class.forName(className).newInstance()得到对象赋值给配置文件里bean标签的id属性的值,就是工厂生成的对象名。
优点:就是满足OCP原则,在不修改源代码的前提下切换底层的实现,达到解耦的目的!
7.开发常用版本:多方法工厂
使用以上两种方法的工厂,都有两个缺点:一是不同的产品需要不同额外参数的时候不支持。二是如果使用时传递的type、Class出错,将不能得到正确的对象,容错率不高。
而多方法的工厂模式为不同产品,提供不同的生产方法,使用时 需要哪种产品就调用该种产品的方法,使用方便、容错率高。
(1)工厂类代码如下:
public class FactoryMoreMethod {
public static Phone getApple(){
return new ApplePhoneImpl();
}
public static Phone getRedmi(){
return new RedmiPhoneImpl();
}
/**新增华为手机产品,只需要在工厂中增加一个静态方法即可,不需要修改原有的方法**/
public static Phone getHonor(){
return new HonorPhoneImpl();
}
}
(2)客户端代码:
@Test
public void test3() throws Exception {
Phone apple = FactoryMoreMethod.getApple();
apple.produce();
Phone redmi = FactoryMoreMethod.getRedmi();
redmi.produce();
Phone honor = FactoryMoreMethod.getHonor();
honor.produce();
}
(3)应用场景:查看java源码:java.util.concurrent.Executors类便是一个生成Executor 的工厂 ,其采用的便是 多方法静态工厂模式:例如ThreadPoolExecutor类构造方法有5个参数,其中三个参数写法固定,前两个参数可配置,如下写。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
又如JDK想增加创建ForkJoinPool类的方法了,只想配置parallelism参数,便在类里增加一个如下的方法:
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
(4)总结:多方法工厂的优势,方便创建同种类型的复杂参数对象。
8.应用场景
(1)在任何需要生成复杂对象的地方,都可以使用工厂方法模式。直接用new可以完成的不需要用工厂模式个人理解,重点就是这个复杂 (构造函数有很多参数)和 是否可以 直接用new。
(2)客户端只知道传入工厂类的参数,对于如何创建对象并不关心。(对于升级后的简单工厂模式只知道类名即可)
(3)工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。(对于升级后的简单工厂模式已解决这个问题,符合开闭原则)
(4)简单工厂在源码中的使用--Calendar:
Calendar cal = Calendar.getInstance(zone.toTimeZone(), locale);
public static Calendar getInstance(TimeZone zone, Locale aLocale) {
return createCalendar(zone, aLocale);
}
private static Calendar createCalendar(TimeZone zone, Locale aLocale) {
'部分删减'
Calendar cal = null;
if (aLocale.hasExtensions()) {
String caltype = aLocale.getUnicodeLocaleType("ca");
if (caltype != null) {
switch (caltype) {
case "buddhist":
cal = new BuddhistCalendar(zone, aLocale);
break;
case "japanese":
cal = new JapaneseImperialCalendar(zone, aLocale);
break;
case "Gregory":
cal = new GregorianCalendar(zone, aLocale);
break;
}
}
}
if (cal == null) {
if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
cal = new BuddhistCalendar(zone, aLocale);
} else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
&& aLocale.getCountry() == "JP") {
cal = new JapaneseImperialCalendar(zone, aLocale);
} else {
cal = new GregorianCalendar(zone, aLocale);
}
}
return cal;
}
(2)工厂方法模式
1.认识
①一句话来说就是,定义一个创建对象的接口,让子类决定实例化那个类。因为当需要增加一个新的产品时,我们需要增加一个具体的产品类和与之对应的具体子工厂,然后在具体子工厂方法中进行对象实例化,所以称为工厂方法模式。
②具体来说就是定义一个用于创建对象的工厂接口,但让实现这个工厂接口的子类来决定实例化哪个具体产品类,工厂方法让类的实例化推迟到子类中进行。
③工厂方法模式非常符合“开闭原则”,当需要增加一个新的产品时,我们只需要增加一个具体的产品类和与之对应的具体工厂即可,无须修改原有系统。同时在工厂方法模式中用户只需要知道生产产品的具体工厂即可,无须关系产品的创建过程,甚至连具体的产品类名称都不需要知道。
④虽然他很好的符合了“开闭原则”,但是由于每新增一个新产品时就需要增加两个类,这样势必会导致系统的复杂度增加。
2.UML类图
UML说明:苹果手机和红米手机实现了手机这个抽象类,苹果工厂和红米工厂实现了抽象工厂,苹果工厂当然要生产(依赖)苹果手机,红米工厂当然要生产(依赖)红米。客户要买苹果手机要去问苹果工厂要苹果手机,客户要买红米手机当然要去问红米工厂要红米手机。
工厂方法有四个对象:抽象产品类:提供抽象方法供具体产品类实现 具体产品类:提供具体的产品 抽象工厂:提供抽象方法供具体工厂实现 具体工厂:提供具体的工厂
3.代码实现
(1)抽象产品类和简单工厂的抽象产品类一样
(2)具体产品类和简单工厂的具体产品类一样
(3)抽象工厂
public interface Factory {
Phone getPhone();
}
(4)具体工厂
public class AppleFactoryImpl implements Factory{
@Override
public Phone getPhone() {
return new ApplePhoneImpl();
}
}
public class RedmiFactoryImpl implements Factory{
@Override
public Phone getPhone() {
return new RedmiPhoneImpl();
}
}
(5)客户端
@Test
public void test1(){
Factory applePhoneFactory = new AppleFactoryImpl();
Factory redmiPhoneFactory = new RedmiFactoryImpl();
Phone applePhone = applePhoneFactory.getPhone();
Phone redmiPhone = redmiPhoneFactory.getPhone();
System.out.println(applePhone);
System.out.println(redmiPhone);
applePhone.produce();
redmiPhone.produce();
}
执行结果如下:
4.总结
优点:①用户只需要关心所需产品的对应工厂,无需关心细节
②完全支持开闭原则,提高可扩展性。所谓的开闭原则就是对扩展开放,对修改关闭,再说白点就是实现工厂方法以后要进行扩展时不需要修改原有代码,只需要增加一个工厂实现类和产品实现类就可以。这样的好处可以降低因为修改代码引进错误的风险。
缺点:①每加入一种产品,会创建一个具体工厂类和具体产品类,因此,类的个数容易过多,增加复杂度。
②抽象工厂和抽象产品增加了系统的抽象性和理解难度
5.工厂方法与简单工厂的区别
①可以看出,工厂方法模式特点:不仅仅做出来的产品要抽象, 工厂也应该需要抽象。
②工厂方法使一个产品类的实例化延迟到其具体工厂子类.
③工厂方法的好处就是更拥抱变化。当需求变化,只需要增删相应的类,不需要修改已有的类。
④而简单工厂需要修改工厂类的方法,多方法静态工厂模式需要增加一个静态方法。
缺点:引入抽象工厂层后,每次新增一个具体产品类,也要同时新增一个具体工厂类,所以我更青睐多方法静态工厂,每次新增一个具体产品类,工厂只需要新增一个静态方法。
6.应用场景
(1)客户端不知道它所需要的对象的类。(需要知道所需的对象的类使用升级版简单工厂模式,需要知道所需的参数的类使用简单工厂模式)。
(2)抽象工厂类通过其子类来指定创建哪个对象。
(3)简单工厂在源码中的使用--Collection:Collection(抽象工厂):
public interface Collection<E> extends Iterable<E> {
Iterator<E> iterator();
}
ArrayList(具体工厂):
public class ArrayList<E>{
public Iterator<E> iterator() {
return new Itr();
}
}
Iterator(抽象产品):
public interface Iterator<E> {
boolean hasNext();
}
Itr(具体产品):
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
'省略代码...'
}
(3)抽象工厂模式
1.认识
①一句话来说就是,创建相关或依赖对象的家族,而无需明确指定具体类。因为我们可以定义具体产品类实现不止一个抽象工厂接口,一个工厂也可以生成不止一个产品类,是三个模式中较为抽象,并具一般性的模式。我们在使用中要注意使用抽象工厂模式的条件。
②所谓抽象工厂模式就是提供一个接口,用于创建相关或者依赖对象的家族,而不需要明确指定具体类。他允许客户端使用抽象的接口来创建一组相关的产品,而不需要关心实际产出的具体产品是什么。这样一来,客户就可以从具体的产品中被解耦。它的优点是隔离了具体类的生成,使得客户端不需要知道什么被创建了,而缺点就在于新增新的行为会比较麻烦,因为当添加一个新的产品对象时,需要更改接口及其下所有子类。
2.UML类图
工厂方法有四个对象:
①抽象产品类:为每种具体产品声明接口,如图中Phone手机抽象类和Charger充电器抽象类
②具体产品类:定义了工厂生产的具体产品对象,实现抽象产品接口声明的业务方法,如图中ApplePhoneImpl、RedmiPhoneImpl,AppleChargerImpl,RedmiChargerImpl
③抽象工厂:它声明了一组用于创建一种产品的方法,每一个方法对应一种产品,如上述类图中的Factory就定义了两个方法,分别创建Phone和Charger
④具体工厂:它实现了在抽象工厂中定义的创建产品的方法,生产一组具体产品,这组产品构件成了一个产品种类,每一个产品都位于某个产品等级结构中,如上述类图中的AppleFactoryImpl和RedmiFactoryImpl
3.代码实现
(1)抽象的产品
public interface Phone {
void produce();
}
public interface Charger {
void produce();
}
(2)具体的产品
① 苹果具体的产品
public class AppleChargerImpl implements Charger{
@Override
public void produce() {
System.out.println("生产苹果充电器");
}
}
public class ApplePhoneImpl implements Phone {
@Override
public void produce() {
System.out.println("生产苹果手机");
}
}
② 红米具体的产品
public class RedmiChargerImpl implements Charger{
@Override
public void produce() {
System.out.println("生产红米充电器");
}
}
public class RedmiPhoneImpl implements Phone {
@Override
public void produce() {
System.out.println("生产了红米手机");
}
}
(3)抽象工厂
public interface Factory {
Phone getPhone();
Charger getCharger();
}
(4)具体的工厂
public class AppleFactoryImpl implements Factory {
@Override
public Phone getPhone() {
return new ApplePhoneImpl();
}
@Override
public Charger getCharger() {
return new AppleChargerImpl();
}
}
public class RedmiFactoryImpl implements Factory {
@Override
public Phone getPhone() {
return new RedmiPhoneImpl();
}
@Override
public Charger getCharger() {
return new RedmiChargerImpl();
}
}
(5)客户端
@Test
public void test1(){
Factory appleFactory = new AppleFactoryImpl();
Phone applePhone = appleFactory.getPhone();
Charger appleCharger = appleFactory.getCharger();
System.out.println(appleFactory);
applePhone.produce();
appleCharger.produce();
Factory redmiFactory = new RedmiFactoryImpl();
Phone redmiPhone = redmiFactory.getPhone();
Charger redmiCharger = redmiFactory.getCharger();
System.out.println(redmiFactory);
redmiPhone.produce();
redmiCharger.produce();
}
(5)执行结果
4.总结
优点:
①具体产品在应用层代码隔离,无须关系创建细节
②将一个系列的产品统一到一起创建
③对于增加新的产品族(一个具体工厂就是一个产品族),抽象工厂模式很好地支持了“开闭原则”,只需要增加具体产品并对应增加一个新的具体工厂,对已有代码无须做任何修改。
缺点:①规定了所有可能被创建的产品集合,产品族扩展新的产品(工厂中添加新的方法)困难。如果产品族扩展新的产品,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。
②增加了系统的抽象性和理解难度
5.应用场景
抽象工厂在实际的开发中运用并不多,主要是在开发工程中很少会出现多个产品种类的情况,大部分情况使用以上两种工厂模式即可解决
6.个人总结
一句话总结工厂模式:方便创建 同种产品类型的 复杂参数 对象工厂模式重点就是适用于 构建同产品类型(同一个接口 基类)的不同对象时,这些对象new很复杂,需要很多的参数,而这些参数中大部分都是固定的,so,懒惰的程序员便用工厂模式封装之。(如果构建某个对象很复杂,需要很多参数,但这些参数大部分都是“不固定”的,应该使用建造者Builder模式)
(4)单例模式
1.认识
①一句话来说就是,某个类只能有一个实例,提供一个全局的访问点。
②单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
③使用Singleton的好处还在于可以节省内存,因为它限制了实例的个数,有利于Java垃圾回收(garbage collection),而且确保所有对象都访问唯一实例。但是不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。用单例模式,就是在适用其优点的状态下使用
2.UML类图
UML说明:1.构造方法私有化:可以使得该类不被实例化即不能被new 2.在类本身里创建自己的对象 3.提供一个公共的方法供其他对象访问
3.代码实现
(1)饿汉式
①第一种:
public class Singleton {
/**
* static:
* ①表示共享变量,语意符合
* ②使得该变量能在getInstance()静态方法中使用
* final:
* ①final修饰的变量值不会改变即常量,语意也符合,当然不加final也是可以的
* ②保证修饰的变量必须在类加载完成时就已经进行赋值。
* final修饰的变量,前面一般加static
*/
private static final Singleton singleton = new Singleton();
/**
* 私有化构造方法,使外部无法通过构造方法构造除singleton外的类实例
* 从而达到单例模式控制类实例数目的目的
*/
private Singleton(){}
/**
* 类实例的全局访问方法
* 因为构造方法以及被私有化,外部不可能通过new对象来调用其中的方法
* 加上static关键词使得外部可以通过类名直接调用该方法获取类实例
* @return
*/
public static Singleton getSingleton() {
return singleton;
}
}
②第二种
public class SingletonStatic {
private static final SingletonStatic singletonStatic;
/**
* 和第一种没有什么区别,这种看起来高大上面试装逼使用
*/
static {
singletonStatic = new SingletonStatic();
}
private SingletonStatic() {}
public static SingletonStatic getSingletonStatic(){
return singletonStatic;
}
}
注:这里有两个小知识点:
a.如果是final非static成员,必须在构造器、代码块、或者直接定义赋值
b.如果是final static 成员变量,必须直接赋值 或者在静态代码块中赋值
(2)懒汉式
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton() {
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
说明:
①优点:在外部需要使用的时候才进行实例化,不使用的时候不会占用空间。
②缺点:线程不安全。看上去,这段代码没什么明显问题,但它不是线程安全的。假设当前有N个线程同时调用getInstance()方法,由于当前还没有对象生成,所以一部分同时都进入if语句new Singleton(),那么就会由多个线程创建多个多个user对象。
(3)线程安全的懒汉式
public class Singleton {
private static Singleton singleton;
private Singleton(){};
private static synchronized Singleton getSingleton(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
说明:①优点:解决了懒汉式线程不安全的问题
②缺点:线程阻塞,影响性能。
(4)DCL单例 - 高性能的懒汉式
public class Singleton {
/*volatile在这里发挥的作用是:禁止指令重排序(编译器和处理器为了优化程序性能
* 而对指令序列进行排序的一种手段。)
* singleton = new Singleton();这句代码是非原子性操作可分为三行伪代码
* a:memory = allocate() //分配内存,在jvm堆中分配一段区域
* b:ctorInstanc(memory) //初始化对象,在jvm堆中的内存中实例化对象
* c:instance = memory //赋值,设置instance指向刚分配的内存地址
* 上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。
* 重排序是为了优化性能,但是不管怎么重排序,在单线程下程序的执行结果不能被改变
* 保证最终一致性。而在多线程环境下,可能发生重排序,会影响结果。
* ①若A线程执行到代码singleton = new Singleton()时;
* ②同时若B线程进来执行到代码到第一层检查if (singleton == null)
* ③当cpu切换到A线程执行代码singleton = new Singleton();时发生了指令重排序,
* 执行了a-b,没有执行c,此时的singleton对象只有地址,没有内容。然后cpu又切换到了B线程,
* 这时singleton == null为false(==比较的是内存地址),
* 则代码会直接执行到了return,返回一个未初始化的对象(只有地址,没有内容)。
* */
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
/*第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
* ①当多个线程第一次进入,所有线程都进入if语句
* ②当多个线程第二次进入,因为singleton已经不为null,因此所有线程都不会进入if语句,
* 即不会执行锁,从而也就不会因为锁而阻塞,避免锁竞争*/
if (singleton == null) {
/*第一层锁,保证只有一个线程进入,
* ①多个线程第一次进入的时候,只有一个线程会进入,其他线程处于阻塞状态
* 当进入的线程创建完对象出去之后,其他线程又会进入创建对象,所以有了第二次if检查
* ②多个线程第二次是进入不到这里的,因为已被第一次if检查拦截*/
synchronized (Singleton.class) {
/*第二层检查,防止除了进入的第一个线程的其他线程重复创建对象*/
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
说明:代码注释已详细讲解volatile在该单例模式的作用,已经双重锁的作用。①优点:解决了线程阻塞的问题
②缺点:多个线程第一次进入的时候会造成大量的线程阻塞,代码不够优雅。
(5)静态内部类的方式
public class Singleton {
private Singleton(){}
private static class LayzInner{
private static Singleton singleton = new Singleton();
}
public static Singleton getSingleton(){
return LayzInner.singleton;
}
}
说明:①优点:第一次类创建的时候加载,避免了内存浪费,不存在阻塞问题,线程安全,唯一性
②缺点:序列化-漏洞:反射,会破坏内部类单例模式
(6)枚举单例模式
public enum EnumSingleton {
INSTANCE;
private Singleton singleton;
EnumSingleton(){
singleton = new Singleton();
}
public Singleton getSingleton(){
return singleton;
}
}
说明:单元素的枚举类型已经成为实现Singleton的最佳方法,无法反射创建对象,但是特殊的饿汉式。
(7)静态内部类升级版
借鉴枚举单例的内部实现的方式
public class Singleton {
private Singleton(){
if(LayzInner.singleton != null){
throw new RuntimeException("不能够进行反射!");
}
}
private static class LayzInner{
private static Singleton singleton = new Singleton();
}
public static Singleton getSingleton (){
return LayzInner.singleton;
}
}
说明:①优点:第一次类创建的时候加载,避免了内存浪费,不存在阻塞问题,线程安全,唯一性,解决了反射会破坏内部类单例模式的问题
②缺点:不是官方的
(8)容器式单例
public class Singleton {
private Singleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
public static Object getBean(String className) {
synchronized (ioc) {
if (ioc.containsKey(className)) {
Object o = null;
try {
o = Class.forName(className).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return o;
} else {
return ioc.get(className);
}
}
}
}
说明:Spring ioc 单例 是懒汉式 枚举上的升级
(9)ThreadLocal单例
说明:局部单例模式:某一个线程里唯一
①ThreadLocal的作用呢,是提供线程内的局部变量,在多线程环境访问时,能保证各个线程内的ThreadLocal变量各自独立。也就是说每个线程的ThreadLocal变量是自己专用的,其他线程是访问不到的。
②ThreadLocal最常用于在多线程环境下存在对非线程安全对象的并发访问,而且该对象不需要在线程内共享,如果对该对象加锁,会造成大量线程阻塞影响程序性能,这时候就可以使用ThreadLocal来使每个线程都持有该对象的副本,这是典型的空间换取时间从而提高执行效率的方式。例如项目里经常使用的SimpleDateFormat日期格式化对象,该对象是线程不安全的,而且不需要在线程内共享,因此可以使用ThreadLocal保证其线程安全。
(5)原型模式
1.认识
①一句话来说就是,通过复制现有的实例来创建新的实例。因为是同过原有的对象创建新的对象,所以称为原型模式。
②原型模式是用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。原型模式允许一个对象再创建另外一个可定制的对象,无须知道任何创建的细节。
③用于创建重复的对象,同时又能保证性能。
(1)浅拷贝:我们只拷贝对象中的基本数据类型(8种),对于数组、容器、引用对象等都不会拷贝,只会拷贝对这些对象的引用。
(2)深拷贝:不仅能拷贝基本数据类型,还能拷贝那些数组、容器、引用对象(不仅拷贝对这些对象的引用,而且拷贝对象本身)。
2.UML类图
UML说明:实体类实现Cloneable接口,重写clone方法
3.代码实现
(1)Prototype类:
public class Prototype implements Cloneable {
private Integer id;
private String name;
private Map<String, Double> map;
@Override
protected Prototype clone() throws CloneNotSupportedException {
//浅拷贝方式
Prototype prototype = (Prototype) super.clone();
//深拷贝方式:对每一个复杂类型分别进行克隆
//测试浅拷贝的时候注释下面代码
prototype.map = (Map<String, Double>) ((HashMap)this.map).clone();
return prototype;
}
public Prototype(Integer id, String name, Map<String, Double> map) {
this.id = id;
this.name = name;
this.map = map;
}
/**省略get、set方法和toString方法*/
}
(2)客户端:
public class Client {
@Test
public void test() throws CloneNotSupportedException {
Map<String, Double> map = new HashMap<>();
map.put("数学",100D);
Prototype prototype = new Prototype(1,"小明",map);
Prototype prototype1 = prototype.clone();
Map<String, Double> map1 = prototype1.getMap();
map1.put("数学",99d);
System.out.println(prototype);
System.out.println(prototype1);
}
}
(3)执行结果:浅拷贝:
改变其中一个对象map的值,两个对象的map内容都发生了变化深拷贝:
改变其中一个对象map的值,该对象的map内容发生了变化,另一个对象map的内容没有发生变化
4.总结
优点:①提高了性能,在需要短时间创建大量的对象和创建对象很耗时的情况下,原型模式比通过new对象大大提高了时间效率。
② 逃避构造函数的约束。
缺点:
1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
2、实现原型模式每个派生类都必须实现 Clone接口。
5.应用场景
1.通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。比如,向数据库表插入多条测试数据,可以用到。
2.在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与Java融为浑然一体,大家可以随手拿来使用。
(6)建造者模式
1.认识:
①一句话来说:封装一个复杂对象的构建过程,并可以按步骤构造。因为需要对对象一步步建造起来,所以称为建造者模式。
②将复杂产品的构建过程封装分解在不同的方法中,使得创建过程非常清晰,能够让我们更加精确的控制复杂产品对象的创建过程,同时它隔离了复杂产品对象的创建和使用,使得相同的创建过程能够创建不同的产品。但是若内部变化复杂,会有很多的建造类。
2.UML类图:
UML说明:Product(产品角色):一个具体的产品对象。Builder(抽象建造者):创建一个Product对象的各个部件指定的抽象接口。ConcreteBuilder(具体建造者):实现抽象接口,构建和装配各个部件。Director(指挥者):构建一个使用Builder接口的对象。它主要是用于创建一个复杂的对象。它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。
3.代码如下:
1.产品类:
public class Product {
private String part1;//可以是任意类型
private String part2;
private String part3;
/**set get 方法省略
}
2.抽象建造者
public abstract class Builder{
Product product = new Product();
public abstract void buildPart1();
public abstract void buildPart2();
public abstract void buildPart3();
public Product getResult(){
return product;
};
}
3.具体建造者
public class ConcreteBuilder extends Builder {
@Override
public void buildPart1() {
System.out.println("建造part1");
}
@Override
public void buildPart2() {
System.out.println("建造part2");
}
@Override
public void buildPart3() {
System.out.println("建造part3");
}
}
4.指挥者:
public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public Product build(){
builder.buildPart1();
builder.buildPart2();
builder.buildPart3();
return builder.getResult();
}
}
5.客户端
public class Client {
@Test
public void test() {
Builder builder = new ConcreteBuilder();
Director director = new Director(builder);
director.build();
}
}
6.执行结果
4.总结
优点:1、建造者独立,易扩展。将复杂产品的构建过程封装分解在不同的方法中,使得创建过程非常清晰,能够让我们更加精确的控制复杂产品对象的创建过程。
2、便于控制细节风险。它隔离了复杂产品对象的创建和使用,使得相同的创建过程能够创建不同的产品。
缺点:1、产品必须有共同点,范围有限制。
2、如内部变化复杂,会有很多的建造类,导致系统庞大。
应用场景1、需要生成的对象具有复杂的内部结构。2、需要生成的对象内部属性本身相互依赖。
5.应用场景
JAVA 中的 StringBuilder。
六、个人体会
设计模式是一种解决问题的思维和方式,不要生搬硬套,为了设计模式而模式。本文作者:王德印,欢迎复制下方链接关注博主动态。
链接:https://blog.csdn.net/qq_41889508/article/details/105953114
推荐阅读
3. SpringSecurity + JWT 实现单点登录