标签 设计模式 下的文章 - 酷游博客
首页
关于
友链
Search
1
阿里的简历多久可以投递一次?次数多了有没有影响?可以同时进行吗?
45 阅读
2
Java中泛型的理解
40 阅读
3
Java 14 发布了,再也不怕 NullPointerException 了!
38 阅读
4
Java中的可变参数
37 阅读
5
该如何创建字符串,使用" "还是构造函数?
29 阅读
技术
登录
/
注册
找到
14
篇与
设计模式
相关的结果
2025-01-22
设计模式(十二)——策略模式
新专题:设计模式,我会在博客(http://www.hollischuang.com)及微信公众号(hollischuang)同步更新,欢迎共同学习。 前几篇文章主要介绍了几种创建型模式,本文开始介绍行为型模式。首先介绍一个比较简单的设计模式——策略模式。 概念 学习过设计模式的人大概都知道Head First设计模式这本书,这本书中介绍的第一个模式就是策略模式。把策略模式放在第一个,笔者认为主要有两个原因:1、这的确是一个比较简单的模式。2、这个模式可以充分的体现面向对象设计原则中的封装变化、多用组合,少用继承、针对接口编程,不针对实现编程等原则。 策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。 用途 结合策略模式的概念,我们找一个实际的场景来理解一下。 假设我们是一家新开的书店,为了招揽顾客,我们推出会员服务,我们把店里的会员分为三种,分别是初级会员、中级会员和高级会员。针对不同级别的会员我们给予不同的优惠。初级会员买书我们不打折、中级会员买书我们打九折、高级会员买书我们打八折。 我们希望用户在付款的时候,只要刷一下书的条形码,会员再刷一下他的会员卡,收银台的工组人员就能直接知道应该向顾客收取多少钱。 在不使用模式的情况下,我们可以在结算的方法中使用if/else语句来区别出不同的会员来计算价格。 但是,如果我们有一天想要把初级会员的折扣改成9.8折怎么办?有一天我要推出超级会员怎么办?有一天我要针对中级会员可打折的书的数量做限制怎么办? 使用if\else设计出来的系统,所有的算法都写在了一起,只要有改动我就要修改整个类。我们都知道,只要是修改代码就有可能引入问题。为了避免这个问题,我们可以使用策略模式。。。 对于收银台系统,计算应收款的时候,一个客户只可能是初级、中级、高级会员中的一种。不同的会员使用不同的算法来计算价格。收银台系统其实不关心具体的会员类型和折扣之间的关系。也不希望会员和折扣之间的任何改动会影响到收银台系统。 在介绍策略模式的具体实现方式之前,再来巩固一下几个面向对象设计原则:封装变化、多用组合,少用继承、针对接口编程,不针对实现编程。想一想如何运用到策略模式中,并且有什么好处。 实现方式 策略模式包含如下角色: Context: 环境类 Strategy: 抽象策略类 ConcreteStrategy: 具体策略类 我们运用策略模式来实现一下书店的收银台系统。我们可以把会员抽象成一个策略类,不同的会员类型是具体的策略类。不同策略类里面实现了计算价格这一算法。然后通过组合的方式把会员集成到收银台中。 先定义一个接口,这个接口就是抽象策略类,该接口定义了计算价格方法,具体实现方式由具体的策略类来定义。 /** * Created by hollis on 16/9/19. 会员接口 */ public interface Member { /** * 计算应付价格 * @param bookPrice 书籍原价(针对金额,建议使用BigDecimal,double会损失精度) * @return 应付金额 */ public double calPrice(double bookPrice); } 针对不同的会员,定义三种具体的策略类,每个类中都分别实现计算价格方法。 /** * Created by hollis on 16/9/19. 初级会员 */ public class PrimaryMember implements Member { @Override public double calPrice(double bookPrice) { System.out.println("对于初级会员的没有折扣"); return bookPrice; } } /** * Created by hollis on 16/9/19. 中级会员,买书打九折 */ public class IntermediateMember implements Member { @Override public double calPrice(double bookPrice) { System.out.println("对于中级会员的折扣为10%"); return bookPrice * 0.9; } } /** * Created by hollis on 16/9/19. 高级会员,买书打八折 */ public class AdvancedMember implements Member { @Override public double calPrice(double bookPrice) { System.out.println("对于中级会员的折扣为20%"); return bookPrice * 0.8; } } 上面几个类的定义体现了封装变化的设计原则,不同会员的具体折扣方式改变不会影响到其他的会员。 定义好了抽象策略类和具体策略类之后,我们再来定义环境类,所谓环境类,就是集成算法的类。这个例子中就是收银台系统。采用组合的方式把会员集成进来。 /** * Created by hollis on 16/9/19. 书籍价格类 */ public class Cashier { /** * 会员,策略对象 */ private Member member; public Cashier(Member member){ this.member = member; } /** * 计算应付价格 * @param booksPrice * @return */ public double quote(double booksPrice) { return this.member.calPrice(booksPrice); } } 这个Cashier类就是一个环境类,该类的定义体现了多用组合,少用继承、针对接口编程,不针对实现编程两个设计原则。由于这里采用了组合+接口的方式,后面我们在推出超级会员的时候无须修改Cashier类。只要再定义一个SuperMember implements Member 就可以了。 下面定义一个客户端来测试一下: /** * Created by hollis on 16/9/19. */ public class BookStore { public static void main(String[] args) { //选择并创建需要使用的策略对象 Member strategy = new AdvancedMember(); //创建环境 Cashier cashier = new Cashier(strategy); //计算价格 double quote = cashier.quote(300); System.out.println("高级会员图书的最终价格为:" + quote); strategy = new IntermediateMember(); cashier = new Cashier(strategy); quote = cashier.quote(300); System.out.println("中级会员图书的最终价格为:" + quote); } } //对于中级会员的折扣为20% //高级会员图书的最终价格为:240.0 //对于中级会员的折扣为10% //中级会员图书的最终价格为:270.0 从上面的示例可以看出,策略模式仅仅封装算法,提供新的算法插入到已有系统中,策略模式并不决定在何时使用何种算法。在什么情况下使用什么算法是由客户端决定的。 策略模式的重心 策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。 算法的平等性 策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正因为这个平等性,才能实现算法之间可以相互替换。所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。 所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现。 运行时策略的唯一性 运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态地在不同的策略实现中切换,但是同时只能使用一个。 公有的行为 经常见到的是,所有的具体策略类都有一些公有的行为。这时候,就应当把这些公有的行为放到共同的抽象策略角色Strategy类里面。当然这时候抽象策略角色必须要用Java抽象类实现,而不能使用接口。(《JAVA与模式》之策略模式) 策略模式的优缺点 优点 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到父类里面,从而避免代码重复。 使用策略模式可以避免使用多重条件(if-else)语句。多重条件语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重条件语句里面,比使用继承的办法还要原始和落后。 缺点 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。 由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。可以通过使用享元模式在一定程度上减少对象的数量。 文中所有代码见GitHub 参考资料 《JAVA与模式》之策略模式
技术
# 设计模式
酷游
1月22日
0
8
0
2025-01-22
设计模式(七)——抽象工厂模式
新专题:设计模式,我会在博客(http://www.hollischuang.com)及微信公众号(hollischuang)同步更新,欢迎共同学习。 在设计模式(五)——工厂方法模式和设计模式(六)——JDK中的那些工厂方法中介绍了工厂方法模式。本文将介绍另外一种工厂模式——抽象工厂模式。 工厂模式的简单回顾 在介绍了简单工厂模式和工厂方法模式之后,相信很多人对工厂模式(如果单独提到工厂模式,即包括简单工厂模式、工厂方法模式及本文即将介绍的抽象工厂模式)的思想都有了一定的理解。这里我们在简单回顾一下。 工厂模式的主要功能就是帮助我们实例化对象的。之所以名字中包含工厂模式四个字,是因为对象的实例化过程是通过工厂实现的,是用工厂方法代替new操作的。 这样做的好处是封装了对象的实例化细节,尤其是对于实例化较复杂或者对象的生命周期应该集中管理的情况。会给你系统带来更大的可扩展性和尽量少的修改量。 简单工厂模式的优缺点 优点: 1、屏蔽产品的具体实现,调用者只关心产品的接口。 2、实现简单 缺点: 1、增加产品,需要修改工厂类,不符合开放-封闭原则 2、工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则 工厂方法模式的优缺点 优点: 1、继承了简单工厂模式的优点 2、符合开放-封闭原则 缺点: 1、增加产品,需要增加新的工厂类,导致系统类的个数成对增加,在一定程度上增加了系统的复杂性。 本文要介绍了抽象工厂模式,是在简单工厂模式和工厂方法模式的基础上衍生出的另外一种创建型设计模式。 概念 抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。 抽象工厂模式提供了一种方式,可以将同一产品族的单独的工厂封装起来。在正常使用中,客户端程序需要创建抽象工厂的具体实现,然后使用抽象工厂作为接口来创建这一主题的具体对象。客户端程序不需要知道(或关心)它从这些内部的工厂方法中获得对象的具体类型,因为客户端程序仅使用这些对象的通用接口。抽象工厂模式将一组对象的实现细节与他们的一般使用分离开来。 产品族 来认识下什么是产品族: 位于不同产品等级结构中,功能相关的产品组成的家族。如下面的例子,就有两个产品族:跑车族和商务车族。 用途 抽象工厂模式和工厂方法模式一样,都符合开放-封闭原则。但是不同的是,工厂方法模式在增加一个具体产品的时候,都要增加对应的工厂。但是抽象工厂模式只有在新增一个类型的具体产品时才需要新增工厂。也就是说,工厂方法模式的一个工厂只能创建一个具体产品。而抽象工厂模式的一个工厂可以创建属于一类类型的多种具体产品。工厂创建产品的个数介于简单工厂模式和工厂方法模式之间。 在以下情况下可以使用抽象工厂模式: 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。 系统中有多于一个的产品族,而每次只使用其中某一产品族。 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。 实现方式 抽象工厂模式包含如下角色: AbstractFactory(抽象工厂):用于声明生成抽象产品的方法 ConcreteFactory(具体工厂):实现了抽象工厂声明的生成抽象产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中; AbstractProduct(抽象产品):为每种产品声明接口,在抽象产品中定义了产品的抽象业务方法; Product(具体产品):定义具体工厂生产的具体产品对象,实现抽象产品接口中定义的业务方法。 本文的例子采用一个汽车代工厂造汽车的例子。假设我们是一家汽车代工厂商,我们负责给奔驰和特斯拉两家公司制造车子。我们简单的把奔驰车理解为需要加油的车,特斯拉为需要充电的车。其中奔驰车中包含跑车和商务车两种,特斯拉同样也包含奔驰车和商务车。 以上场景,我们就可以把跑车和商务车分别对待,对于跑车有单独的工厂创建,商务车也有单独的工厂。这样,以后无论是再帮任何其他厂商造车,只要是跑车或者商务车我们都不需要再引入工厂。同样,如果我们要增加一种其他类型的车,比如越野车,我们也不需要对跑车或者商务车的任何东西做修改。 下面是抽象产品,奔驰车和特斯拉车: public interface BenzCar { //加汽油 public void gasUp(); } public interface TeslaCar { //充电 public void charge(); } 下面是具体产品,奔驰跑车、奔驰商务车、特斯拉跑车、特斯拉商务车: public class BenzSportCar implements BenzCar { public void gasUp() { System.out.println("给我的奔驰跑车加最好的汽油"); } } public class BenzBusinessCar implements BenzCar{ public void gasUp() { System.out.println("给我的奔驰商务车加一般的汽油"); } } public class TeslaSportCar implements TeslaCar { public void charge() { System.out.println("给我特斯拉跑车冲满电"); } } public class TeslaBusinessCar implements TeslaCar { public void charge() { System.out.println("不用给我特斯拉商务车冲满电"); } } 下面是抽象工厂: public interface CarFactory { public BenzCar getBenzCar(); public TeslaCar getTeslaCar(); } 下面是具体工厂: public class SportCarFactory implements CarFactory { public BenzCar getBenzCar() { return new BenzSportCar(); } public TeslaCar getTeslaCar() { return new TeslaSportCar(); } } public class BusinessCarFactory implements CarFactory { public BenzCar getBenzCar() { return new BenzBusinessCar(); } public TeslaCar getTeslaCar() { return new TeslaBusinessCar(); } } “开闭原则”的倾斜性 “开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的。对于涉及到多个产品族与多个产品等级结构的系统,其功能增强包括两方面: 增加产品族:对于增加新的产品族,工厂方法模式很好的支持了“开闭原则”,对于新增加的产品族,只需要对应增加一个新的具体工厂即可,对已有代码无须做任何修改。 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,不能很好地支持“开闭原则”。 抽象工厂模式的这种性质称为“开闭原则”的倾斜性,抽象工厂模式以一种倾斜的方式支持增加新的产品,它为新产品族的增加提供方便,但不能为新的产品等级结构的增加提供这样的方便。 三种工厂模式之间的关系 当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式; 抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构。 当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。 总结 抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。 抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。 抽象工厂模式的主要优点是隔离了具体类的生成,使得客户并不需要知道什么被创建,而且每次可以通过具体工厂类创建一个产品族中的多个对象,增加或者替换产品族比较方便,增加新的具体工厂和产品族很方便;主要缺点在于增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对“开闭原则”的支持呈现倾斜性。 文中所有代码见GitHub 参考资料 大话设计模式 深入浅出设计模式 抽象工厂模式(Factory Method Pattern)
技术
# 设计模式
酷游
1月22日
0
16
0
2025-01-22
设计模式(十四)——JDK中的迭代器模式
新专题:设计模式,我会在博客(http://www.hollischuang.com)及微信公众号(hollischuang)同步更新,欢迎共同学习。 上一篇介绍了迭代器模式,而且我们也提到,迭代器模式在JAVA的很多集合类中应用广泛,本文就来看看JAVA源码中是如何使用迭代器模式的。 以下这段代码是JAVA中比较常见的,使用迭代器来遍历List: List list = new ArrayList(); Iterator iterator = list.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } 在上一篇文章中,我们介绍过迭代器模式中包括的几个角色: Iterator 抽象迭代器 ConcreteIterator 具体迭代器 Aggregate 抽象容器 Concrete Aggregate 具体容器 我们看看ArrayList在使用迭代器的时候是否也包含这些角色。 首先,具体的容器就不用说了,ArrayList类本身就是这个具体的容器类。我们看得到,ArrayList中包含一个iterator方法,该方法用来返回一个具体的迭代器。 在介绍这个具体的迭代器的实现之前,我们先来找找这个迭代器模式的实现中是否包含抽象迭代器和抽象容器。 抽象容器比较好找,只要我们找到这个iterator方法在哪个接口中定义的就可以了。这里直接给出答案:java.util.Iterable。 抽象迭代器就是java.util.Iterator 当我们在使用JAVA开发的时候,想使用迭代器模式的话,只要让我们自己定义的容器类实现java.util.Iterable并实现其中的iterator方法使其返回一个java.util.Iterator的实现类就可以了。 我们接下来看一下ArrayList中的iterator方法是如何实现的,直接贴源码(jdk1.8.0_73): public Iterator iterator() { return new Itr(); } private class Itr implements Iterator { 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; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer
技术
# 设计模式
酷游
1月22日
0
9
0
2025-01-22
设计模式(二)——单例模式
新专题:设计模式,我会在博客(http://www.hollischuang.com)及微信公众号(hollischuang)同步更新,欢迎共同学习。 设计模式(一)——设计模式概述中简单介绍了设计模式以及各种设计模式的基本概念,本文主要介绍单例设计模式。包括单例的概念、用途、实现方式、如何防止被序列化破坏等。 概念 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式。在 GOF 书中给出的定义为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。 单例模式一般体现在类声明中,单例的类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。 用途 单例模式有以下两个优点: 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)。 避免对资源的多重占用(比如写文件操作)。 有时候,我们在选择使用单例模式的时候,不仅仅考虑到其带来的优点,还有可能是有些场景就必须要单例。比如类似”一个党只能有一个主席”的情况。 实现方式 我们知道,一个类的对象的产生是由类构造函数来完成的。如果一个类对外提供了public的构造方法,那么外界就可以任意创建该类的对象。所以,如果想限制对象的产生,一个办法就是将构造函数变为私有的(至少是受保护的),使外面的类不能通过引用来产生对象。同时为了保证类的可用性,就必须提供一个自己的对象以及访问这个对象的静态方法。 饿汉式 下面是一个简单的单例的实现: //code 1 public class Singleton { //在类内部实例化一个实例 private static Singleton instance = new Singleton(); //私有的构造函数,外部无法访问 private Singleton() { } //对外提供获取实例的静态方法 public static Singleton getInstance() { return instance; } } 使用以下代码测试: //code2 public class SingletonClient { public static void main(String[] args) { SimpleSingleton simpleSingleton1 = SimpleSingleton.getInstance(); SimpleSingleton simpleSingleton2 = SimpleSingleton.getInstance(); System.out.println(simpleSingleton1==simpleSingleton2); } } 输出结果: true code 1就是一个简单的单例的实现,这种实现方式我们称之为饿汉式。所谓饿汉。这是个比较形象的比喻。对于一个饿汉来说,他希望他想要用到这个实例的时候就能够立即拿到,而不需要任何等待时间。所以,通过static的静态初始化方式,在该类第一次被加载的时候,就有一个SimpleSingleton的实例被创建出来了。这样就保证在第一次想要使用该对象时,他已经被初始化好了。 同时,由于该实例在类被加载的时候就创建出来了,所以也避免了线程安全问题。(原因见:在深度分析Java的ClassLoader机制(源码级别)、Java类的加载、链接和初始化) 还有一种饿汉模式的变种: //code 3 public class Singleton2 { //在类内部定义 private static Singleton2 instance; static { //实例化该实例 instance = new Singleton2(); } //私有的构造函数,外部无法访问 private Singleton2() { } //对外提供获取实例的静态方法 public static Singleton2 getInstance() { return instance; } } code 3和code 1其实是一样的,都是在类被加载的时候实例化一个对象。 饿汉式单例,在类被加载的时候对象就会实例化。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。而且,如果这个类被多次加载的话也会造成多次实例化。其实解决这个问题的方式有很多,下面提供两种解决方式,第一种是使用静态内部类的形式。第二种是使用懒汉式。 静态内部类式 先来看通过静态内部类的方式解决上面的问题: //code 4 public class StaticInnerClassSingleton { //在静态内部类中初始化实例对象 private static class SingletonHolder { private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton(); } //私有的构造方法 private StaticInnerClassSingleton() { } //对外提供获取实例的静态方法 public static final StaticInnerClassSingleton getInstance() { return SingletonHolder.INSTANCE; } } 这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉式不同的是(很细微的差别):饿汉式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉式更加合理。 懒汉式 下面看另外一种在该对象真正被使用的时候才会实例化的单例模式——懒汉模式。 //code 5 public class Singleton { //定义实例 private static Singleton instance; //私有构造方法 private Singleton(){} //对外提供获取实例的静态方法 public static Singleton getInstance() { //在对象被使用的时候才实例化 if (instance == null) { instance = new Singleton(); } return instance; } } 上面这种单例叫做懒汉式单例。懒汉,就是不会提前把实例创建出来,将类对自己的实例化延迟到第一次被引用的时候。getInstance方法的作用是希望该对象在第一次被使用的时候被new出来。 有没有发现,其实code 5这种懒汉式单例其实还存在一个问题,那就是线程安全问题。在多线程情况下,有可能两个线程同时进入if语句中,这样,在两个线程都从if中退出的时候就创建了两个不一样的对象。(这里就不详细讲解了,不理解的请恶补多线程知识)。 线程安全的懒汉式 针对线程不安全的懒汉式的单例,其实解决方式很简单,就是给创建对象的步骤加锁: //code 6 public class SynchronizedSingleton { //定义实例 private static SynchronizedSingleton instance; //私有构造方法 private SynchronizedSingleton(){} //对外提供获取实例的静态方法,对该方法加锁 public static synchronized SynchronizedSingleton getInstance() { //在对象被使用的时候才实例化 if (instance == null) { instance = new SynchronizedSingleton(); } return instance; } } 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的延迟加载,但是,遗憾的是,他效率很低,因为99%情况下不需要同步。(因为上面的synchronized的加锁范围是整个方法,该方法的所有操作都是同步进行的,但是对于非第一次创建对象的情况,也就是没有进入if语句中的情况,根本不需要同步操作,可以直接返回instance。) 双重校验锁 针对上面code 6存在的问题,相信对并发编程了解的同学都知道如何解决。其实上面的代码存在的问题主要是锁的范围太大了。只要缩小锁的范围就可以了。那么如何缩小锁的范围呢?相比于同步方法,同步代码块的加锁范围更小。code 6可以改造成: //code 7 public class Singleton { private static Singleton singleton; private Singleton() { } public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } } code 7是对于code 6的一种改进写法,通过使用同步代码块的方式减小了锁的范围。这样可以大大提高效率。(对于已经存在singleton的情况,无须同步,直接return)。 但是,事情这的有这么容易吗?上面的代码看上去好像是没有任何问题。实现了惰性初始化,解决了同步问题,还减小了锁的范围,提高了效率。但是,该代码还存在隐患。隐患的原因主要和Java内存模型(JMM)有关。考虑下面的事件序列: 线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。 由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。 线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有穿过B使用的内存(缓存一致性)),程序很可能会崩溃。 (上面的例子不太能理解的同学,请恶补JAVA内存模型相关知识) 在J2SE 1.4或更早的版本中使用双重检查锁有潜在的危险,有时会正常工作(区分正确实现和有小问题的实现是很困难的。取决于编译器,线程的调度和其他并发系统活动,不正确的实现双重检查锁导致的异常结果可能会间歇性出现。重现异常是十分困难的。) 在J2SE 5.0中,这一问题被修正了。volatile关键字保证多个线程可以正确处理单件实例 所以,针对code 7 ,可以有code 8 和code 9两种替代方案: 使用volatile //code 8 public class VolatileSingleton { private static volatile VolatileSingleton singleton; private VolatileSingleton() { } public static VolatileSingleton getSingleton() { if (singleton == null) { synchronized (VolatileSingleton.class) { if (singleton == null) { singleton = new VolatileSingleton(); } } } return singleton; } } 上面这种双重校验锁的方式用的比较广泛,他解决了前面提到的所有问题。但是,即使是这种看上去完美无缺的方式也可能存在问题,那就是遇到序列化的时候。详细内容后文介绍。 使用final //code 9 class FinalWrapper { public final T value; public FinalWrapper(T value) { this.value = value; } } public class FinalSingleton { private FinalWrapper helperWrapper = null; public FinalSingleton getHelper() { FinalWrapper wrapper = helperWrapper; if (wrapper == null) { synchronized (this) { if (helperWrapper == null) { helperWrapper = new FinalWrapper(new FinalSingleton()); } wrapper = helperWrapper; } } return wrapper.value; } } 枚举式 在1.5之前,实现单例一般只有以上几种办法,在1.5之后,还有另外一种实现单例的方式,那就是使用枚举: // code 10 public enum Singleton { INSTANCE; Singleton() { } } 这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象(下面会介绍),可谓是很坚强的壁垒啊,在深度分析Java的枚举类型—-枚举的线程安全性及序列化问题中有详细介绍枚举的线程安全问题和序列化问题,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过,但是不代表他不好。 单例与序列化 在单例与序列化的那些事儿一文中,Hollis就分析过单例和序列化之前的关系——序列化可以破坏单例。要想防止序列化对单例的破坏,只要在Singleton类中定义readResolve就可以解决该问题: //code 11 package com.hollis; import java.io.Serializable; /** * Created by hollis on 16/2/5. * 使用双重校验锁方式实现单例 */ public class Singleton implements Serializable{ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } private Object readResolve() { return singleton; } } 总结 本文中介绍了几种实现单例的方法,主要包括饿汉、懒汉、使用静态内部类、双重校验锁、枚举等。还介绍了如何防止序列化破坏类的单例性。 从单例的实现中,我们可以发现,一个简单的单例模式就能涉及到这么多知识。在不断完善的过程中可以了解并运用到更多的知识。所谓学无止境。 文中所有代码见GitHub 参考资料 单例模式的七种写法 双重检查锁定模式 深入浅出设计模式 单例模式
技术
# 设计模式
酷游
1月22日
0
17
0
2025-01-22
设计模式(一)——设计模式概述
新专题:设计模式,我会在博客(http://www.hollischuang.com)及微信公众号(hollischuang)同步更新,欢迎共同学习。 在软件工程中,设计模式(design pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。设计模式并不是固定的一套代码,而是针对某一特定问题的具体解决思路与方案。可以认为是一种最佳实践,因为他是无数软件开发人员经过长时间的实践总结出来的。 提到设计模式不得不提《设计模式:可复用面向对象软件的基础》(Design Patterns – Elements of Reusable Object-Oriented Software) 一书。这本书由著名的四人帮——GoF(Gang of Four)编写,其中总结了23种设计模式,并将他们分成几个大类。 设计模式的六大原则 1、开闭原则(Open Close Principle) 开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。 2、里氏代换原则(Liskov Substitution Principle) 里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。 3、依赖倒转原则(Dependence Inversion Principle) 这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。 4、接口隔离原则(Interface Segregation Principle) 这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。 5、迪米特法则(最少知道原则)(Demeter Principle) 为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 6、合成复用原则(Composite Reuse Principle) 合成复用原则是尽量使用合成/聚合的方式,而不是使用继承。 其中前四种也是面向对象的四个基本原则。 设计模式分类 设计模式分为三种类型,共23种。 创建型模式:单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。 行为型模式:模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)、访问者模式。 23种设计模式简单介绍 按字典序排列简介如下。 Abstract Factory(抽象工厂模式):提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。 Adapter(适配器模式):将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 Bridge(桥接模式):将抽象部分与它的实现部分分离,使它们都可以独立地变化。 Builder(建造者模式):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 Chain of Responsibility(责任链模式):为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。 Command(命令模式):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。 Composite(组合模式):将对象组合成树形结构以表示“部分-整体”的层次结构。它使得客户对单个对象和复合对象的使用具有一致性。 Decorator(装饰模式):动态地给一个对象添加一些额外的职责。就扩展功能而言, 它比生成子类方式更为灵活。 Facade(外观模式):为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 Factory Method(工厂模式):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。Factory Method使一个类的实例化延迟到其子类。 Flyweight(享元模式):运用共享技术有效地支持大量细粒度的对象。 Interpreter(解析器模式):给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。 Iterator(迭代器模式):提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。 Mediator(中介模式):用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。 Memento(备忘录模式):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。 Observer(观察者模式):定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。 Prototype(原型模式):用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。 Proxy(代理模式):为其他对象提供一个代理以控制对这个对象的访问。 Singleton(单例模式):保证一个类仅有一个实例,并提供一个访问它的全局访问点。 单例模式是最简单的设计模式之一,但是对于Java的开发者来说,它却有很多缺陷。在九月的专栏中,David Geary探讨了单例模式以及在面对多线程(multi-threading)、类装载器(class loaders)和序列化(serialization)时如何处理这些缺陷。 State(状态模式):允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。 Strategy(策略模式):定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法的变化可独立于使用它的客户。 Template Method(模板方法模式):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 Visitor(访问者模式):表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。 23种设计模式之间的关系 图片来自网络,侵删
技术
# 设计模式
酷游
1月22日
0
6
0
1
2
3
下一页
易航博客