第2章 创建和销毁对象

本章的主题是创建和销毁对象:何时以及如何创建对象,何时以及如何避免创建对象,如何确保它们能够适时地销毁,以及如何管理对象销毁之前必须进行的各种清理动作。


第一条:用静态工厂方法代替构造器

类为了让客户端获取它自身的一个实例,最传统的方法是提供一个公有的构造器。除此之外,类可以提供一个公有的静态工厂方法,它只是一个返回类的实例的静态方法。

/**
 * 一个来自Boolean(基本类型boolean的装箱类)的简单示例,
 * 该方法将boolean基本类型值转换成了一个Boolean对象引用。
 */
public static Boolean vauleOf(boolean b) {
    return b ? Boolean.TURE : Boolean.FALSE;
}

提供静态工厂方法而不是公有的构造器,这样做既有优势,也有劣势。

静态工厂方法与构造器不同的第一大优势在于,它们有名称。如果构造器的参数本身没有确切地描述正被返回的对象,那么具有适当名称的静态工厂会更容易使用,产生的客户端代码也更易于阅读。
方法的名字和参数列表称为方法的签名,一个类只能有一个带有指定签名的构造器。编程人员通常知道如何避开这一限制:通过提供两个构造器,它们的参数列表只在参数类型的顺序上有所不同。

public class Person {
    private String name;
    private String gender;
    private int age;

    public Person() {
        //TODO Auto-generated constructor stub
    }

    /**
     * 构造器1和构造器2不可同时存在,构造器1和构造器3可以同时存在。
     * 即使方法括号中的参数个数和类型完全一样,只要参数类型的顺序不同,
     * 也是不同的参数类型列表。  
     */
 
    //构造器1
    public Person(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    } 

    //构造器2
    public Person(String gender, String name, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }

    //构造器3
    public Person(String name, int age, String gender) {     
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
}

然而,用户永远也记不住该用哪个构造器,并且在读到使用了这些构造器的代码时,如果没有参考类的文档,往往不知所云。
由于静态工厂方法有名称,所以它们不受上述限制。当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且仔细地选择名称以便突出静态工厂方法之间的区别。

静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新对象。 这使得不可变类可以使用预先构建好的实例,或者将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。
静态工厂方法能够为重复的调用返回相同的对象,这样有助于类总能严格控制在某个时刻哪些实例因该存在。这种类被称作实例受控的类,编写实例受控的类有几个原因,实例受控使得类可以确保它是一个Singleton或者是不可实例化的。它还是得不可变的值类可以确保不会存在两个相等的实例,即当且仅当a == b时,a.equals(b)才为true。这是享元模式的基础。枚举(enum)类型保证了这一点。

静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象。这样我们在选择返回对象的类时就有了更大的灵活性。
这种灵活性的一种应用是,API可以返回对象,同时又不会使对象的类变成公有的。以这种方式隐藏实现类会使API变得非常简洁。

静态工厂方法与构造器不同的第四大优势在于,所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。只要是已声明的返回类型的子类型,都是允许的。返回对象的类也可能随着发行版本的不同而不同。

静态工厂方法与构造器不同的第五大优势在于,方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。这种灵活的静态工厂方法构成了服务提供者框架的基础。服务提供者框架:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把他们从多个实现中解耦出来。

组件说明
服务接口提供者实现的。
提供者注册API提供者用来注册实现的。
服务访问API客户端用来获取服务的实例。
是客户端用来指定某种选择实现的条件,如果没有这样的规定,API就会返回默认实现的一个实例,或者允许客户端遍历所有可用的实现。
是“灵活的静态工厂”,构成了服务提供者框架的基础。
服务提供者接口可选。
表示产生服务接口之实例的工厂对象。
如果没有服务提供者接口,实现就通过反射方式进行实例化。

静态工厂方法的主要缺点在于,类如果不含公有的或者受保护的构造器,就不能被子类化。

静态工厂方法的第二个缺点在于,程序员很难发现它们。

惯用的静态工厂方法名称:

  • from——类型转换方法,它只有单个参数,返回该类型的一个相对应的实例。
  • of——聚合方法,带有多个参数,返回该类型的一个实例,把它们合并起来。
  • valueOf——比from和of更繁琐的一种替代方法。
  • instance或者getInstance——返回的实例是通过方法的(如有)参数来描述的,但是不能说与参数具有同样的值。
  • create或者newInstance——像instance或者getInstance一样,但create或者newInstance能够确保每次调用都返回一个新的实例。
  • getType——像getInstance一样,但是在工厂方法处于不同的类中的时候使用,Type表示工厂方法所返回的对象类型。
  • newType——像newInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。
  • type——getType和newType的简版。

第二条:遇到多个构造器参数时要考虑使用构建器

静态工厂和构造器有个共同的局限性:它们都不能很好地拓展到大量的可选参数。

解决方法:

① 程序员一向习惯采用重叠构造器模式。在这种模式下,提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,以此类推,最后一个构造器包含所有可选的参数。

public class NutritionFacts {
    private final int servingSize;   //(mL)             required
    private final int servings;      //(per container)  required 
    private final int calories;      //(per serving)    optional 
    private final int fat;           //(g/serving)      optional 
    private final int sodium;        //(mg/serving)     optional 
    private final int carbohydrate;  //(g/serving)      optional 

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories) {
        this(servingSize, servings, calories, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize  = servingSize;
        this.servings     = servings;
        this.calories     = calories;
        this.fat          = fat;
        this.sodium       = sodium;
        this.carbohydrate = carbohydrate;
    }
}

当你要创建实例的时候,就利用参数列表最短的构造器,但该列表中包含了要设置的所有参数:

NutritionFacts cocaCola =
    new NutritionFacts(240, 8, 100, 0, 35, 27);

这个构造器调用通常需要许多你本不想设置的参数,但还是不得不为它们传递值。
简而言之,重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然较难以阅读。

② 遇到许多可选的构造器参数的时候,还有第二种代替办法,即JavaBeans模式,在这种模式下,先调用一个无参构造器来创建对象,然后在调用setter方法来设置每个必要的参数,以及每个相关的可选参数:

public class NutritionFacts {
    private int servingSize  = -1;   //Required; no default value
    private int servings     = -1;   //Required; no default value
    private int calories     = 0;
    private int fat          = 0;
    private int sodium       = 0;
    private int carbohydrate = 0;

    public NutritionFacts() {}

    //Setters
    public void setServingSize(int val)  { servingSize = val; }
    public void setServings(int val)     { servings = val; }
    public void setCalories(int val)     { calories = val; }
    public void setFat(int val)          { fat = val; }
    public void setSodium(int val)       { sodium = val; }
    public void setCarbohydrate(int val) { carbohydrate = val; }
}

这种模式弥补了重叠构造器模式的不足,创建实例很容易,这样产生的代码读起来也很容易:

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

遗憾的是,JavaBeans模式自身有着很严重的缺点。因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。类无法仅仅通过检验构造器参数的有效性来保证一致性。试图使用处于不一致状态的对象会导致失败,这种失败与包含错误的代码大相径庭,因此调试起来十分困难。与此相关的另一点不足在于,JavaBeans模式使得把类做成不可变的可能性不复存在,这就需要程序员付出额外的努力来确保它的线程安全。

发表评论

电子邮件地址不会被公开。 必填项已用*标注