Effective Java——创建和销毁对象

来源:互联网 发布:手游源码交易 编辑:程序博客网 时间:2024/04/28 18:45

                       目录
一、考虑用静态工厂方法代替构造器
二、遇到多个构造参数时要考虑用构建器(Builder模式)
三、用私有构造器或者枚举类型强化Singleton属性
四、通过私有构造器强化不可实例化的能力
五、避免创建不必要的对象
六、消除过期的对象引用
七、避免使用终结方法


一、考虑用静态工厂方法代替构造器

        为了让客户端获取类的一个实例,除了使用公有的构造器,类还可以提供一个公有的静态工厂方法,下面这个方法将boolean基本类型值转换成Boolean对象引用:

public static Boolean valueOf(boolean b) {    return b ? Boolean.TRUE : Boolean.FALSE;}

        静态构造方法与构造器不同有以下主要优势:
        1.有意义的名称。当一个类需要多个带有相同签名的构造器时,用静态工厂方法代替构造器,并慎重地选择名称以便突出他们之间的区别。
        2.不必在每次调用他们的时候都创建一个新对象。可以使得不可变类使用预先构建好的实例,或将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。
        3.可以返回原返回类型的任何子类型的对象。在选择返回对象的类时有了更大的灵活性。在Java Collections Framework的集合接口中,提供了大量的静态方法返回集合接口类型的实现类型,如Collections.subList()、Collections.unmodifiableList()等。返回的接口是明确的,然而针对具体的实现类,函数的使用者也无需知晓。
        4.在创建参数化类型实例的时候,它们使代码变得更加简洁。调用参数化类的构造器时,要指明类型参数:

Map<String, String> m = new HashMap<String, String>();

        使用静态工厂方法编译器可以替你找到类型参数,称作类型推导。假设HashMap提供了这个静态工厂方法:

public static <K,V> HashMap<K,V> newInstance() {    return new HashMap<K,V>();}
        可以用下面这句简洁的代码代替:
Map<String,String> m = MyHashMap.newInstance();

        静态工厂方法的缺点:
        1.类如果不含有公有的或者受保护的构造器,就不能被继承
        2.与其他静态方法没有任何区别,Javadoc工具不会注意到静态工厂方法

        静态工厂方法的一些惯用名称:valueOf、of、getInstance、newInstance、getType、newType


二、遇到多个构造参数时要考虑用构建器(Builder模式)

        如果类的构造器或者静态工厂中具有多个参数,设计这种类时,一种选择是使用重叠构造器(第一个构造器只有必要参数,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推),一种选择是JavaBeans模式(调用无参构造器来创建对象,然后调用setter方法来设置参数),另一种较好的方法是使用Builder模式:不直接生成想要的对象,而是让客户端利用所有必要的参数构造器得到一个builder对象,客户端在builder对象上调用类似于setter的方法来设置可选参数。

class NutritionFacts {    private final int servingSize;    private final int servings;    private final int calories;    private final int fat;    private final int sodium;    private final int carbohydrate;    public static class Builder {        //对象的必选参数        private final int servingSize;        private final int servings;        //对象的可选参数的缺省值初始化        private int calories = 0;        private int fat = 0;        private int carbohydrate = 0;        private int sodium = 0;        //只用少数的必选参数作为构造器的函数参数        public Builder(int servingSize,int servings) {        this.servingSize = servingSize;        this.servings = servings;        }        public Builder calories(int val) {        calories = val;        return this;        }        public Builder fat(int val) {        fat = val;        return this;        }        public Builder carbohydrate(int val) {        carbohydrate = val;        return this;         }        public Builder sodium(int val) {        sodium = val;        return this;        }        public NutritionFacts build() {        return new NutritionFacts(this);        }    }    private NutritionFacts(Builder builder) {        servingSize = builder.servingSize;        servings = builder.servings;        calories = builder.calories;        fat = builder.fat;        sodium = builder.sodium;        carbohydrate = builder.carbohydrate;    }}//使用方式public static void main(String[] args) {    NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)      .calories(100).sodium(35).carbohydrate(27).build();    System.out.println(cocaCola);}


三、用私有构造器或者枚举类型强化Singleton属性

        Java中主要有三种单例的创建方式:
        1.将构造函数私有化,直接通过静态公有的final域字段获取单实例对象:
public class Elvis {    public static final Elvis INSTANCE = new Elvis();    private Elivs() { ... }    public void leaveTheBuilding() { ... }}

        这样的方式主要优势在于简洁高效,使用者很快就能判定当前类为单实例类,在调用时直接操作Elivs.INSTANCE即可。但是若Elvis的使用代码被迁移到多线程的应用环境下了,系统希望能够做到每个线程使用同一个Elvis实例,不同线程之间则使用不同的对象实例,那么这种创建方式将无法实现该需求。

        2. 通过公有域成员的方式返回单实例对象:

public class Elvis {    public static final Elvis INSTANCE = new Elvis();    private Elivs() { ... }    public static Elvis getInstance() { return INSTANCE; }    public void leaveTheBuilding() { ... }}

        这种方法很好的弥补了第一种方式的缺陷,如果今后需要适应多线程环境的对象创建逻辑,仅需要修改Elvis的getInstance()方法内部即可,对用调用者而言则是不变的,这样便极大的缩小了影响的范围。

        3.编写一个包含单个元素的枚举类型:

public enum Elvis {    INSTANCE;    public void leaveTheBuilding() { ... } }

        主函数可直接使用Elvis.INSTANCE.leaveTheBuilding()调用单例中的函数。这种方法简洁清晰,线程安全,是实现Singleton的最佳方法。


四、通过私有构造器强化不可实例化的能力

        对于有些工具类如java.lang.Math、java.util.Arrays等,其中只是包含了静态方法和静态域字段,因此对这样的类,实例化就显得没有任何意义了。然而在实际的使用中,如果不加任何特殊的处理,这样的类是可以像其他类一样被实例化的。而企图通过将类做成抽象类来强制该类不可被实例化,是行不通的。因为该类还可以被继承,其子类也可以被实例化。解决方法是,将构造函数设置为private,这样类的外部将无法实例化该类,与此同时,在这个私有的构造函数的实现中直接抛出异常,从而也避免了类的内部方法调用该构造函数。

public class UtilityClass {    //Suppress default constructor for noninstantiability.     private UtilityClass() {        throw new AssertionError();    }}

        这样定义之后,该类将不会再被外部实例化了,否则会产生编译错误。然而这样的定义带来的最直接的负面影响是该类将不能再被子类化。


五、避免创建不必要的对象

        比较下面的例子,并测试他们在运行时的效率差异:

Boolean b = Boolean.valueOf("true");Boolean b = new Boolean("true");

        前者通过静态工厂方法保证了每次返回的对象都是同一个true或false,即valueOf将只会返回Boolean.TRUE或Boolean.FALSE两个静态域字段之一。而后面的Boolean构造方式,每次都会构造出一个新的Boolean实例对象。这样在多次调用后,第一种静态工厂方法将会避免大量不必要的Boolean对象被创建,从而提高了程序的运行效率,也降低了垃圾回收的负担。

        继续比较下面的代码:
public class Person {    private final Date birthDate;    //判断该婴儿是否是在生育高峰期出生的。    public boolean isBabyBoomer {        Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));        c.set(1946,Calendar.JANUARY,1,0,0,0);        Date dstart = c.getTime();        c.set(1965,Calendar.JANUARY,1,0,0,0);        Date dend = c.getTime();        return birthDate.compareTo(dstart) >= 0 && birthDate.compareTo(dend) < 0;    }}public class Person {    private static final Date BOOM_START;    private static final Date BOOM_END;    static {        Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));        c.set(1946,Calendar.JANUARY,1,0,0,0);        BOOM_START = c.getTime();        c.set(1965,Calendar.JANUARY,1,0,0,0);        BOOM_END = c.getTime();    }    public boolean isBabyBoomer() {        return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;    }}

        改进后的Person类只是在初始化的时候创建Calender、TimeZone和Date实例一次,而不是在每次调用isBabyBoomer方法时都创建一次他们。如果该方法会被频繁调用,效率的提升将会极为显著。


六、消除过期的对象引用
        Java中也有内存泄露:

public class Stack {    private Object[] elements;    private int size = 0;    private static final int DEFAULT_INITIAL_CAPACITY = 16;    public Stack() {        elements = new Object[DEFAULT_INITIAL_CAPACITY];    }    public void push(Object e) {        ensureCapacity();        elements[size++] = e;    }    public Object pop() {        if (size == 0)            throw new EmptyStackException();        return elements[--size];    }    private void ensureCapacity() {        if (elements.length == size)            elements = Arrays.copys(elements,2*size+1);    }}

        在以上代码中,在正常的使用中不会产生任何逻辑问题,然而随着程序运行时间不断加长,内存泄露造成的副作用将会慢慢的显现出来。当我们调用pop方法时,该方法将返回当前栈顶的elements,同时将该栈的活动区间(size)减一,然而此时被弹出的Object仍然保持至少两处引用,一个是返回的对象,另一个则是该返回对象在elements数组中原有栈顶位置的引用。这样即便外部对象在使用之后不再引用该Object,那么它仍然不会被垃圾收集器释放,久而久之导致了更多类似对象的内存泄露。修改方式如下:

public Object pop() {    if (size == 0)        throw new EmptyStackException();    Object result = elements[--size];    elements[size] = null; //手工将数组中的该对象置空    return result;}

        推荐在以下3中情形下需要考虑资源手工处理问题:
        1.类是自己管理内存,如例子中的Stack类。
        2.使用对象缓存机制时,需要考虑被从缓存中换出的对象,或是长期不会被访问到的对象。
        3.事件监听器和相关回调。用户经常会在需要时显示的注册,然而却经常会忘记在不用的时候注销这些回调接口实现类。


七、避免使用终结方法
        在Java的实际开发中,并非所有的资源都是可以被垃圾回收器自动释放的,如FileInputStream、Graphic2D等类中使用的底层操作系统资源句柄,并不会随着对象实例被GC回收而被释放。在Java中完成这样的工作主要是依靠try-finally机制来协助完成的。然而Java中还提供了另外一种被称为finalizer的机制,使用者仅仅需要重载Object对象提供的finalize方法,这样当JVM的在进行垃圾回收时,就可以自动调用该方法。但是由于对象何时被垃圾收集的不确定性,以及finalizer给GC带来的性能上的影响,因此并不推荐使用者依靠该方法来达到关键资源释放的目的。

        Java的语言规范中并没有保证该方法会被及时的执行,甚至都没有保证一定会被执行。即便开发者在code中手工调用了System.gc和System.runFinalization这两个方法,这仅仅是提高了finalizer被执行的几率而已。还有一点需要注意的是,被重载的finalize()方法中如果抛出异常,其栈帧轨迹是不会被打印出来的。在Java中被推荐的资源释放方法为,提供显式的具有良好命名的接口方法,如FileInputStream.close()和Graphic2D.dispose()等。然后使用者在finally区块中调用该方法,见如下代码:

public void test() {    FileInputStream fin = null;    try {        fin = new FileInputStream(filename);        //do something.    }    finally {        fin.close();    }}

        总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则不要使用终结方法。如果使用了终结方法,即在类中覆盖了finalize方法,请记住一定要在finally子句中调用super.finalize()。

0 0