设计模式常见问答

1、举例java中什么时候用重载?什么时候用重写?

答:

  • 重载是指方法名相同,参数个数不同或者返回值类型不同或者有不同的访问修饰符、可以抛出不同的异常。

  • 重写是指在子类中将父类的成员方法名保留,但是重写成员方法的内容,子类函数的访问修饰权限不能小于父类的,构造方法不能被重写。

  • 使用场景:
    1) 例如项目中经常需要做日期格式转换,要写一个返回日期格式的函数,根据不同的参数返回不同的日期格式

      public  String getDay() {
      	return formatDate(new Date(), "yyyy-MM-dd");
      }
      public  String getDay(Date date) {
      	return formatDate(date, "yyyy-MM-dd");
      }
    

2)当要去扩展或更改之前的某个类的功能时候可以用重写, 遵循开闭原则,例如在controller进行返回数据时,之前老版本里面需要返回的Map中有data,code,messgae。现在新版本里面需要再增加返回值的长度size这个属性,就可以使用重写父类的方法了。

2、举例一个更倾向于用抽象类而不是接口的业务场景?

答: 抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么

应用场景:

  • 当需要去携程、去哪儿等在线旅游网站上面爬取酒店用户评论的时候,需要先调用这些网站的数据接口,然后获取数据,其次解析成我们需要的数据,然后进行敏感词过滤,最后按照固定的格式存储到自己的数据库中。调用数据接口和获取数据以及过滤并存储到mysql中这4个步骤都是相同的,所以可以打在一个模板中,不需要子类去实现,只有解析数据的过程需要子类实现。

3、在java中为什么不允许从静态方法中访问非静态变量?

答:

  • 一个方法用static修饰,便是静态方法或类方法。静态方法不属于特定对象。
  • java中软件设计原则是类的初始化顺序是先属性后方法、先静态后动态,从上往下的顺序进行初始化,所以静态方法先优于非静态变量之前初始化,访问一个还没有初始化的变量,显然是不合理的。
  • java中类的方法池中都有一个隐含的参数this,他表示本对象的引用,但是static方法是没有这个隐含参数的,因为static方法和类的实例无关
    ,它只在类装载的时候初始化。由于static方法在装载class时先完成的,比构造方法早,此时非static属性和方法还没有完成初始化。 所以,在static方法内部无法直接调用非static方法。

4、软件架构中的上层应用是指哪些层,客户端属于上层应用吗?

答:

  • 软件架构是一系列相关的抽象模式,是根据要解决的问题,对目标系统的边界进行界定,在工作中常见的软件架构模式有以下几种

    1) 分层模式
    2) 客户端/服务器模式
    3) 主/从模式
    4) MVC模式
    5) 代理模式
    6) 对等模式
    

例如我们使用最经典的就是MVC模式,MVC 模式通过将内部信息表示、用户信息呈现以及用户操作接收分开的方式解耦组件,实现高效代码重用。

  • 上层应用是相对的一个概念,例如对于一个MVC模式的架构来说一般会是展示层的组件,例如app,UI,pc端等,主要是一个展示的壳;对于主/从模式的软件架构来说,例如数据库的同步主从复制,上层应用是指主库。

  • 客户端是否属于上层应用其实要根据你的目标系统边界来确定的,例如对于客户端/服务器模式的架构设计来说,客户端就是上层应用,对于对等模式的软件架构,例如网络文件共享、基于RTSP的流媒体应用,这种在运行过程中可以动态地改变其角色,既可以单独做为客户端或服务端运行,又可同时作为客户端与服务端运行,这种时候客户端是不属于上层应用。

  • 所以,具体“上层”指那些层,取决于架构分层的结构,被依赖的层总是依赖方的底层。


5、总结设计模式分类中的创建型、结构型、行为型之间的根本区别

  • 对设计模式进行分类的目的是为了区分通过指定的模式用来完成什么样的工作

  • 创建型模式重点关注的是这个对象是如何被创建出来的,表示的是一个从哪里来的过程;例如有工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式

  • 结构型模式重点关注的是处理类或对象的组合,如何将类或者对象组合在一起形成更大的结构,就像搭积木,可以通过简单积木的组合形成复杂的,功能更强大的结构。简单来说就是表示的是一个人和其他人的关系的一个过程;常见的有装饰器模式、代理模式、适配器模式

  • 行为型模式重点关注类和对象之间的具体行为,行为型模式帮助回答了“软件组件是如何运行的?”,例如去做什么,如何去执行这个方法,doXXX,常见的有策略模式,模板方法模式、观察者模式

6、单例模式的双重检查锁为何要做2次非空检测?

答:

  • 第一重检查的目的是避免对除第一次调用外的所有调用都实行同步的昂贵代价。同步的代价在不同的 JVM 间是不同的。在早期,代价相当高。随着更高级的 JVM 的出现,同步的代价降低了,但出入synchronized 方法或块仍然有性能损失。
  • 第二重检查是为了考虑这样一种情况,就是有两个线程同时到达,即同时调用getInstance() 方法,此时由于singleTon== null ,所以很明显,两个线程都可以通过第一重的 singleTon== null ,进入第一重 if语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 singleTon== null ,而另外的一个线程则会在lock 语句的外面等待。而当第一个线程执行完new SingleTon()语句后,便会退出锁定区域,此时,第二个线程便可以进入lock 语句块,此时,如果没有第二重singleTon== null 的话,那么第二个线程还是可以调用new SingleTon()语句,这样第二个线程也会创建一个SingleTon实例,这样也还是违背了单例模式的初衷的,所以这里必须要使用双重检查锁定。
  • 总之:第一层的判断用于提高性能,第二层判断用于做逻辑控制,避免违背单例的原则

7、单例模式双重检查锁中的Synchronized为什么锁住的是class,可以使用int或者Object吗?

答:

  • class在jvm中只装载一次,它有一个缓存机制,针对这个class调用方法的时候,会使用一个全局共享锁,锁住class之后,这个对象的所有操作都是线程安全的,如果锁住class之外的,那么作用范围就会有限,就无法保证作用域是全局的。
  • 由于一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只由一份。所以,一旦一个静态的方法被申明为synchronized。此类所有的实例化对象在调用此方法,共用同一把锁,当一个同步静态方法被调用时,系统获取的其实就是代表该类的类对象的对象锁。
  • synchronized加在非静态方法前和synchronized(this)都是锁住了这个类的对象,如果多线程访问,对象不同,就锁不住,对象固定是一个,就可锁住。
  • Synchronized可以作用在Object上,但是不能支持Int

8、你觉得原型模式能够给你解决的最大麻烦是什么?

答:
我在工作中常将原型模式用于解决以下场景的问题:

  • 1、资源优化场景:需要从A的实例得到一份与A内容相同,但是又互不干扰的实例的话,就需要使用原型模式
  • 2、性能和安全要求的场景:通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式
  • 3、一个对象多个修改者的场景:一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时
  • 举例说明:例如在Spring中,用户也可以采用原型模式来创建新的bean实例,从而实现每次获取的是通过克隆生成的新实例,对其进行修改时对原有实例对象不造成任何影响
  • 总之:对我来说原型模式给我解决的最大的麻烦就是在于一个对象多个修改的场景下,例如工作中做游客画像中,游客来源地排行榜要进行多次修改单不能改变原排行榜时,就对全国省排行榜做了深拷贝然后再做相应的逻辑处理。

9、工厂模式中工厂类一定要私有化构造方法么

答:不一定

  • 是否需要私有化构造方法要根据自己的业务需求来,构造方法私有化之后,在其他类中是不可见的,这个类不能在外部实例化。
  • 如果业务上需要的这个工厂是一个单例工厂,那么就需要私有化构造方法;如果是一个抽象工厂,那么就不能把构造方法私有化
  • 举例说明: 为了使程序能获得代表某个类的Class实例,在Class类中提供了静态工厂方法forName(String name),Class实例是Java虚拟机在加载一个类时自动创建的,程序无法用new语句创建java.lang.Class类的实例,因为Class类没有提供public类型的构造方法。
  • 扩展:单例设计模式核心就是将类的构造方法私有化,之后在类的内部产生实例化对象,并通过类名引用类的静态方法(static)返回实例化对象的引用。

10、如何用最精简的语句描述产品等级和产品族

答:

  • 产品等级结构:如果一系列产品,他们所提供的接口相同,比如都是苹果,但是来自不同的产地(例如中国,日本,美国等),就称为一个产品等级结构
  • 产品族:如果来自同一个产地(例如中国),但是他们每个产品完成的功能接口不同(例如这个产地生成苹果、香蕉、牛奶等),就称为一个产品族

11、实现代理模式的底层逻辑是什么

答:
本质上是使用一个新类来持有被代理类的引用。然后在调用被代理类方法之前或之后添加额外代码进行增强。新类手工写,我们叫简单代理,新类自动生成,我们叫动态代理。jdk动态代理使用反射拿到接口的对象,cglib代理使用asm字节码重组。

12、为什么JDK Proxy一定要目标对象实现接口,而CGLib Proxy对目标对象没有任何要求

答:

  • 通过JDK代理的过程中,会生成对应的代理类形如 P r o x y 0 , Proxy0, Proxy1…这样的匿名类,这些类都是继承Proxy类和实现了我们传入
    的接口,在需要继承proxy类获得有关方法和InvocationHandler构造方法传参的同时,java不能同时继承两个类,我们需要和想要代理的类建立联系,只能实现一个接口
  • 从代理模式的设计来说,充分利用了java的多态特性,也符合基于接口编码的规范
  • 扩展:CGLib Proxy对目标对象是有要求的,cglib代理的类不能为final,否则报错;cglib代理的目标对象的方法如果为final,那么就不会被拦截,即不会执行目标对象额外的业务方法

13、通过绑定方法来实现观察者模式会有什么隐患

答:
观察者模式的两种实现方式:

jdk方式:

  1. 观察者实现Observer接口的update方法,监听被观察者,并做出操作
  2. 被观察者继承Observable类,定义被监听方法,并通知所有观察者

guava方式:

  1. 观察者定义监听方法,参数为被监听事件类型,并添加Subscribe注解,表示该方法为监听方法
  2. 被观察者继承Eventbus类,并添加观察者,被观察者使用post发送事件,当事件类型和观察者监听方法所要求的事件类型一致时,通知成功

带来的隐患:

  • 通过+=去把方法绑定到委托,很容易忘记-=。如果只绑定不移除,这个方法会一直被引用。我们知道GC去回收的时候,只会处理没有被引用的对象,只要是还被引用的对象时不会被回收掉的。所以如果在长期不关闭的系统中(比如监控系统),大量的代码使用+=而不-=,运行时间长以后有可能会内存溢出。所以在使用绑定方法的时候,一定要注意移除方法,否则会导致循环调用。

最后,实际案例:

  • 在工作中,通过观察者模式监听客流预警和舆情预警两个种类的监听,每个预警触发的时候会分别操作三个步骤(提交mq未读消息,保存预警信息到mysql中,发送预警短信),两个种类预警会有不同的执行时间,在并发过大的情况下,执行没有完成,每次监听移除绑定未完成,从而产生了循环调用,就会导致在下一个种类的预警时间周期再次触发,导致上报多次预警通知。

-总之,带来的隐患:一方面要注意的地方就是回调,不要出现递归引用/循环引用,另一方面同一个事件上面绑定的事件不要太多,可能会创建过多的线程,引起过多的资源消耗。

14、什么场景下才应该使用委派模式

答:

  • 委派模式不属于23种设计模式中的一种。委派模式的基本作用就是
    负责任务的调用和分配任务,跟代理模式很像,可以看做是一种特殊情况下的静态代理的全权代理,但是代理模式注重过程,而委派模式注重结果。

  • 如果有一个项目的代码或者功能很杂乱,后期维护的时候会很麻烦,所以在设计阶段把对于比较特殊的功能可以独立出来,分开写,让一个调度器去分开实现这些功能,最后把结果汇总在一起,如果是功能性不多也不复杂的情况下可以不用委派模式

  • 应用场景:在使用“监控宝”对网站性能指标进行监控的时候,可以根据任务类型,例如cpu指标、数据库性能、web页面可达性等不同的监控任务委托给“监控宝”对我们的服务器和网站进行监测,“监控宝”只需要把返回的结果生成一份报告告诉我即可,我不需要关心它是如何去监控这些任务指标的。

  • spring源码中的体现:DispatcherServlet、BeanDefinitionParserDelegate等

15、为什么双亲委派一般用继承来实现

答:

  • 双亲委派模型过程:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

  • 遵循单一职责原则,子类可以直接获得父类的某些属性,这样在使用的时候更加方便

  • 这样做的好处是:一方面防止内存中出现多份同样的字节码,保证了运行的安全性,防止不可信类扮演可信任的类。另一方面可以使层次分明,确保了在各种加载环境的加载顺序

16、描述类适配器和接口适配器的应用场景

答:
类适配器模式:

  • 原理:通过继承来实现适配器功能。
  • 应用场景:适用于系统需要使用现有的类,而这些类的接口不符合系统的需要的情况下。例如当我们要访问的接口A中没有我们想要的方法 ,却在另一个接口B中发现了合适的方法,我们又不能改变访问接口A,在这种情况下,我们可以定义一个适配器p来进行中转,这个适配器p要实现我们访问的接口A,这样我们就能继续访问当前接口A中的方法,然后再继承接口B的实现类BB,这样我们可以在适配器P中访问接口B的方法了,这时我们在适配器P中的接口A方法中直接引用BB中的合适方法,这样就完成了一个简单的类适配器。

接口适配器模式:

  • 原理:通过抽象类来实现适配。
  • 应用场景:适用于一个接口不想使用其所有的方法的情况。例如当存在这样一个接口,其中定义了N多的方法,而我们现在却只想使用其中的一个到几个方法,如果我们直接实现接口,那么我们要对所有的方法进行实现,哪怕我们仅仅是对不需要的方法进行置空(只写一对大括号,不做具体方法实现)也会导致这个类变得臃肿,调用也不方便,这时我们可以使用一个抽象类作为中间件,即适配器,用这个抽象类实现接口,而在抽象类中所有的方法都进行置空,那么我们在创建抽象类的继承类,而且重写我们需要使用的那几个方法即可。
发布了139 篇原创文章 · 获赞 466 · 访问量 86万+

猜你喜欢

转载自blog.csdn.net/sdksdk0/article/details/89702682
今日推荐