转载

《Effective Java》Second Edition 第2章 创建和销毁对象

第2章 创建和销毁对象

第1条:考虑用静态工厂方法代替构造器

  1. 一个类只能有一个带有指定签名的构造器。编程人员通常知道如何避开这一限制:通过提供两个构造器,它们的参数列表只在参数类型的顺序上有所不同。实际上这并不是个好主意。面对这样的API,用户永远也记不住该用哪个构造器,结果常常会调用错误的构造器。并且,读到使用了这些构造器的代码时,如果没有参考类的文档,往往不知所云。
  2. 由于静态工厂方法有名称,所以它们不受上述的限制。当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且慎重地选择名称以便突出它们之间的区别。
  3. 静态工厂方法能够为重复的调用返回相同的对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在。这种类被称作实例受控的类(instance-controlled)。编写实例受控的类有几个原因。实例受控使得类可以确保它是一个Singleton或者是不可实例化的。
  4. 静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象。

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

  1. 静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。
  2. 遇到许多构造器参数的时候,还有第二种代替方法,即JavaBeans模式,在这种模式下,调用一个无参构造器来创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数。
  3. 遗憾的是,JavaBeans模式自身有着很严重的缺点。因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。
  4. 与此相关的另一点不足在于,JavaBeans模式阻止了把类做成不可变的可能,这就需要程序员付出额外的努力来确保它的线程安全。
  5. 注意NutritionFacts是不可变的,所有的默认参数值都单独放在一个地方。builder的setter方法返回builder本身,以便可以把调用链接起来。下面就是客户端代码:
NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();

builder模式模拟了具名的可选参数,就像Ada和Python中的一样。

// Builder Pattern
public 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 {
        // Required Parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        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;
    }
}

第3条:用私有构造器或者枚举类型强化Singleton属性

//Singleton with public final field
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }

    public void leaveTheBuilding() { ... }

私有构造器仅被调用一次,用来实例化公有的静态final域Elvis.INSTANCE。由于缺少公有的或者受保护的构造器,所以保证了Elvis的全局唯一性:一旦Elvis类被实例化,只会存在一个Elvis实例,不多也不少。

//Singleton with static factory
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() { ... }
}

公有域方法的主要好处在于,组成类的成员的声明很清楚地表明了这个类是一个Singleton:公有的静态域是final的,所以该域将总是包含相同的对象引用。公有域方法在性能上不再有任何优势:现代的JVM实现几乎都能够将静态工厂方法的调用内联化。

为了使利用这其中一种方法实现的Singleton类变成是可序列化的(Serializable),仅仅在声明中加上“implements Serializable”是不够的。为了维护并保证Singleton,必须声明所有实例域都是瞬时(transient)的,并提供一个readResolve方法。

从Java 1.5发行版本起,实现Singleton还有第三种方法。只需编写一个包含单个元素的枚举类型:

//Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { ... }
}

这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

第4条:通过私有构造器强化不可实例化的能力

  1. 有时候,你可能需要编写只包含静态方法和静态域的类。这些类的名声很不好,因为有些人在面向对象的语言中滥用这样的类来编写过程化的程序。尽管如此,它们也确实有它们特有的用处。我们可以利用这种类,以java.lang.Math或者java.util.Arrays的方式,把基本类型的值或者数组类型上的相关方法组织起来。我们也可以通过java.util.Collections的方式,把实现特定接口的对象上的静态方法(包括工厂方法)组织起来。最后,还可以利用这种类把final类上的方法组织起来,以取代扩展该类的做法。
  2. 这样的工具类(utility class)不希望被实例化,实例对它没有任何意义。然而,在缺少显式构造器的情况下,编译器会自动提供一个公有的、无参的缺省构造器(default constructor)。对于用户而言,这个构造器与其他的构造器没有任何区别。
  3. 企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的。该类可以被子类化,并且该子类也可以被实例化。这样做甚至会误导用户,以为这种类是专门为了继承而设计的。
// Noninstantiable utility class
public class UtilityClass {
    //Suppress default constructor for noninstantiability
    private UtilityClass() {
        throw new AssertionError();
    }
    ... //Remainder omitted
}

第5条:避免创建不必要的对象

  1. 如果对象是不可变的(immutable),它就始终可以被重用。
  2. 传递给String构造器的参数(”stringette”)本身就是一个String实例,功能方面等同于构造器创建的所有对象。
  3. 而且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。
  4. 对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。例如,静态工厂方法Boolean.valueOf(String)几乎总是优先于构造器Boolean(String)。
  5. 下面的版本用一个静态的初始化器(initializer),避免了这种效率低下的情况:
class Person {
    private final Date birthDate;
    // Other fields, methods, and constructor omitted

    private static final Date BOOM_START;
    private static final Date BOOM_END;

    static {
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_START = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }

    public boolean isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) >= 0 && 
            birthDate.compareTo(BOOM_END) < 0;
    }
}

改进后的Person类只在初始化的时候创建Calendar、TimeZone和Date实例一次,而不是在每次调用isBabyBoomer的时候都创建这些实例。

把boomStart和boomEnd从局部变量改为final静态域,这些日期显然是被作为常量对待,从而使得代码更易于理解。

如果改进后的Person类被初始化了,它的isBabyBoomer方法却永远不会被调用,那就没有必要初始化BOOM_START和BOOM_END域。通过延迟初始化(lazily initializing),即把对这些域的初始化延迟到isBabyBoomer方法第一次被调用的时候进行,则有可能消除这些不必要的初始化工作,但是不建议这样做。正如延迟初始化中常见的情况一样,这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平。

考虑适配器(adpater)的情形,有时也叫做视图(view)。适配器是指这样一个对象:它把功能委托给一个后备对象(backing object),从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。

例如,Map接口的keySet方法返回该Map对象的Set视图,其中包含该Map中所有的键(key)。粗看起来,好像每次调用keySet都应该创建一个新的Set实例,但是,对于一个给定的Map对象,实际上每次调用keySet都返回同样的Set实例。虽然被返回的Set实例一般是可改变的,但是所有返回的对象在功能上是等同的:当其中一个返回对象发生变化的时候,所有其他的返回对象也要发生变化,因为它们是由同一个Map实例支撑的。

变量sum被声明成Long而不是long,意味着程序构造了大约<nobr aria-hidden="true" style="outline: 0px; transition: none; border: 0px; max-width: none; max-height: none; min-width: 0px; min-height: 0px; vertical-align: 0px; line-height: normal; word-break: break-all;">231</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mn>2</mn><mrow><mn>31</mn></mrow></msup></math>个多余的Long实例(大约每次往Long sum中增加long时构造一个实例)。结论很明显:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。

真正正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。而且,数据库的许可可能限制你只能使用一定数量的连接。但是,一般而言,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用(footprint),并且还会损害性能。现代的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。

第6条:消除过期的对象引用

  1. 如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。这是因为,栈内部维护着对这些对象的过期引用(obsolete reference)。所谓的过期引用,是指永远也不会再被解除的引用。在本例中,凡是在elements数组的”活动部分(active portion)”之外的任何引用都是过期的。活动部分是指elements中下标小于size的那些元素。
  2. 在支持垃圾回收的语言中,内存泄漏是很隐蔽的(称这类内存泄漏为“无意识的对象保持(unintentional object retention)”更为恰当。
  3. 清空对象引用应该是一种例外,而不是一种规范行为。
  4. 一般而言,只要类是自己管理内存,程序员就应该警惕内存泄漏问题。
  5. 如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
  6. 内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们就会积聚。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如,只将它们保存成WeakHashMap中的键。

第7条:避免使用终结方法

  1. 终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。
  2. C++的析构器也可以被用来回收其他的非内存资源。而在Java中,一般用try-finally块来完成类似的工作。
  3. 这意味着,注重时间(time-critical)的任务不应该由终结方法来完成。例如,用终结方法来关闭已经打开的文件,这是严重错误,因为打开文件的描述符是一种很有限的资源。
  4. 一位同事最近在调试一个长期运行的GUI应用程序的时候,该应用程序莫名其妙地出现OutOfMemoryError错误而死掉。分析表明,该应用程序死掉的时候,其终结方法队列中有数千个图形对象正在等待被终结和回收。
  5. 不要被System.gc和System.runFinalization这两个方法所诱惑,它们确实增加了终结方法被执行的机会,但是它们并不保证终结方法一定会被执行。唯一声称保证终结方法被执行的方法是System.runFinalizersOnExit,以及它臭名昭著的孪生兄弟Runtime.runFinalizersOnExit。这两个方法都有致命的缺陷,已经被废弃了[ThreadStop]。
  6. 正常情况下,未被捕获的异常将会使线程终止,并打印出轨迹栈(Stack Trace),但是,如果异常发生在终结方法之中,则不会如此,甚至连警告都不会打印出来。
  7. 那么,如果类的对象中封装的资源(例如文件或者线程)确实需要终止,应该怎么做才能不用编写终结方法呢?只需提供一个显式的终止方法,并要求该类的客户端在每个实例不再有用的时候调用这个方法。
  8. 显示终止方法的典型例子是InputStream、OutputStream和java.sql.Connection上的close方法。另一个例子是java.util.Timer上的cancel方法,它执行必要的状态改变,使得与Timer实例相关联的该线程温和地终止自己。java.awt中的例子还包括Graphics.dispose和Window.dispose。这些方法通常由于性能不好而不被人们关注。一个相关的方法是Image.flush,它会释放所有与Image实例相关联的资源,但是该实例仍然处于可用的状态,如果有必要的话,会重新分配资源。
  9. 显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。在finally子句内部调用显式的终止方法,可以保证即使在使用对象的时候有异常抛出,该终止方法也会执行。
  10. 但是如果终结方法发现资源还未被终止,则应该在日志中记录一条警告,因为这表示客户端代码中的一个Bug,应该得到修复。
  11. 显式终止方法模式的示例中所示的四个类(FileInputStream、FileOutputStream、Timer和Connection),都具有终结方法,当它们的终止方法未能被调用的情况下,这些终结方法充当了安全网。
  12. 本地对等体是一个本地对象(native object),普通对象通过本地方法(native method)委托给一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的Java对等体被回收的时候,它不会被回收。在本地对等体并不拥有关键资源的前提下,终结方法正式执行这项任务最合适的工具。

原文地址:http://www.jsdblog.com/article/63 

正文到此结束
Loading...