11.3.1 单例(Singleton)类
11.3.2 枚举类
11.3.3 不可变(immutable)类与可变类
11.3.4 具有实例缓存的不可变类
11.3.5 松耦合的系统接口
创建类的实例的最常见的方式是用new语句调用类的构造方法。在这种情况下,程序可以创建类的任意多个实例,每执行一条new语句,都会导致Java虚拟机的堆区中产生一个新的对象。假如类需要进一步封装创建自身实例的细节,并且控制自身实例的数目,那么可以提供静态工厂方法。
例如Class实例是Java虚拟机在加载一个类时自动创建的,程序无法用new语句创建java.lang.Class类的实例,因为Class类没有提供public类型的构造方法。为了使程序能获得代表某个类的Class实例,在Class类中提供了静态工厂方法forName(String name),它的使用方式如下:
Class c=Class.forName("Sample"); //返回代表Sample类的实例
静态工厂方法与用new语句调用的构造方法相比,有以下区别。
(1)构造方法的名字必须与类名相同,这一特性的优点是符合Java语言的规范,缺点是类的所有重载的构造方法的名字都相同,不能从名字上区分每个重载方法,容易引起混淆。
静态工厂方法的方法名可以是任意的,这一特性的优点是可以提高程序代码的可读性,在方法名中能体现与实例有关的信息。例如以下例程11-5的Gender类有两个静态工厂方法getFemale()和getMale()。
例程11-5 Gender.java
public class Gender{
private String description;
private static final Gender female=new Gender("女");
private static final Gender male=new Gender("男");
private Gender(String description){this.description=description;}
public static Gender getFemale(){
return female;
}
public static Gender getMale(){
return male;
}
public String getDescription(){return description;}
}
这一特性的缺点是与其他的静态方法没有明显的区别,使用户难以识别类中到底哪些静态方法专门负责返回类的实例。为了减少这一缺点带来的负面影响,可以在为静态工厂方法命名时尽量遵守约定俗称的规范,当然这不是必须的。目前比较流行的规范是把静态工厂方法命名为valueOf或者getInstance:
1)valueOf:该方法返回的实例与它的参数具有同样的值。例如:
Integer a=Integer.valueOf(100); //返回取值为100的Integer对象
从上面代码可以看出,valueOf()方法能执行类型转换操作,在本例中,把int类型的基本数据转换为Integer对象。
2)getInstance:返回的实例与参数匹配,例如:
//返回符合中国标准的日历
Calendar cal=Calendar.getInstance(Locale.CHINA);
(2)每次执行new语句时,都会创建一个新的对象。而静态工厂方法每次被调用的时候,是否会创建一个新的对象完全取决于方法的实现。
(3)new语句只能创建当前类的实例,而静态工厂方法可以返回当前类的子类的实例,这一特性可以在创建松耦合的系统接口时发挥作用,参见本章11.3.5节(松耦合的系统接口)。
静态工厂方法最主要的特点是:每次被调用的时候,不一定要创建一个新的对象。利用这一特点,静态工厂方法可用来创建以下类的实例:
1)单例(Singleton)类:只有惟一的实例的类。
2)枚举类:实例的数量有限的类。
3)具有实例缓存的类:能把已经创建的实例暂且存放在缓存中的类。
4)具有实例缓存的不可变类:不可变类的实例一旦创建,其属性值就不会被改变。
在下面几小节,将结合具体的例子,介绍静态工厂方法的用途。
11.3.1 单例(Singleton)类
单例类是指仅有一个实例的类。在系统中具有惟一性的组件可作为单例类,这种类的实例通常会占用较多的内存,或者实例的初始化过程比较冗长,因此随意创建这些类的实例会影响系统的性能。
熟悉Struts和Hibernate软件的读者会发现,Struts框架的ActionServlet类就是单例类,此外,Hibernate的SessionFactory和Configuration类也是单例类。
以下例程11-6的GlobalConfig类就是个单例类,它用来存放软件系统的配置信息。这些配置信息本来存放在配置文件中,在GlobalConfig类的构造方法中,会从配置文件中读取配置信息,把它存放在properties属性中。
例程11-6 GlobalConfig.java
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
public class GlobalConfig {
private static final GlobalConfig INSTANCE=new GlobalConfig();
private Properties properties = new Properies();
private GlobalConfig(){
try{
//加载配置信息
InputStream in=getClass().getResourceAsStream("myapp.properties");
properties.load(in);
in.close();
}catch(IOException e){throw new RuntimeException("加载配置信息失败");}
}
public static GlobalConfig getInstance(){ //静态工厂方法
return INSTANCE;
}
public Properties getProperties() {
return properties;
}
}
实现单例类有两种方式:
(1)把构造方法定义为private类型,提供public static final类型的静态变量,该变量引用类的惟一的实例,例如:
public class GlobalConfig {
public static final GlobalConfig INSTANCE =new GlobalConfig();
private GlobalConfig() {…}
…
}
这种方式的优点是实现起来比较简洁,而且类的成员声明清楚的表明该类是单例类。
(2)把构造方法定义为private类型,提供public static类型的静态工厂方法,例如:
public class GlobalConfig {
private static final GlobalConfig INSTANCE =new GlobalConfig();
private GlobalConfig() {…}
public static GlobalConfig getInstance(){return INSTANCE;}
…
}
这种方式的优点是可以更灵活的决定如何创建类的实例,在不改变GlobalConfig类的接口的前提下,可以修改静态工厂方法getInstance()的实现方式,比如把单例类改为针对每个线程分配一个实例,参见例程11-7。
例程11-7 GlobalConfig.java
package uselocal;
public class GlobalConfig {
private static final ThreadLocal
new ThreadLocal
private Properties properties = null;
private GlobalConfig(){…}
public static GlobalConfig getInstance(){
GlobalConfig config=threadConfig.get();
if(config==null){
config=new GlobalConfig();
threadConfig.set(config);
}
return config;
}
public Properties getProperties() {return properties; }
}
11.3.2 枚举类
枚举类是指实例的数目有限的类,比如表示性别的Gender类,它只有两个实例:Gender.FEMALE和Gender.MALE,参见例程11-8。在创建枚举类时,可以考虑采用以下设计模式:
1)把构造方法定义为private类型。
2)提供一些public static final类型的静态变量,每个静态变量引用类的一个实例。
3) 如果需要的话,提供静态工厂方法,允许用户根据特定参数获得与之匹配的实例。
例程11-8是改进的Gender类的源程序,它采用了以上设计模式。
例程11-8 Gender.java
import java.io.Serializable;
import java.util.*;
public class Gender implements Serializable {
private final Character sex;
private final transient String description;
public Character getSex() {
return sex;
}
public String getDescription() {
return description;
}
private static final Map
new HashMap
/**
* 把构造方法声明为private类型,以便禁止外部程序创建Gender类的实例
*/
private Gender(Character sex, String description) {
this.sex = sex;
this.description = description;
instancesBySex.put(sex, this);
}
public static final Gender FEMALE =
new Gender(new Character('F'), "Female");
public static final Gender MALE =
new Gender(new Character('M'), "Male");
public static Collection getAllValues() {
return Collections.unmodifiableCollection(instancesBySex.values());
}
/**
* 按照参数指定的性别缩写查找Gender实例
*/
public static Gender getInstance(Character sex) {
Gender result = (Gender)instancesBySex.get(sex);
if (result == null) {
throw new NoSuchElementException(sex.toString());
}
return result;
}
public String toString() {
return description;
}
/**
* 保证反序列化时直接返回Gender类包含的静态实例
*/
private Object readResolve() {
return getInstance(sex);
}
}
在例程11-8的Gender类中,定义了两个静态Gender类型的常量:FEMALE和MALE,它们被存放在HashMap中。Gender类的getInstance(Character sex)静态工厂方法根据参数返回匹配的Gender实例。在其他程序中,既可以通过Gender.FEMALE的形式访问Gender实例,也可以通过Gender类的getInstance(Character sex)静态工厂方法来获得与参数匹配的Gender实例。
以下程序代码演示了Gender类的用法。
public class Person{
private String name;
private Gender gender;
public Person(String name,Gender gender){this.name=name;this.gender=gender;}
//此处省略name和gender属性的相应的public类型的get和set方法
…
public static void main(String args[]){
Person mary=new Person("Mary",Gender.FEMALE);
}
}
也许你会问:用一个int类型的变量也能表示性别,比如用0表示女性,用1表示男性,这样不是会使程序更简洁吗?在以下代码中,gender变量被定义为int类型:
public class Person{
private String name;
private int gender;
public static final int FEMALE=0;
public static final int MALE=1;
public Person(String name,int gender){
if(gender!=0 && gender!=1)throw new IllegalArgumentException("无效的性别");
this.name=name;
this.gender=gender;
}
//此处省略name和gender属性的相应的public类型的get和set方法
public static void main(String args[]){
Person mary=new Person("Mary", FEMALE);
Person tom=new Person("Tom",-1); //运行时抛出IllegalArgumentException
}
}
在以上Person类的构造方法中,gender参数为int类型,编程人员可以为gender参数传递任意的整数值,如果传递的gender参数是无效的,Java编译器不会检查这种错误,只有到运行时才会抛出IllegalArgumentException。
假如使用Gender枚举类,在Person类的构造方法中,gender参数为Gender类型,编程人员只能把Gender类型的实例传给gender参数,否则就通不过Java编译器的类型检查。由此可见,枚举类能够提高程序的健壮性,减少程序代码出错的机会。
假如枚举类支持序列化,那么必须提供readResolve()方法,在该方法中调用静态工厂方法getInstance(Character sex)来获得相应的实例,这可以避免在每次反序列化时,都创建一个新的实例。这条建议也同样适用于单例类。关于序列化和反序列化的概念参见第16章的16.12节(对象的序列化与反序列化)。
11.3.3 不可变(immutable)类与可变类
所谓不可变类,是指当创建了这个类的实例后,就不允许修改它的属性值。在JDK的基本类库中,所有基本类型的包装类,如Integer和Long类,都是不可变类,java.lang.String也是不可变类。以下代码创建了一个String对象和Integer对象,它们的值分别为"Hello"和10,在程序代码中无法再改变这两个对象的值,因为Integer和String类没有提供修改其属性值的接口:
String s=new String("Hello");
Integer i=new Integer(10);
用户在创建自己的不可变类时,可以考虑采用以下设计模式:
1) 把属性定义为private final类型。
2)不对外公开用于修改属性的setXXX()方法。
3)只对外公开用于读取属性的getXXX()方法。
4)在构造方法中初始化所有属性。
5)覆盖Object类的equals()和hashCode()方法,在equals()方法中根据对象的属性值来比较两个对象是否相等,并且保证用equals()方法判断为相等的两个对象的hashCode()方法的返回值也相等,这可以保证这些对象能正确的放到HashMap或HashSet集合中,第15章的15.2.2节(HashSet类)对此做了进一步解释。
6)如果需要的话,提供实例缓存和静态工厂方法,允许用户根据特定参数获得与之匹配的实例,参见本章第
11.3.4节 具有实例缓存的不可变类
下面例程11-9的Name类就是不可变类,它仅仅提供了读取sex和description属性的getXXX()方法,但没有提供修改这些属性的setXXX()方法。
例程11-9 Name.java
public class Name {
private final String firstname;
private final String lastname;
public Name(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public String getFirstname(){
return firstname;
}
public String getLastname(){
return lastname;
}
public boolean equals(Object o){
if (this == o) return true;
if (!(o instanceof Name)) return false;
final Name name = (Name) o;
if(!firstname.equals(name.firstname)) return false;
if(!lastname.equals(name.lastname)) return false;
return true;
}
public int hashCode(){
int result;
result= (firstname==null?0:firstname.hashCode());
result = 29 * result + (lastname==null?0:lastname.hashCode());
return result;
}
public String toString(){
return lastname+" "+firstname;
}
}
假定Person类的name属性定义为Name类型:
public class Person{
private Name name;
private Gender gender;
…
}
以下代码创建了两个Person对象,他们的姓名都是"王小红",一个是女性,一个是男性。在最后一行代码中,把第一个Person对象的姓名改为"王小虹":
Name name=new Name("小红","王");
Person person1=new Person(name,Gender.FEMALE);
Person person2=new Person(name,Gender.MALE);
name=new Name("小虹","王");
person1.setName(name); //修改名字
与不可变类对应的是可变类,可变类的实例的属性是允许修改的。如果把以上例程11-9的Name类的firstname属性和lastname属性的final修饰符去除,并且增加相应的public类型的setFirstname()和setLastname()方法,Name类就变成了可变类。以下程序代码本来的意图也是创建两个Person对象,她们的姓名都是"王小红",接着把第一个Person对象的姓名改为"王小虹":
//假定以下Name类是可变类
Name name=new Name("小红","王");
Person person1=new Person(name,Gender.FEMALE);
Person person2=new Person(name,Gender.MALE);
name.setFirstname("小虹"); //试图修改第一个Person对象的名字
以上最后一行代码存在错误,因为它会把两个Person对象的姓名都改为"王小虹"。由此可见,使用可变类更容易使程序代码出错。因为随意改变一个可变类对象的状态,有可能会导致与之关联的其他对象的状态被错误的改变。
不可变类的实例在整个生命周期中永远保持初始化的状态,它没有任何状态变化,简化了与其他对象之间的关系。不可变类具有以下优点:
1) 不可变类能使程序更加安全,不容易出错。
2)不可变类是线程安全的,当多个线程访问不可变类的同一个实例时,无需进行线程的同步。关于线程安全的概念,参见本书第13章的13.8.4节(线程安全的类)。
由此可见,应该优先考虑把类设计为不可变类,假使必须使用可变类,也应该把可变类的尽可能多的属性设计为不可变的,即用final修饰符来修饰,并且不对外公开用于改变这些属性的方法。
在创建不可变类时,假如它的属性的类型是可变类型,必要的情况下,必须提供保护性拷贝,否则,这个不可变类的实例的属性仍然有可能被错误的修改。这条建议同样适用于可变类中用final修饰的属性。
例如以下例程11-10的Schedule类包含学校的开学时间和放假时间信息,它是不可变类,它的两个属性start和end都是final类型,表示不允许被改变,但是这两个属性都是Date类型,而Date类是可变类。
例程11-10 Schedule.java
import java.util.Date;
public final class Schedule{
private final Date start; //开学时间,不允许被改变
private final Date end; //放假时间,不允许被改变
public Schedule(Date start,Date end){
//不允许放假日期在开学日期的前面
if(start.compareTo(end)>0)
throw new IllegalArgumentException(start +" after " +end);
this.start=start;
this.end=end;
}
public Date getStart(){return start;}
public Date getEnd(){return end;}
}
尽管以上Schedule类的start和end属性是final类型的,由于它们引用Date对象,在程序中可以修改所引用Date对象的属性。以下程序代码创建了一个Schedule对象,接下来把开学时间和放假时间都改为当前系统时间:
Calendar c= Calendar.getInstance();
c.set(2006,9,1);
Date start=c.getTime();
c.set(2007,1,25);
Date end=c.getTime();
Schedule s=new Schedule(start,end);
end.setTime(System.currentTimeMillis()); //修改放假时间
start=s.getStart();
start.setTime(System.currentTimeMillis()); //修改开学时间
为了保证Schedule对象的start属性和end属性值不会被修改,必须为这两个属性使用保护性拷贝,参见例程11-11。
例程11-11 采用了保护性拷贝的Schedule.java
import java.util.Date;
public final class Schedule {
private final Date start;
private final Date end;
public Schedule(Date start,Date end){
//不允许放假日期在开学日期的前面
if(start.compareTo(end)>0)throw new IllegalArgumentException(start +" after " +end);
this.start=new Date(start.getTime()); //采用保护性拷贝
this.end=new Date(end.getTime()); //采用保护性拷贝
}
public Date getStart(){return (Date)start.clone();} //采用保护性拷贝
public Date getEnd(){return (Date)end.clone();} //采用保护性拷贝
}
通过采用保护性拷贝,其他程序无法获得与Schedule对象关联的两个Date对象的引用,因此就无法修改这两个Date对象的属性值。
假如Schedule类中被final修饰的属性所属的类是不可变类,就无需提供保护性拷贝,因为该属性所引用的实例的值永远不会被改变。这进一步体现了不可变类的优点。
11.3.4 具有实例缓存的不可变类
不可变类的实例的状态不会变化,这样的实例可以安全的被其他与之关联的对象共享,还可以安全的被多个线程共享。为了节省内存空间,优化程序的性能,应该尽可能的重用不可变类的实例,避免重复创建具有相同属性值的不可变类的实例。
在JDK1.5的基本类库中,对一些不可变类,如Integer类作了优化,它具有一个实例缓存,用来存放程序中经常使用的Integer实例。JDK1.5的Integer类新增了一个参数为int类型的静态工厂方法valueOf(int i),它的处理流程如下:
if(在实例缓存中存在取值为i的实例)
直接返回这个实例
else{
用new语句创建一个取值为i的Integer实例
把这个实例存放在实例缓存中
返回这个实例
}
在以下程序代码中,分别用new语句和Integer类的valueOf(int i)方法来获得Integer实例:
Integer a=new Integer(10);
Integer b=new Integer(10);
Integer c=Integer.valueOf(10);
Integer d= Integer.valueOf(10);
System.out.println(a==b); //打印false
System.out.println(a==c); //打印false
System.out.println(c==d); //打印true
以上代码共创建了三个Integer对象,参见图11-4。每个new语句都会创建一个新的Integer对象。而Integer.valueOf(10)方法仅仅在第一次被调用时,创建取值为10的Integer对象,第二次被调用时,直接从实例缓存中获得它。由此可见,在程序中用valueOf()静态工厂方法获得Integer对象,可以提高Integer对象的可重用性。
图11-4 引用变量与Integer对象的引用关系
到底如何实现实例的缓存呢?缓存并没有固定的实现方式,完善的缓存实现不仅要考虑何时把实例加入缓存,还要考虑何时把不再使用的实例从缓存中及时清除,以保证有效合理的利用内存空间。一种简单的实现是直接用Java集合来作为实例缓存。本章11.3.2节的例程11-8的Gender类中的Map类型的instancesBySex属性就是一个实例缓存,它存放了Gender.MALE和Gender.FEMALE这两个实例的引用。Gender类的getInstance()方法从缓存中寻找Gender实例,由于Gender类既是不可变类,又是枚举类,因此它的getInstance()方法不会创建新的Gender实例。
以下例程11-12为本章11.3.3节介绍的不可变类Name增加了一些代码,使它拥有了实例缓存和相应的静态工厂方法valueOf()。Name类的实例缓存中可能会加入大量Name对象,为了防止耗尽内存,在实例缓存中存放的是Name对象的软引用(SoftReference)。如果一个对象仅仅持有软引用,Java虚拟机会在内存不足的情况下回收它的内存,本章第11.6节对此作了进一步介绍。
例程11-12 Name.java
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
import java.lang.ref.*;
public class Name {
…
//实例缓存,存放Name对象的软引用
private static final Set
new HashSet
public static Name valueOf(String firstname, String lastname){ //静态工厂方法
Iterator
while(it.hasNext()){
SoftReference
Name name=ref.get(); //获得软引用所引用的Name对象
if(name!=null
&& name.firstname.equals(firstname)
&& name.lastname.equals(lastname))
return name;
}
//如果在缓存中不存在Name对象,就创建该对象,并把它的软引用加入到实例缓存
Name name=new Name(firstname,lastname);
names.add(new SoftReference
return name;
}
public static void main(String args[]){
Name n1=Name.valueOf("小红","王");
Name n2=Name.valueOf("小红","王");
Name n3=Name.valueOf("小东","张");
System.out.println(n1);
System.out.println(n2);
System.out.println(n3);
System.out.println(n1==n2); //打印true
}
}
在程序中,既可以通过new语句创建Name实例,也可以通过valueOf()方法创建Name实例。在程序的生命周期中,对于程序不需要经常访问的Name实例,应该使用new语句创建它,使它能及时结束生命周期;对于经常需要访问的Name实例,那就用valueOf()方法来获得它,因为该方法能把Name实例放到缓存中,使它可以被重用。
从例程11-12的Name类也可以看出,在有些情况下,一个类可以同时提供public的构造方法和静态工厂方法。用户可以根据实际需要,灵活的决定到底以何种方式获得类的实例。
另外要注意的是,没有必要为所有的不可变类提供实例缓存。随意创建大量实例缓存,反而会浪费内存空间,降低程序的运行性能。通常,只有满足以下条件的不可变类才需要实例缓存:
1)不可变类的实例的数量有限。
2)在程序运行过程中,需要频繁访问不可变类的一些特定实例。这些实例拥有与程序本身同样长的生命周期。
11.3.5 松耦合的系统接口
一个类的静态工厂方法可以返回子类的实例,这一特性有助于创建松耦合的系统接口。如果系统规模比较简单,静态工厂方法可以直接作为类本身的静态方法;如果系统规模比较大,根据创建精粒度对象模型的原则,可以把创建特定类的实例的功能专门由一个静态工厂类来负责。第1章的1.6节的例程1-11的ShapeFactory就是一个静态工厂类,它负责构造Shape类的实例。ShapeFacory类有一个静态工厂方法:
public static Shape getShape(int type){…}
以上方法声明的返回类型是Shape类型,实际上返回的是Shape子类的实例。对于Shape类的使用者Panel类,只用访问Shape类,而不必访问它的子类:
//获得一个Circle实例
Shape shape=ShapeFactory.getInstance(ShapeFactory.SHAPE_TYPE_CIRCLE);
在分层的软件系统中,业务逻辑层向客户层提供服务,静态工厂类可以进一步削弱这两个层之间的松耦合关系。例如在以下图11-5中,业务逻辑层向客户层提供ServiceIFC接口,在该接口中声明了所提供的各种服务,它有三个实现类ServiceImpl1、ServiceImpl2和ServiceImpl3类。ServiceFactory静态工厂类负责构造ServiceIFC的实现类的实例,它的定义如下。
public ServiceFactory{
private static final String serviceImpl;
static{
//读取配置信息,根据配置信息设置服务实现类的类型,假定为ServiceImpl1
serviceImpl="ServiceImpl1";
}
public static ServiceIFC getInstance(){
Class.forName(serviceImpl).newInstance();
}
}
图11-5 静态工厂模型
当客户层需要获得业务逻辑层的服务时,先从静态工厂类ServiceFactory中获得ServiceIFC接口的实现类的实例,然后通过接口访问服务:
ServiceIFC service=ServiceFactory.getInstance();
service.service1();
在客户层只会访问ServiceIFC接口,至于业务逻辑层到底采用哪个实现类的实例提供服务,这对客户层是透明的。