1.单例模式
- 在大部分时候,我们把类的构造器定义成public访问权限,允许任何类自由创建该类的对象。但在某些时候,允许其他类自由创建该类的对象没有任何意义,还可能造成系统性能下降(因为创建一个对象的系统开销问题)。例如系统可能只有一个窗口管理器,一个假脱机打印设备或一个数据库引擎访问点,此时如果在系统中为这些类创建多个对象就没有太大的实际意义。
- 如果一个类始终只能创建一个实例,则这个类被称为单例类。
- 总之,在一些特殊场景下,要求不允许自由创建该类的对象,而是只允许为该类创建一个对象。为了避免其他类自由创建该类的实例,我们把该类的构造器使用private修饰,从而把该类的所有构造器隐藏起来。
- 根据良好封装的原则:一旦把该类的构造器隐藏起来,则需要提供一个public方法作为该类的访问点,用于创建该类的对象,且该方法必须使用 static修饰(因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)。
- 除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否曾经创建过对象,也就无法保证只创建一个对象。为此该类需要使用一个属性来保存曾经创建的对象,因为该属性需要被上面的静态方法访问,故该属性必须使用static修饰。
基于上面的介绍,下面的程序创建了一个单例类(懒汉式):
class Singleton{
//使用一个变量来缓存曾经创建的对象,要被getInstance访问,所以static
private static Singleton instance;
//private私有化无参构造
private Singleton(){}
//提供外界接口,静态类方法,类加载的时候调用,可以通过类名直接调用
//调用方法前不存在对象,因此调用方法时不可能是类,只能是对象,所以要加static修饰
public static Singleton getInstance(){
if(instance==null)instance=new Singleton();
return instance;
}
}
public class Main {
public static void main(String[] args){
//测试 instance1是直接创建的,instance2是返回之前创建的
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//返回true
System.out.println(instance1==instance2);
}
}
正是通过上面getInstance方法提供的自定义控制(这也是封装的优势:不允许自由访问类的属性和实现细节,而是通过方法来控制合适暴露),保证Singleton类只能产生一个实例,所以在TestSingleton类的 main方法中看到两次产生的Singleton对象实际上是同一个对象。
- 单例模式主要有4种
- 懒汉式-线程不安全
public class Singleton {
private static Singleton Instance;
private Singleton() {
}
public static Singleton GetInstance() {
if (Instance == null) {
Instance = new Singleton();
}
return Instance;
}
}
优缺点:
私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那
么就不会实例化 uniqueInstance,从而节约资源。
线程不安全,多线程情况下会多次创建实例。
- 饿汉式-线程安全
public class Singleton {
private static Singleton Instance = new Singleton();
private Singleton() {
}
public static Singleton GetInstance() {
return Instance;
}
}
优缺点:
采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。
但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。
- 懒汉式-线程安全
public class Singleton {
private static Singleton Instance;
private Singleton() {
}
public static synchronized Singleton GetInstance() {
if (Instance == null) {
Instance = new Singleton();
}
return Instance;
}
}
- 双重校验锁-线程安全
public class Singleton {
private volatile static Singleton Instance;
private Singleton() {
}
public static Singleton getInstance() {
if (Instance == null) {//先判断实例是否存在,不存在再加锁
synchronized (Singleton.class) {//由于Instace实例有没有被创建过实例不知道,只能对其clss加锁
if (Instance == null) {//当多个线程的时候,只有一个进入,避免多次创建对象!
Instance = new Singleton();
}
}
}
return Instance;
}
}
追问1:这里面试官可能会问使用volatile的好处?
答:Instance 采用 volatile 关键字修饰也是很有必要的, Instance = new Singleton();这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境
下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线
程 T1 执行了 1 和 3,此时 T2 调用 geteInstance() 后发现 Instance 不为空,因此返回
Instance,但此时 Instance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
2. 模板模式
- 抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会大致保留抽象类的行为方式。
- 如果编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,这就是一种模板模式,模板模式也是最常见、最简单的设计模式之一。例如前面介绍的Shape、Circle和Triangle 三个类,已经使用了模板模式。下面再介绍一个模板模式的范例,在这个范例的抽象父类中,父类的普通方法依赖于一个抽象方法,而抽象方法则推迟到子类中提供实现。
//Mian函数可以当作计算具体一种汽车的转速(假如Main是小轿车的意思)
public class Main extends SpeedMeter{
//在小轿车的类里面重写定义它的半径
@Override
public double getRadius() {
return 0.28;
}
public static void main(String[] args){
Main csm = new Main();
//设置转速
csm.setTurnRate(15);
System.out.println(csm.getSpeed());
}
}
//这个是抽象的父类
abstract class SpeedMeter{
//转速
private double turnRate;
public SpeedMeter(){}
//把返回车轮半径的方法定义成抽象方法
public abstract double getRadius();
public void setTurnRate(double turnRate){
this.turnRate=turnRate;
}
//定义计算速度的通用算法
public double getSpeed(){
//速度等于 车轮半径*2*PI*转速
return Math.PI*2*getRadius()*turnRate;
}
}
- 上面程序种SpeedMeter类定义了一个抽象的SpeedMeter类(车速表),该表里定义了一个getSpeed方法,该方法用于返回当前车速,getSpeed方法依赖于getRadius方法的返回值。对于一个抽象的SpeedMeter类而言,它无法确定车轮的半径,因此getRadius方法必须推迟到其子类中实现。
- 上面程序中其子类Main的代码,该类实现了其抽象父类的getRadius()方法,即可创建SpeedMeter类的对象,也可通过该对象来取得当前速度。
- SpeedMeter类里提供了速度表通用算法,但一些具体的实现细节则推迟到其子类Main类中实现。这也是一种典型的模板模式。
- 模板模式在面向对象的软件中很常用。其原理简单,实现也很简单。下面是使用模板模式的一些简单规则:
- 抽象父类可以只定义需要使用的某些方法,其余则留给其子类实现。
- 父类中可能包含需要调用的其他系列方法的方法,这些被调方法既可以由父类实现,也可以由其子类实现。父类里提供的方法只是定义了一个通用算法,其实现也许并不完全由自身实现,而必须依赖于其子类的辅助。
3.命令模式
- 考虑这样一种场景:某个方法需要完成某一个行为,但这个行为的具体实现无法确定,必须等到执行该方法时才可以确定。具体一点:假设有个方法需要遍历某个数组的数组元素,但无法确定在遍历数组元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。
- 这个要求看起来有点奇怪:这个方法不仅需要普通数据可以变化,甚至还有方法执行体也需要变化,难道我们能把“处理行为”作为一个参数传入该方法?
- 对于这样一个需求,我们必须把“处理行为”作为参数传入该方法,这个“处理行为”用编程来实现就是一段代码。那如何把这段代码传入该方法呢?
- 因为Java不允许代码块单独存在,因此我们使用一个Command 接口来定义一个方法,用这个方法来封装“处理行为”。下面是该Command接口代码。
public interface Commond{
//接口里面定义process方法用于封装"处理行为"
void process(int[] target);
}
- 上面的 Command 接口里定义了一个process方法,这个方法用于封装“处理行为”,但这个方法没有方法体——因为现在还无法确定这个处理行为。
- 下面是需要处理数组的处理类,在这个处理类中包含一个process方法,这个方法无法确定处理数组的处理行为,所以定义该方法时使用了一个 Command参数,这个Command参数负责对数组的处理行为。该类的程序代码如下。
public class ProcessArray{
public void process(int[] target,Commond cmd){
cmd.process(target);
}
}
通过一个Command类,就实现了让 ProcessArray类和具体“处理行为”的分离,程序使用Command接口代表了对数组的处理行为。Command接口也没有提供真正的处理,只有等到需要调用ProcessArray对象的process方法时,才真正传入一个 Command对象,才确定对数组的处理行为。
public class TestCommand{
public static void main(String[] args) {
ProcessArray pa = new ProcessArray();
int[] target={-3,-4,5,6};
//第一次处理数组,具体的处理行为取决于PrintCommand
pa.process(target,new PrintCommand());
//第二次处理数组,具体的处理行为取决于AddCommand
pa.process(target,new AddCommand());
}
}
- 两次不同处理行为的效果,也就实现了process方法和“处理行为”的分离,两次不同的处理行为是通过 PrintCommand类和 AddCommand类提供的。下面分别是 PrintCommand类和AddCommand类的代码。
public class PrintCommand implements Command{
public void process(int[] target) {
for(int tmp:target){
System.out.println("迭代输出目标数组的元素"+tmp);
}
}
}
public class AddCommand implements Command{
public void process(int[] target) {
int sum=0;
for(int tmp:target){
sum+=tmp;
}
System.out.println("数组的元素总和是:"+sum);
}
}
对于PrintCommand和 AddCommand两个实现类而言,实际有意义的部分就是process(int[] target)方法,该方法的方法体就是传入ProcessArray类里 process方法的“处理行为”,通过这种方式就可实现process方法和“处理行为”的分离。
4. 简单工厂模式(可以看DateFormate源码就是使用的这种模式)
- 定义:定义一个工厂类,他可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类
- 在简单工厂模式中用于被创建实例的方法通常为静态(static)方法,因此简单工厂模式又被成为静态工厂方法(Static Factory Method),属于类的创建型模式
- 需要什么,只需要传入一个正确的参数,就可以获取所需要的对象,而无需知道其实现过程
- 例如,我开一家披萨店,当客户需要某种披萨并且我这家店里也能做的时候,我就会为其提供所需要的披萨(当然是要钱的哈哈),如果其所需的我这没有,则是另外的情况,后面会谈。这时候,我这家 披萨店就可以看做工厂(Factory),而生产出来的披萨被成为产品(Product),披萨的名称则被称为参数,工厂可以根据参数的不同返回不同的产品,这就是简单工厂模式
- 结构
- Factory(工厂):核心部分,负责实现创建所有产品的内部逻辑,工厂类可以被外界直接调用,创建所需对象
- Product(抽象类产品):工厂类所创建的所有对象的父类,封装了产品对象的公共方法,所有的具体产品为其子类对象
- ConcreteProduct(具体产品):简单工厂模式的创建目标,所有被创建的对象都是某个具体类的实例。它要实现抽象产品中声明的抽象方法(有关抽象类)
- 实现
public class Main{
public static void main(String[] args) {
//测试
Product product=Factory.GetProduct("A");
product.MethodDiff();
Product product0=Factory.GetProduct("B");
product0.MethodDiff();
}
}
//定义一个工厂类
class Factory{
public static Product GetProduct(String arg){
Product product=null;
if(arg.equals("A")){
product = new ProductA();
}else {
product = new ProductB();
}
return product;
}
}
//总的公共的抽象类
abstract class Product{
public void MethodName(){
System.out.println("我是一个产品");
//公共方法实现
}
//声明抽象的业务方法
public abstract void MethodDiff();
}
class ProductA extends Product{
@Override
public void MethodDiff() {
System.out.println("产品A");
}
}
class ProductB extends Product{
@Override
public void MethodDiff() {
System.out.println("产品B");
}
}
- 优点:实现对象的创建和使用分离,创建交给工厂去负责,不关心怎么创建,只关心怎么使用
- 缺点:工厂类不够灵活,新增一个就要修改工厂类,修改判断逻辑,产品很多的话,逻辑将会非常的复杂