java面试题:讲一讲了解的的设计模式

设计模式

一. 单例模式

1.1 单例模式是什么

  • 单例模式保证整个应用中某个实例有且只有一个,即类似于一个全局的静态变量,所有线程获取的都是一个实例。保证一个类仅有一个实例,并提供一个访问它的全局访问点。

1.2 单例模式的应用场景

  1. Windows的Task Manager(任务管理器)就是很典型的单例模式。我们无法打开两个任务管理器
  2. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。

当然肯定还有很多应用场景

1.3 单例模式的五种基本实现

  • 单例模式的五种基本实现:饱汉模式,饿汉模式,DCL双锁检查,静态内部类,枚举类。
  • 我的这篇博客中有讲解这五种实现:
    单例模式五种实现方式——java

1.4 Spring中的单例模式实现

  • Spring中实现单例模式使用的并不是上述的五种,而是使用单例注册表实现的
  • 单例注册表实际上就是一个map,使用的是ConcurrentHashMap实现的,里面存放了已经实例过的单例bean。
  • Spring实现单例的过程如下:
    • 首先从单例注册表中查找是否已经存放单例实例,如果有的话就直接取出,然后返回。
    • 如果没有的话就进入synchronized修饰的同步方法
    • 再次检查单例注册表是否存在该bean的实例,如果有就取出返回
    • 如果没有就开始创建该bean的实例,返回
  • 简而言之就是用一个map存放注册过的单例bean,每次需要获取单例的时候就从里面取,没有的话就再创建。
  • 使用ConcurrentHashMap的原因是为了保证线程安全,同时两次检查单例注册表也是保证线程安全。类似于DCL双锁检查。

下面为部分源码和注释,是搬运别人的:

public abstract class AbstractBeanFactory implements ConfigurableBeanFactory{  
   /** 
    * 充当了Bean实例的缓存,实现方式和单例注册表相同 
    */  
   private final Map singletonCache=new HashMap();  
   public Object getBean(String name)throws BeansException{  
       return getBean(name,null,null);  
   }  
...  
   public Object getBean(String name,Class requiredType,Object[] args)throws BeansException{  
      //对传入的Bean name稍做处理,防止传入的Bean name名有非法字符(或则做转码)  
      String beanName=transformedBeanName(name);  
      Object bean=null;  
      //手工检测单例注册表  
      Object sharedInstance=null;  
      //使用了代码锁定同步块,原理和同步方法相似,但是这种写法效率更高  
      synchronized(this.singletonCache){  
         sharedInstance=this.singletonCache.get(beanName);  
       }  
      if(sharedInstance!=null){  
         ...  
         //返回合适的缓存Bean实例  
         bean=getObjectForSharedInstance(name,sharedInstance);  
      }else{  
        ...  
        //取得Bean的定义  
        RootBeanDefinition mergedBeanDefinition=getMergedBeanDefinition(beanName,false);  
         ...  
        //根据Bean定义判断,此判断依据通常来自于组件配置文件的单例属性开关  
        //<bean id="date" class="java.util.Date" scope="singleton"/>  
        //如果是单例,做如下处理  
        if(mergedBeanDefinition.isSingleton()){  
           synchronized(this.singletonCache){  
            //再次检测单例注册表  
             sharedInstance=this.singletonCache.get(beanName);  
             if(sharedInstance==null){  
                ...  
               try {  
                  //真正创建Bean实例  
                  sharedInstance=createBean(beanName,mergedBeanDefinition,args);  
                  //向单例注册表注册Bean实例  
                   addSingleton(beanName,sharedInstance);  
               }catch (Exception ex) {  
                  ...  
               }finally{  
                  ...  
              }  
             }  
           }  
          bean=getObjectForSharedInstance(name,sharedInstance);  
        }  
       //如果是非单例,即prototpye,每次都要新创建一个Bean实例  
       //<bean id="date" class="java.util.Date" scope="prototype"/>  
       else{  
          bean=createBean(beanName,mergedBeanDefinition,args);  
       }  
}  
...  
   return bean;  
}  
} 

二.代理模式

2.1 代理模式概述

定义:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。

通俗的来讲代理模式就是我们生活中常见的中介。例如我要买房,那么我就要调用这个已有的买房的方法,但是呢买房之前还需要看房,看环境等一系列操作,因此为了省事,我就使用代理去调用这个买房的方法,但是使用代理不同的是,代理不光是仅仅调用买房的方法,买房之前还会执行看房的操作。

如下图所示:

客户类就是我,接口就是开放的买房的接口,委托类中就有买房的方法。
在这里插入图片描述

2.2 代理模式的作用

也许有人会说了,我不用代理,我就喜欢先去看房,看环境。这样难道不行吗?实际上是可以的,但是放到一个应用上的话就不是这么简单的问题了。

如果现在已经有一套买房系统(不使用代理),一般客户就是直接买房。但是如果客户突然有一天有看房看环境的需求怎么办?那么就必须修改委托类中的源码,增加功能方法,然后客户类进行调用。

但是如果我们之前就使用代理的方式进行买房,那么此时我完全不需要修改委托类,而是修改代理类,在代理类中增加功能即可,对客户而言还是调用代理,没有任何的改变。对应用程序而言,没有修改内部源码。

因此代理模式的作用有两个。

- 中介隔离作用:

在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。

- 符合代码的开闭原则——对外扩展开放,对内修改关闭

代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。

2.3 代理模式的分类及原理

代理模式分为静态代理和动态代理,动态代理中又有两种代理方法实现:JDK和CGLib。

2.3.1 静态代理

静态代理的意思就是程序员自己写一个代理类去实现代理功能。也就是说在程序运行之前这个类的class文件就已经存在了。

优点:

  • 可以做到在符合开闭原则的情况下对目标对象进行功能扩展。

缺点:

  • 我们得为接口中每一个服务方法都得创建代理类,如果接口中的服务方法增加了,就必须再新增一个代理类来代理这个方法。工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。

2.4.1 动态代理

刚刚我们提到静态代理是要提前为每一个服务方法写好代理类,并且服务方法增加的话,就需要增加代理类来代理,这样的工作量太大了,因此动态代理应运而生。

动态代理实现的功能就是,通过编写一个动态代理器,某个方法需要代理的时候,我才会创建这个代理类去代理该方法。

说的再简单一点,我现在A类中的B方法需要被代理了,我只需要将A类和B方法传入这个动态代理器,动态代理器就会自动创建一个代理类来代理这个方法。

这里需要注意的是:静态代理和动态代理中的jdk代理都有一个要求,那就是被代理的委托类必须要实现接口。因此对于没有实现接口的委托类,如果需要被代理的话,就可以使用CGLib代理。

2.4.1.1 JDK代理原理

JDK代理是程序执行过程中,使用JDK的反射机制,创建代理类对象,生成一个实现代理接口的匿名类,然后重写方法,实现方法的增强。

什么是反射?

反射是可以在运行时期动态获取任何类的信息,如属性和方法.
反射是可以在运行时期动态调用任何类的属性和方法.
这种动态调用获取信息和动态调用类的方法叫做反射.

注意:JDK动态代理只能对实现了接口的类生成代理,而不能针对类

为什么JDK动态代理只能支持接口代理?

答:之所以 JDK 的动态代理只能通过接口实现,是因为通过JDK动态代理生成的代理类默认已经继承了 Proxy 类,因此只能实现代理我们传入的一堆接口;由于 Java 是单继承的,所以 JDK 动态代理只能代理接口,接口可以实现多个,但是类只能继承实现一个。

再说简单一点就是java不支持多继承。

2.4.1.1 CGLib代理原理

Cglib是针对类实现代理,主要是对指定的类的字节码生成一个子类,覆盖其中的方法,并覆盖其中方法的增强,但是因为采用的是继承,所以该类或方法最好不要生成final,对于final类或方法,是无法继承的。

说简单一点CGLib代理是直接通过操作类的字节码,来创建一个子类。

2.5 JDK代理和CGLib代理的区别

原理不同:
1、JDK动态代理使用的是反射的原理,创建一个代理类,然后重写方法,实现方法的增强。但是运行时因为是基于反射,调用后续的类操作会比较慢。
2、CGLIB是基于继承机制,继承被代理类,所以方法不要声明为final,然后重写父类方法达到增强了类的作用。它底层是基于asm第三方框架,是对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。生成类的速度慢,但是后续执行类的操作时候很快。

简单来说就是:jdk使用反射,CGLib使用字节码编程。

局限性不同:

1、JDK动态代理只能对实现了接口的类生成代理,而不能针对类
2、Cglib是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法的增强,但是因为采用的是继承,所以该类或方法最好不要生成final,对于final类或方法,是无法继承的。

也就是说如果一个类实现了接口,那么可以使用JDK代理或者Cglib代理。如果没有实现接口,则只能使用Cglib代理。

简单来说就是:jdk针对接口,Cglib针对类

效率方面:
1、cglib底层是ASM字节码生成框架,但是字节码技术生成代理类,在JDL1.6之前比使用java反射的效率要高
2、在jdk6之后逐步对JDK动态代理进行了优化,在调用次数比较少时效率高于cglib代理效率
3、只有在大量调用的时候cglib的效率高,但是在1.8的时候JDK的效率已高于cglib
4、Cglib不能对声明final的方法进行代理,因为cglib是动态生成代理对象,final关键字修饰的类不可变只能被引用不能被修改

因此就现在而言:jdk的动态代理效率不低于Cglib。

2.6 spring如何抉择代理使用

1、当bean实现接口时,会用JDK代理模式
2、当bean没有实现接口,用cglib实现
3、可以强制使用cglib(在spring配置中加入<aop:aspectj-autoproxy proxyt-target-class=”true”/>)

2.7 代理模式的应用场景

2.8 代理模式的原理

三.工厂模式

四.装饰器模式

五.观察者模式

六.责任链模式

七.模板方法模式

通用模板类图如下:

在这里插入图片描述

模板方法模式实现起来很简单,实际上使用的是java的继承机制。一共分为两个部分:抽象模板类和具体实现类。

  • 抽象模板类:其实就是抽象类,里面包括基本方法模板方法
  • 具体实现类:继承抽象模板类,实现其中的基本方法

为了防止恶意的操作,一般模板方法上都添加上final关键字,不允许被覆写。下面放上一个别人博客的例子:

这是一个悍马车的模型:

在这里插入图片描述

抽象类实现:

public abstract class HummerModel {
	public abstract void start(); //发动
	public abstract void stop();  //停止
	public abstract void alarm(); //鸣笛
	public abstract void engineBoom(); //轰鸣
	public final void run() { //车总归要跑
		this.start();
		this.engineBoom();
		this.alarm();
		this.stop();
	}
}

两个悍马实现类:

//悍马H1
public class HummerH1 implements HummerModel {
 
	@Override
	public void start() {
		System.out.println("H1发动……");
	}
 
	@Override
	public void stop() {
		System.out.println("H1停止……");
	}
 
	@Override
	public void alarm() {
		System.out.println("H1鸣笛……");
	}
 
	@Override
	public void engineBoom() {
		System.out.println("H1轰鸣……");
	}
	
}
 
//悍马H2
public class HummerH2 implements HummerModel {
 
    @Override
    public void start() {
        System.out.println("H2发动……");
    }
 
    @Override
    public void stop() {
        System.out.println("H2停止……");
    }
 
    @Override
    public void alarm() {
        System.out.println("H2鸣笛……");
    }
 
    @Override
    public void engineBoom() {
        System.out.println("H2轰鸣……");
    }
 
  

这里也许就很奇怪为什么单独的run方法要放在抽象类,因为两个实现类中如果都重新实现run方法,实际上这两个run方法是一样的。所以就可以直接放到抽象类中。这其实就是抽象类存在的意义。

参考博客:https://blog.csdn.net/eson_15/article/details/51323902

八.策略模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值