EffectiveJava Item3:使用私有constructor 或者enum实现单例

在EffectiveJava Item3 中,对于单例(Singleton),做出了一个解释:单例就是一个只能实例化(instantiate) 一次,且恰好一次的类。
   单例模式的缺点,在于难以测试其客户端(client),对于singleton类不能采用一个mock的实现,除非单例类实现了一个接口(interface)。

一、JDK 1.5之前的单例实现方法
    JDK 1.5 release之前,一般有两种方法,都是采用保持constructor私有和公开一个static memeber来提供solo instance 访问。
    第一种方法:public static final member方法
[Java]  纯文本查看  复制代码
?
1
2
3
4
5
6
7
8
//Singleton with public final field
  public class Elvis{
    public static final Elvis INSTANCE= new Elvis();
   private Elvis(){ //...}
   public void leaveTheBuilding(){...}
 
 
}



    第二种方法:static factory:

[Java]  纯文本查看  复制代码
?
1
2
3
4
5
6
7
8
9
     public class Elvis{
     private static final Elvis INSTANCE= new Elvis();
     private Elvis(){...}
     public static Elvis getInstance(){ return INSTANCE}
     
     public void leaveTheBuilding(){...}
 
 
}


  这两种方法比较起来,public field 方法的优势在于Singleton的声明清晰明了:它提供了一个public static final的属性,因此总是包含同一个对象的引用。缺点在于,没有任何performance的优势。当代JVM,对于static factory method的调用(call),会采用内联(inline)的方式
在说内联函数之前,先说说函数的调用过程。 

    调用某个函数实际上将程序执行顺序转移到该函数所存放在内存中某个地址,将函数的程序内容执行完后,再返回到 
转去执行该函数前的地方。这种转移操作要求在转去前要保护现场并记忆执行的地址,转回后先要恢复现场,并按原来保 
存地址继续执行。也就是通常说的压栈和出栈。因此,函数调用要有一定的时间和空间方面的开销。那么对于那些函数体 
代码不是很大,又频繁调用的函数来说,这个时间和空间的消耗会很大。 

    那怎么解决这个性能消耗问题呢,这个时候需要引入内联函数了。内联函数就是在程序编译时,编译器将程序中出现 
的内联函数的调用表达式用内联函数的函数体来直接进行替换。显然,这样就不会产生转去转回的问题,但是由于在编译 
时将函数体中的代码被替代到程序中,因此会增加目标程序代码量,进而增加空间开销,而在时间代销上不象函数调用时 
那么大,可见它是以目标代码的增加为代价来换取时间的节省。 

    在大学里学习写C代码时,我们都学到将一些简短的逻辑定义在宏里。这样做的好处是,在编译器编译的时候会将用 
到该宏的地方直接用宏的代码替换。这样就不再需要象调用方法那样的压栈、出栈,传参了。性能上提升了。内联函数的 
处理方式与宏类似,但与宏又有所不同,内联函数拥有函数的本身特性(类型、作用域等等) 

    写过C++代码的应该都知道,在C++里有个内联函数,使用inline关键字修饰。另外,写在Class定义内的函数也会被 
编译器视为内联函数。 

那么,在java中的内联函数长什么模样呢?在java中使用final关键字来指示一个函数为内联函数
public final void method1() {   
   //TODO something   
}

这个指示并不是必需的。final关键字只是告诉编译器,在编译的时候考虑性能的提升,可以将final函数视为内联函数。 
但最后编译器会怎么处理,编译器会分析将final函数处理为内联和不处理为内联的性能比较了。
  
Java不存在“嵌入”(inline)方法。Java编译器也许会自行决定嵌入一个方法,但我们对此没有更多的控制权力。在Java中,可为一个方法使用final关键字,从而“建议”进行嵌入操作。然而,嵌入函数对于C++的编译器来说也只是一种建议。

java的成员函数,无论是否内联的,其定义都是在类定义的代码中,在c++里面只有inline函数才是这样。在java中,区别内联与否就要靠final关键字了,加上了final关键字,就可以“建议”编译器对此方法进行嵌入操作。究竟编译器是否会真的嵌入,这取决于它的判断,final关键字只是代表写程序的人的一种建议,拿主意的还是编译器。



嵌入操作是什么样的操作?

参数入栈,执行函数代码的副本。和c++是一样的



final关键字:

1. final关键字加上以后,成员函数就被锁定,不会在继承中被重载。(这和“建议”嵌入操作是两回事)

2. private成员函数隐含为final的,当然如果愿意给private成员再加上final关键字也是允许的,只是不会有什么额外的效果。

3. 因为private成员函数其实并不能作为继承的接口,因此并不存在怎么处置重载private final函数的问题。

4. final关键字加在了某类上,则意味着不允许此类被修改被继承;加上了final的类,其中的函数成员也都隐含成为final的,无需再分别给函数加上final关键字了。
    另外,static factory  method的另外一个优势,就是后期可以灵活改变是否是单例类,而不用修改已经暴露的API。

     当面临serialiable时,仅仅考加上一个Serialiable接口是不合适的, 因为readObject()方法总是返回一个新的实例对象,就像java中的构造器一样。你可以使用readResolve()方法来避免这种情况, 需要将每一个field都添加一个关键字:transient,并提供一个readResolve方法, 通过像下面的例子中这样用单例来替换新创建的实例:
  
[Java]  纯文本查看  复制代码
?
1
2
3
4
//readResolve to prevent another instance of Singleton
    private Object readResolve(){
        return INSTANCE;
    }


二、JDK 1.5 之后的enum 实现Singleton
    在JDK1.5之后,可以通过enum来实现单例:

[Java]  纯文本查看  复制代码
?
1
2
3
4
5
     //Enum singleton -the preferred approach
    public enum Elvis{
     INSTANCE;
     public void leaveTheBuilding(){...}
}

       这是迄今为止最大的优点,如果你曾经在java5之前写过单例模式实现代码,那么你会知道即使是使用双检锁你有时候也会返回不止一个实例对象。虽然这种问题通过改善java内存模型和使用volatile变量可以解决,但是这种方法对于很多初学者来说写起来还是很棘手。相比用synchronization的双检索实现方式来说,枚举单例就简单多了。你不相信?比较一下下面的双检索实现代码和枚举实现代码就知道了。
用枚举实现的单例:
这是我们通常写枚举单例的方式,它可能包含实例变量和实例方法,但是简单来说我什么都没用,需要注意的是如果你使用实例方法,你就需要确保方法的线程安全性,避免它会影响对象的状态。通常情况下枚举里面创建实例是线程安全的,但是其它的方法就需要编程者自己去考虑了。
/**
* Singleton pattern example using Java Enumj
*/
public enum EasySingleton{
    INSTANCE;
}
代码就这么简单,你可以使用EasySingleton.INSTANCE调用它,比起你在单例中调用getInstance()方法容易多了。

你可能感兴趣的:(JEE)