创建型[对类的实例化过程的抽象化]、结构型[描述如何将类或对象结合在一起形成更大的结构]、行为型[对在不同的对象之间划分责任和算法的抽象化]
- 工厂模式:工厂类可以根据条件生成不同的子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。
- 代理模式:给一个对象提供一个代理对象,并由代理对象控制原对象的引用。实际开发中,按照使用目的的不同,代理可以分为:远程代理、虚拟代理、保护代理、Cache代理、防火墙代理、同步化代理、智能引用代理。
- 适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。
- 模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。
除此之外,还可以讲讲上面提到的门面模式、桥梁模式、单例模式、装潢模式(Collections工具类和I/O系统中都使用装潢模式)等,反正基本原则就是拣自己最熟悉的、用得最多的作答,以免言多必失。
0.画出最熟悉的三个设计模式的类图
工厂模式
单例模式
适配器模式
https://www.runoob.com/design-pattern/factory-pattern.html
1.你在设计一个工厂的包的时候会遵循哪些原则?
(1)单一职责:每个类的极限原则是职责单一性;
(2)里氏替换原则:凡是父类出现的地方,子类一定适用。在编译器进行语法检查时可查出;
(3)依赖倒置原则:不同层次的功能对接时,必须是接口(抽象类)的对接,减少对具体代码的依赖。对于以后项目扩大,对代码的改动最少。(符合开闭原则);
(4)接口隔离原则:客户端不应该依赖其不需要的接口,类间的依赖关系应该建立在最小的接口上(如订单,门户网站只需要取订单,其它外部系统也只要读取订单,而admin需要对订单的增删改取,这样就可以建三个接口,各自用各自的,而不是用一个庞大臃肿的接口,甚至此接口还可能被污染,做些它本不应该做的事情。因此,使用多个接口比使用一个单一接口要好);
(5)迪米特法则:类应当尽量少的了解其它对象,又叫最少知识原则。具体体现在:只和朋友通信,不要和陌生人直接通信;
(6)开闭原则:是一种极限的理想状态,即只增加代码而不用修改代码。如设计一个Book类,有一个获取价格的方法,而在我们需求发生变化的时候如打折,我们可以写一个子类,重写获取价格的方法覆盖,在接口处直接拿就是。虽然开闭原则无法真正实现,但是在具体实现的过程中,我们只能不断向它靠拢。
2.你能列举一个使用了Visitor/Decorator模式的开源项目/库吗?
(1)Visitor模式的使用:
简介:对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
您在朋友家做客,您是访问者,朋友接受您的访问,您通过朋友的描述,然后对朋友的描述做出一个判断。
主要类:被访问的对象都有一个接受访问者访问的接口;访问者接口实现中写具体的对访问对象操作。
https://www.runoob.com/design-pattern/visitor-pattern.html
DOM4J对Visitor的支持,这样可以大大缩减代码量,并且清楚易懂。了解设计模式的人都知道,Visitor是GOF设计模式之一。其主要原理就是两种类互相保有对方的引用,并且一种作为Visitor去访问许多Visitable。我们来看DOM4J中的Visitor模式(快速文档中没有提供)
只需要自定一个类实现Visitor接口即可。
public class MyVisitor extends VisitorSupport {
public void visit(Element element){
System.out.println(element.getName());
}
public void visit(Attribute attr){
System.out.println(attr.getName());
}
}
// 调用:
root.accept(new MyVisitor())
Visitor接口提供多种Visit()的重载,根据XML不同的对象,将采用不同的方式来访问。上面是给出的Element和Attribute的简单实现,一般比较常用的就是这两个。VisitorSupport是DOM4J提供的默认适配器,Visitor接口的Default Adapter模式,这个模式给出了各种visit(*)的空实现,以便简化代码。
注意,这个Visitor是自动遍历所有子节点的。如果是root.accept(MyVisitor),将遍历子节点。我第一次用的时候,认为是需要自己遍历,便在递归中调用Visitor,结果可想而知。
(2)Decorator模式使用:
InputStream类,详情见问题3
3.你在编码时最常用的设计模式有哪些?在什么场景下用?
(1)适配器模式。多个类不兼容,系统可以建一个适配器,通过适配器选择适合的类的方法来执行。
应用实例:在一次估算房产价格的流程中,有很多不同的测算方法,各个方法之间是相互独立的,那么可以为这些方法建立一个适配器接口,每个测算方法都有一个适配器类(都实现适配器接口),使用时,遍历这些适配器类找到合适的适配器,就能执行相应的方法了。优点在于新增方法时,不需要修改查找方法的代码,符合“开闭原则”。
https://juejin.im/post/5ba28986f265da0abc2b6084#heading-5
缺点: 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构
(2)建造者模式。多个简单的对象一步一步构建成一个复杂的对象。
应用实例: 1、去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的"套餐"。 2、JAVA 中的 StringBuilder。Lombok中的@Builder注解。
实现建造者模式的代码为
public class User {
private Integer id;
private String name;
private String address;
private User() {
}
private User(User origin) {
this.id = origin.id;
this.name = origin.name;
this.address = origin.address;
}
public static User.Builder builder() {
return new User.Builder();
}
public static class Builder {
private User target;
public Builder() {
this.target = new User();
}
public Builder id(Integer id) {
target.id = id;
return this;
}
public Builder name(String name) {
target.name = name;
return this;
}
public Builder address(String address) {
target.address = address;
return this;
}
public User build() {
return new User(target);
}
}
}
如果项目中有使用lombok的话,可以直接使用@Builder注解来实现
import lombok.Builder;
import lombok.ToString;
@ToString
@Builder
public class UserExample {
private Integer id;
private String name;
private String address;
}
使用
UserExample userExample = UserExample.builder()
.id(1)
.name("aaa")
.address("bbb")
.build();
System.out.println(userExample);
(3)装饰者模式
用来修饰对象的。
应用实例:例如一碗豆浆,加不同的配料后价格不一样,不同配料形成了不同的套餐,比如加了火腿的豆浆。装饰者模式与建造者模式的区别在于,建造者用来组装复杂的对象,而装饰者用来修饰一个对象,给对象的属性做一些变更等。
主要涉及到的类有:
网上的例子一看就懂:https://blog.csdn.net/weixin_45393094/article/details/104859456
4.如何实现一个单例?单例模式(懒汉模式,恶汉模式,并发初始化如何解决,volatile与lock的使用)
单例类再整个项目中只能有一个实例。单例类必须自己创建自己的唯一实例。单例类必须给所有其他对象提供这一实例。
(1)懒汉模式:类加载时不初始化
/**
* 非线程安全的懒汉模式
*/
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
/**
* 线程安全的懒汉模式
*/
public class Singleton1 {
private static Singleton1 singleton;
private Singleton1(){}
public static synchronized Singleton1 getSingleton(){
if(singleton == null) {
singleton = new Singleton1();
}
return singleton;
}
}
/**
* synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。
* 因此就有了双重校验锁
* 更高效的线程安全的懒汉模式:双校验
*/
public class SingletonA2 {
private volatile static SingletonA2 instance;
private SingletonA2(){}
public static SingletonA2 getInstance() {
if (instance == null) {
synchronized (SingletonA2.class) {
if (instance == null) {
instance = new SingletonA2();
}
}
}
return instance;
}
}
- 可以看到上面在同步代码块外多了一层instance为空的判断。由于单例对象只需要创建一次,如果后面再次调用getInstance()只需要直接返回单例对象。因此,大部分情况下,调用getInstance()都不会执行到同步代码块,从而提高了程序性能。
- 不过还需要考虑一种情况,假如两个线程A、B,A执行了if (instance == null)语句,它会认为单例对象没有创建,此时线程切到B也执行了同样的语句,B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。为了解决这个问题,还需要在同步代码块中增加if (instance == null)语句,也就是上面看到的代码中的校验2。
- 问题:不使用volitile关键字会出错
- 由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。
- 在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。
- 此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。因此使用了volitile关键字
/**
* 懒汉模式:内置静态类,线程安全
*/
public class SingletonC {
private static class SingletonHolder {
private static SingletonC instance = new SingletonC();
}
public static SingletonC getInstance() {
return SingletonHolder.instance;
}
}
- 内置静态类方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。
- 不一样的是,它是在内部类里面去创建对象实例。这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance
- 这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全
(2)饿汉模式:饿汉模式在类加载的时候就对实例进行创建,是线程安全的,实例在整个程序周期都存在。
/**
* 饿汉模式,线程安全
*/
public class Singleton3 {
private static Singleton3 instance = new Singleton3();
private Singleton3() {
}
public static Singleton3 getInstance() {
return instance;
}
}
(3)枚举写法
单例是如何保证的:
- 在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法。
- 同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
- 也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
/**
* 枚举写法
*/
public enum SingletonD {
INSTANCE;
public void doSomething() {
System.out.println("Hello singleton.");
}
}
https://blog.csdn.net/fly910905/article/details/79286680
5.代理模式(动态代理)
(1)静态代理:代理类的源代码是由程序员编写的,在程序运行前,它的.class文件就已经存在了,这种代理类称为静态代理类
package proxy;
import java.util.Date;
public interface HelloService{
public String echo(String msg);
public Date getTime();
}
package proxy;
import java.util.Date;
public class HelloServiceImpl implements HelloService{
public String echo(String msg){
return "echo:"+msg;
}
public Date getTime(){
return new Date();
}
}
package proxy;
import java.util.Date;
public class HelloServiceProxy implements HelloService{
//表示被代理的HelloService 实例
private HelloService helloService;
public HelloServiceProxy(HelloService helloService){
this.helloService=helloService;
}
public void setHelloServiceProxy(HelloService helloService){
this.helloService=helloService;
}
public String echo(String msg){
//预处理
System.out.println("before calling echo()");
//调用被代理的HelloService 实例的echo()方法
String result=helloService.echo(msg);
//事后处理
System.out.println("after calling echo()");
return result;
}
public Date getTime(){
//预处理
System.out.println("before calling getTime()");
//调用被代理的HelloService 实例的getTime()方法
Date date=helloService.getTime();
//事后处理
System.out.println("after calling getTime()");
return date;
}
}
使用
public class Client1{
public static void main(String args[]){
HelloService helloService=new HelloServiceImpl();
HelloService helloServiceProxy=new HelloServiceProxy(helloService);
System.out.println(helloServiceProxy.echo("hello"));
}
}
运行Client1 类,打印结果如下:
before calling echo()
after calling echo()
echo:hello
(2)动态代理:动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。
package ProxyMode;
/*
* 抽象接口,对应类图中的Subject
*
*/
public interface Subject {
public void SujectShow();
}
package ProxyMode;
public class RealSubject implements Subject{
@Override
public void SujectShow() {
// TODO Auto-generated method stub
System.out.println("杀人是我指使的,我是幕后黑手!By---"+getClass());
}
}
package ProxyMode;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
//建立InvocationHandler用来响应代理的任何调用
public class ProxyHandler implements InvocationHandler {
private Object proxied;
public ProxyHandler( Object proxied )
{
this.proxied = proxied;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("准备工作之前:");
//转调具体目标对象的方法
Object object= method.invoke( proxied, args);
System.out.println("工作已经做完了!");
return object;
}
}
package ProxyMode;
import java.lang.reflect.Proxy;
public class DynamicProxy {
public static void main( String args[] )
{
RealSubject real = new RealSubject();
Subject proxySubject = (Subject)Proxy.newProxyInstance(Subject.class.getClassLoader(),
new Class[]{Subject.class},
new ProxyHandler(real));
proxySubject.SujectShow();;
}
}
测试结果
准备工作之前:
杀人是我指使的,我是幕后黑手!By---class ProxyMode.RealSubject
工作已经做完了!
http://blog.sina.com.cn/s/blog_72893c860100puzg.html
6.JDK源码里面都有些什么让你印象深刻的设计模式使用,举例看看?
https://blog.csdn.net/baiye_xing/article/details/76427717
创作模式
抽象工厂模式 (通过创造性的方法来识别工厂本身,这又可以用于创建另一个抽象/接口类型)
- javax.xml.parsers.DocumentBuilderFactory#newInstance()
- javax.xml.transform.TransformerFactory#newInstance()
- javax.xml.xpath.XPathFactory#newInstance()
建造者模式 (通过创建方法识别返回实例本身)
- java.lang.StringBuilder#append() (非线程安全)
- java.lang.StringBuffer#append() (线程安全)
- java.nio.ByteBuffer#put()(还CharBuffer,ShortBuffer,IntBuffer,LongBuffer
- FloatBuffer和DoubleBuffer)
- javax.swing.GroupLayout.Group#addComponent()
- 所有的实现 java.lang.Appendable
工厂模式 (可通过创建方法识别返回抽象/接口类型的实现)
- java.util.Calendar#getInstance()
- java.util.ResourceBundle#getBundle()
- java.text.NumberFormat#getInstance()
- java.nio.charset.Charset#forName()
- java.net.URLStreamHandlerFactory#createURLStreamHandler(String) (每个协议返回单例对象)
- java.util.EnumSet#of()
- javax.xml.bind.JAXBContext#createMarshaller() 和其他类似的方法
原型模式 (通过创建方法识别,返回具有相同属性的其他实例)
- java.lang.Object#clone()(班必须实施java.lang.Cloneable)
单例模式(通过创造性方法识别,每次返回相同的实例(通常是自己))
- java.lang.Runtime#getRuntime()
- java.awt.Desktop#getDesktop()
- java.lang.System#getSecurityManager()
结构模式
适配器模式 (可通过创建方法识别采用不同抽象/接口类型的实例,并返回自己/另一个抽象/接口类型的实现,其装饰/覆盖给定实例)
- java.util.Arrays#asList()
- java.util.Collections#list()
- java.util.Collections#enumeration()java.io.InputStreamReader(InputStream)(返回a Reader)
- java.io.OutputStreamWriter(OutputStream)(返回a Writer)
- javax.xml.bind.annotation.adapters.XmlAdapter#marshal() 和 #unmarshal()
桥接模式 (可以通过创建方法识别采用不同抽象/接口类型的实例,并返回自己的使用给定实例的抽象/接口类型的实现)
一个虚构的例子将会new LinkedHashMap(LinkedHashSet< K>, List< V>)返回一个不可修改的链接映射,它不会克隆,而是使用它们。该java.util.Collections#newSetFromMap()和singletonXXX()方法却接近。
组合模式 (通过将具有相同抽象/接口类型的实例的行为方法识别为树结构)
- java.awt.Container#add(Component) (几乎全部摆动)
- javax.faces.component.UIComponent#getChildren()
装饰器模式 (通过创作方法识别采用相同抽象/接口类型的实例,添加额外的行为)
- 所有子类java.io.InputStream,OutputStream,Reader并Writer有一个构造函数取相同类型的实例。
- java.util.Collections的checkedXXX(),synchronizedXXX()和unmodifiableXXX()方法。
- javax.servlet.http.HttpServletRequestWrapper 和 HttpServletResponseWrapper
门面模式 (可通过内部使用不同独立抽象/接口类型实例的行为方法识别)
- javax.faces.context.FacesContext,它在内部等使用抽象/接口类型LifeCycle,ViewHandler,NavigationHandler等等而没有终端用户具有担心它(它们然而通过注射覆写投放)。
- javax.faces.context.ExternalContext,其在内部使用ServletContext,HttpSession,HttpServletRequest,HttpServletResponse,等。
享元模式 (使用缓存来加速大量小对象的访问时间)
- java.lang.Integer#valueOf(int)(还Boolean,Byte,Character,Short,Long和BigDecimal)
代理模式 (可通过创建方法识别,该方法返回给定的抽象/接口类型的实现,该类型依次代表/使用给定抽象/接口类型的不同实现)
- java.lang.reflect.Proxy
- java.rmi.*
- javax.ejb.EJB
- javax.inject.Inject
- javax.persistence.PersistenceContext
行为模式
责任链模式 (通过行为方法识别(间接地)在队列中的相同抽象/接口类型的另一个实现中调用相同的方法)
- java.util.logging.Logger#log()
- javax.servlet.Filter#doFilter()
命令模式 (可以通过抽象/接口类型中的行为方法识别,该方法在创建时由命令实现封装的不同抽象/接口类型的实现中调用方法)
- 所有的实现 java.lang.Runnable
- 所有的实现 javax.swing.Action
解释器模式 (通过行为方法识别,返回结构不同的实例/给定实例/类型的类型;请注意,解析/格式化不是模式的一部分,确定模式以及如何应用它)
- java.util.Pattern
- java.text.Normalizer
- 所有子类 java.text.Format
- 所有子类 javax.el.ELResolver
迭代器模式 (可通过行为方法识别,从队列中顺序返回不同类型的实例)
- 所有的实现java.util.Iterator(因此还有java.util.Scanner!)。
- 所有的实现 java.util.Enumeration
中介者模式 (通过采用不同的抽象/接口类型(通常使用命令模式)实例的行为方法来识别给定实例)
- java.util.Timer(所有scheduleXXX()方法)
- java.util.concurrent.Executor#execute()
- java.util.concurrent.ExecutorService(invokeXXX()和submit()方法)
- java.util.concurrent.ScheduledExecutorService(所有scheduleXXX()方法)
- java.lang.reflect.Method#invoke()
备忘录模式 (可以通过内部改变整个实例的状态的行为方法来识别)
- java.util.Date(setter方法这样做,Date内部由一个long值表示)
- 所有的实现 java.io.Serializable
- 所有的实现 javax.faces.component.StateHolder
观察者模式(或发布/订阅) (可以通过行为方法识别,根据自己的状态调用另一个抽象/接口类型的实例上的方法)
- java.util.Observer/ java.util.Observable(很少在现实世界中使用)
- 所有实现java.util.EventListener(因此实际上各地的Swing)
- javax.servlet.http.HttpSessionBindingListener
- javax.servlet.http.HttpSessionAttributeListener
- javax.faces.event.PhaseListener
状态模式 (可以通过行为方法识别,根据可以从外部控制的实例的状态改变其行为)
- javax.faces.lifecycle.LifeCycle#execute()(FacesServlet由此控制,行为取决于JSF生命周期的当前阶段(状态))
策略 (可以通过抽象/接口类型中的行为方法识别,该方法在已经作为方法参数传递到策略实现中的不同抽象/接口类型的实现中调用方法)
- java.util.Comparator#compare(),由其他人执行Collections#sort()。
- javax.servlet.http.HttpServlet,service()所有的doXXX()方法HttpServletRequest
- HttpServletResponse实现者必须处理它们(而不是把它们保持为实例变量!)。
- javax.servlet.Filter#doFilter()
模板方法 (可以由已经具有抽象类型定义的“默认”行为的行为方法识别)
- 所有非抽象方法java.io.InputStream,java.io.OutputStream,java.io.Reader和java.io.Writer。
- 所有非抽象方法java.util.AbstractList,java.util.AbstractSet和java.util.AbstractMap。
- javax.servlet.http.HttpServlet,doXXX()默认情况下,所有方法都会向响应发送HTTP 405“方法不允许”错误。你可以自由地执行任何一个或任何它们。
访问者 (可以通过两种不同的抽象/接口类型识别,它们的方法定义为采用每个其他抽象/接口类型;实际上调用另一个抽象/接口类型的方法,另一个执行所需的策略)
- javax.lang.model.element.AnnotationValue 和 AnnotationValueVisitor
- javax.lang.model.element.Element 和 ElementVisitor
- javax.lang.model.type.TypeMirror 和 TypeVisitor
- java.nio.file.FileVisitor 和 SimpleFileVisitor
7.Reactor模式
Reactor 是一种应用在服务器端的开发模式(也有说法称 Reactor 是一种 IO 模式),目的是提高服务端程序的并发能力。
Reactor 模式的核心思想:减少等待。当遇到需要等待 IO 时,先释放资源,而在 IO 完成时,再通过事件驱动 (event driven) 的方式,继续接下来的处理。从整体上减少了资源的消耗。
8.适配器模式与桥梁模式的区别
答:适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。又称为转换器模式、变压器模式、包装模式(把已有的一些类包装起来,使之能有满足需要的接口)。适配器模式的用意是将接口不同而功能相同或者相近的两个接口加以转换,包括适配器角色补充一些源角色没有但目标接口需要的方法。就像生活中电器插头是三相的,而电源插座是两相的,这时需要一个三相变两相的转换器来满足。
比如,在Java I/O库中使用了适配器模式,象FileInputStream是一个适配器类,其继承了InputStream类型,同时持有一个对FileDiscriptor的引用。这是将一个FileDiscriptor对象适配成InputStrem类型的对象形式的适配器模式。StringReader是一个适配器类,其继承了Reader类型,持有一个对String对象的引用。它将String的接口适配成Reader类型的接口。等等。
桥梁模式的用意是要把实现和它的接口分开,以便它们可以独立地变化。桥梁模式并不是用来把一个已有的对象接到不相匹配的接口上的。当一个客户端只知道一个特定的接口,但是又必须与具有不同接口的类打交道时,就应该使用桥梁模式。
比如,JDBC驱动器就是一个桥梁模式的应用,使用驱动程序的应用系统就是抽象化角色,而驱动器本身扮演实现化角色。应用系统和JDBC驱动器是相对独立的。应用系统动态地选择一个合适的驱动器,然后通过驱动器向数据库引擎发出指令就可以访问数据库中的数据。