跳转至

创建和销毁对象

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

  1. 静态工厂方法与构造器不同的第一大优势在于,它们有名称
    如果构造器的参数本身没有确切地描述正被返回的对象,那么具有适当名称的静态工厂会更容易使用,产生的客户端代码也更易于阅读。
    当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,而且慎重地选择名称以便突出它们之间的区别。
  2. 静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新对象
    这使得不可变类可以使用预先构造好的实例,或者将构建好的实力缓存起来,进行重复利用,从而避免创建不必要的重复对象。这种方法类似于Flyweight模式。
    静态工厂方法能够为重复的调用返回想用对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在。
    这种类被称为 实例受控的类 。编写实例受控的类有几个原因。
    1.实例受控的类可以确保它是一个Singleton或者是不可实例化的。
    2.它还使得不可变的类可以确保不会存在两个相等的实例,当且仅当a=b的时候才有a.equals(b)为true。如果类保证了这一点,它的客户端就可以使用==操作符来代替equals方法,这样就可以提升性能。枚举类型保证了这一点
  3. 静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象 这种灵活的一种应用是,API可以返回对象,同时又不会使对象的类变成公有的。以这种方式隐藏实现类会使API变得非常简洁。这样我们在选择返回对象的类时就有了更大的灵活性。比如ArrayList#SubList。使用这种静态工厂方法时,甚至要求客户端通过接口来引用被返回的对象,而不是通过它的实现类来引用被返回的对象,这是一种良好的习惯。
    公有的静态工厂方法所返回的对象的类不仅可以是非公有的,而且该类还可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。只要是已声明的返回类型的子类行,都是允许的。为了提升软件的可维护性和性能,返回对象的类也可能随着发行版本的不同而不同。
  4. 静态工厂方法与构造器不同的第四大优势在于,在创建参数化类型实例的时候,它们使代码变得更加简洁(现在这条不适用了)
  5. 静态工厂方法的主要缺点在于,类如果不含公有的或者受保护的构造器,就不能被子类化
  6. 静态工厂方法的第二个缺点在于,它们与其他的静态方法实际上没有任何区别
    在API文档中,它们没有像构造器那样在API文档中明确标识出来,因此,对于提供了静态工厂方法而不是构造器的类来说,想要查明如何实例化一个类,这是非常困难的。但是我们可以在类或者接口注释中关注静态工厂,并遵守标准的命名习惯,用来弥补这一劣势。下面是静态工厂方法的一些惯用名称:
    1.valueOf——不太严格地讲,该方法返回的实例与它的参数具有相同的值。这样静态工厂方法实际上是类型转换方法
    2.of——valueOf的简洁替代
    3.getInstance——返回的实例是通过方法的参数来描述的,但是不能够说与参数具有同样的值。对于Singleton来说,该方法没有参数,并返回唯一的实例
    4.newInstance——像getInstance一样,但newInstance能够确保返回的每个实例都与所有其他实例不同
    5.getType ——像getInstance一样,但是在工厂方法处于不同的类中的时候使用。Type 表示工厂方法所返回的对象类型
    6.newType ——像newInstance一样,但是在工厂方法处于不同的类中的时候使用。Type 表示工厂方法所返回的对象类型

简而言之,静态工厂方法和公有构造器都各有用处,我们需要理解它们各自的长处。静态工厂通常更加合适,因此切忌第一反应就是提供公有的构造器,而不先考虑静态工厂。

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

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

重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然较难以阅读。

遇到许多构造器参数的时候,还有第二种代替方法,即 JavaBeans模式 ,在这种模式下,调用一个无参构造器来创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数。这种模式弥补了重叠构造器模式的不足——创建实例很容易,产生的代码也很容易读。
但是,JavaBeans模式自身有着很严重的缺点。

  1. 因为在构造过程中被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。类无法仅仅通过校验构造器参数的有效性来保证一致性。试图使用处于不一致状态的对象,将会导致失败,这种失败与含有Bug的代码不同,因此它调试起来十分困难。
  2. 另一点不足在于,JavaBeans模式排除了使类成为不可变的可能性,这就需要程序员付出额外的努力确保它的线程安全。

幸运的是,还有第三种替代方法,既能保证像重叠构造器模式那样的安全性,也能保证像JavaBeans模式那么好的可读性——这就是Builder模式的一种形式。实例代码如下

public class SimpleBuilderPattern {
    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 sodium = 0;
        private int carbohydrate = 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 sodium(int val) {
            sodium = val;
            return this;
        }

        public Builder carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }

        public SimpleBuilderPattern build() {
            return new SimpleBuilderPattern(this);
        }
    }

    private SimpleBuilderPattern(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

    @Override
    public String toString() {
        return "SimpleBuilderPattern{" +
                "servingSize=" + servingSize +
                ", servings=" + servings +
                ", calories=" + calories +
                ", fat=" + fat +
                ", sodium=" + sodium +
                ", carbohydrate=" + carbohydrate +
                '}';
    }

    public static void main(String[] args) {
        SimpleBuilderPattern simpleBuilderPattern = new Builder(240, 8)
                .calories(100).sodium(35).carbohydrate(27).build();
    }
}
注意SimpleBuilderPattern是不可变的,所有的默认参数值都单独放在一个地方。
Builder模式模拟了具名的可选参数,就想Ada和Python中的一样。

Builder可以像构造器一样对其参数强加约束条件。build方法可以检验这些约束条件。将参数从builder拷贝到对象中之后,并在对象域而不是builder域中对它们进行检验,这一点很重要。如何违反了任何约束条件,build方法就应该抛出IllegalStateException。异常的详细信息应该显示出违反了哪个约束条件。
对多个参数强加约束条件的另一种方法是,用多个setter方法对某个约束条件必须持有的所有参数进行检查。如果该约束条件没有得到满足,setter方法就会抛出IllegalArgumentException。这有个好处,就是一旦传递了无效的参数,立即就会发现约束条件失败,而不是等着调用build方法。

与构造器相比,builder的略微优势在于,builder可以有多个可变参数。
Builder模式十分灵活,可以利用单个Builder构建多个对象。

Java中传统的抽象工厂实现是Class对象,用newInstance方法充当build方法的一部分。newInstance方法总是企图调用类的无参构造器,这个构造器甚至可能根本不存在。Class.newInstance破坏了编译时的异常检查。Builder模式弥补了这点。

Builder模式的确也有它自身的不足。比如为了创建对象而创建的Builder类。这在某些十分注重性能的情况下,可能就成问题了。
Builder模式还比重叠构造器模式更加冗长,因此它只有在有很多参数的时候才使用,比如4个或更多参数
但是,如果将来可能需要添加参数,那么通常最好一开始就使用Builder模式。

如果类的构造器或静态工厂中具有多个参数时,Builder模式就是不错的选择, 特别是当大多数参数都是可选的时候。与使用传统的重写构造器模式相比,使用Builder模式的客户端代码更易于阅读和编写,Builder也比JavaBeans更加安全。

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

在Java 1.5版本之前,实现Singleton有两种方式。这两种方法都要把构造器保持为私有的,并导出公有的静态成员,以便允许客户端能够访问该类的唯一实例。

在第一种方法中,公有静态成员是个final域:

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
}

由于缺少公有的或者受保护的构造器,所以保证了Elvis的全局唯一性:一旦Elvis类被实例化,只会存在一个Elvis实例,不多也不少。
客户端的任何行为都不会改变这一点,但要提醒一点:享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。
如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。

在第二种实现方法中,公有的成员是个静态工厂方法:

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

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

为了使利用这其中一种方法实现的Singleton类变成是可序列化的,仅仅是声明中加上'implements Serializable'是不够的。为了维护并保证Singleton,必须声明所有实例域都是瞬时(transient)的,并提供一个readResolve方法。否则,每次反序列化一个序列化的实例时,都会创建一个新的实例。为了防止这种情况,需要在Elvis类中加入readResolve方法:

private Object readResolve() {
  return INSTANCE;
}

从Java1.5开始,实现Singleton还有第三种方式——枚举。

public enum Elvis {
  INSTANCE;

  public void leaveTheBuilding() { ... }
}
这种方式在功能上与公有域方法相近,但是它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化。即使是在面复杂的序列化或者反射攻击的时候。

单元素的枚举类型已经成为实现Singleton的最佳方法。

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

有时候,我们需要编写只包含静态方法和静态域的类。这些类的名声很不好,因为有些人在面向对象的语言中滥用这样的类来编写过程化的程序。尽管如此,它们确实有它们特有的好处——比如工具类。

这样的工具类不希望被实例化,然而在缺少显式构造器的情况下,编译器会自动提供一个公有的、无参的缺省构造器。对用户而言,这个构造器与其他的构造器没有任何区别。

企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的。该类可以被子类化,而且该子类也可以被实例化。这样做甚至会误导用户,以为这种类是专门为了继承而设计的。
然而,有一些简单的习惯用法可以确保类不可被实例化——我们只需要让这个类包含私有构造器,它就不能被实例化了。

// Noninstantiable utility class
public class UtilityClass {
  private UtilityClass() {
    throw new AssertionError();
  }
  ...
}
AssertionError不是必须的,但是它可以避免不小心在类的内部调用构造器。它保证该类在任何情况下都不会被实例化。这种习惯用法有点违背直觉,好像构造器就是专门设计成不能被调用一样。因此,明智的做法就是在代码中添加一条注释。

这种习惯用法也有副作用,它使得一个类不能被子类化。所有的构造器都必须显式或隐式地调用超类构造器,在这种情形下,子类就没有可访问的超类构造器可调用了。

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

一般来说,最好能重用对象而不是在每次需要的时候创建一个相同功能的新对象。重用方式既快速又流行。如果对象是不可变的,它就始终可以被重用。

举一个例子:

String s = new String("stringette"); // DON'T DO THIS!
该语句每次被执行的时候都创建一个新的String实例,但这些创建对象的动作全都是不必要的。
改进后的版本如下:
String s = "stringette";
该版本只用了一个String实例,而不是每次执行的时候都创建一个新的实例。而且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面变量,该对象就会被重用。

对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。
除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。

对于一些不会改变的常量,我们可以通过延迟初始化(第71条),来可能消除这些不必要的初始化工作。但是不建议这么做。正如延迟初始化中常见的情况一样,这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平(第55条)。

要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。

不要错误的认为本条目所介绍的内容暗示着“创建对象的代价非常昂贵,我们应该要尽可能避免创建对象”。相反,由于小对象的构造器只做很少量的显式工作,所以,小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。

反之,通过维护自己的对象池来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的——比如说数据库连接池。
但是,一般而言,维护自己的对象池必定会把代码弄的很乱,同时增加内存占用,并且还会损害性能。现代的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。

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

在支持垃圾回收的语言中,内存泄漏是很隐蔽的(称这类内存泄露为“无意识的对象保持”更为恰当)。如果一个对象引用被无意识地保留起来了,那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。即使只有少量的几个对象引用被无意识地保留下来,也会有许许多多的对象被排除在垃圾回收机制之外,从而对性能造成潜在的重大影响。一旦对象引用已经过期,只需清空这些引用即可。

清除过期引用的另一个好处是,如果它们以后又被错误地解除引用,程序就会立即抛出NullPointerException异常,而不是悄悄地错误运行下去。尽快地检测出程序中的错误总是有益的。

当程序员第一次被类似这样的问题困扰时,他们往往会过分小心:对于每一个对象引用,一旦程序不再用到它,就把它清空。其实这样做既没必要,也不是我们所期望的。因为这样就会把程序代码弄的很乱。清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法就是让包含该引用的变量结束其生命周期。如果你是在最紧凑的作用域范围内定义每一个变量,这种情形就会自然而然地发生。

一般而言,只要是类自己管理内存,程序员就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。

内存泄漏的另一个常见来源是缓存。一旦把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。对于这个问题,有几种可能的解决方案。如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所有的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
更为常见的情形则是,“缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时地清除掉没用的项。这项清除工作可以由一个后台线程(可能是Timer或者ScheduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新条目的时候顺便进行清理。LinkedHashMap类利用它的removeEldestEntry方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用java.lang.ref。

内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取其他动作,否则它们就会积聚。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用。例如,只将它们保存成WeakHashMap中的键。

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

终结方法是指Object#finalize方法

终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定、降低性能,以及可移植性问题。

C++程序员被告知“不要把终结方法当作是C++析构器的对应物”。在C++中,析构器是回收一个对象所占用资源的常规做法,是构造器所必须的对应物。在Java中,当一个对象变得不可到达的时候,垃圾回收器会回收与该对象相关联的存储空间,并不需要程序员做专门的工作。C++的析构器也可以被用来回收其他的非内存资源。而在Java中,一般用try-finally块来完成类似的工作。

终结方法的缺点在于不能保证会被及时地执行。及时地执行终结方法正是垃圾回收算法的一个主要功能,这种算法在不同的JVM实现中大相径庭。

Java语言规范不仅不保证终结方法会被及时地执行,而且根本就不保证它们会被执行。当一个程序终止的时候,某些已经无法被访问的对象上的终结方法却根本没有被执行,这是完全有可能的。结论是:不应该依赖终结方法来更新重要的持久状态

不要被System.gcSystem.runFinalization这两个方法所迷惑,它们确实增加了终结方法被执行的机会,但是它们并不保证终结方法一定会被执行。
唯一声称保证终结方法被执行的方法是System.runFinalizersOnExit,以及它的孪生兄弟Runtime.runFinalizersOnExit。这两个方法都有致命缺陷,已经被废弃。

当你并不确定是否应该避免使用终结方法的时候,这里还有一种值得考虑的情形:如果未捕获的异常在终结过程中被抛出来,那么这种异常可以被忽略,并且该对象的终结过程也会被终止。未捕获的异常会使对象处于破坏的状态,如果另一个线程企图使用这种被破坏的对象,则可能发生任何不确定的行为。正常情况下,未捕获的异常将会使线程终止,并打印出栈轨迹,但是,如果异常发生在终结方法之中,则不会如此,甚至连警告都不会打印出来。

还有一点:使用终结方法有一个非常严重的性能损失

那么,如果类的对象中封装的资源(例如文件或者线程)确实需要终止,应该怎么做才能不用编写终结方法呢?只需提供一个显式的终结方法,并要求该类的客户端在每个实例不再有用的时候调用这个方法。
值得提及的一个细节是,该实例必须记录下自己是否已经被终止了:显式的终止方法必须在一个私有域中记录下“该对象已经不再有效”。如果这些方法是在对象已经终止以后被调用,其他的方法就必须检查这个域,并抛出IllegalStateException异常。
显式终止方法的典型例子是InputStreamOutputStreamjava.sql.Connection上的close方法。另一个例子是java.util.Timer上的cancel方法。

显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。在finally子句内部调用显式的终止方法,可以保证即使在使用对象的时候有异常抛出,该终止方法也会执行:

// try-finally block guarantees execution of termination methods
Foo foo = new Foo(...);
try {
    // Do what must be done with foo
    ...
} finally {
    foo.terminate(); // Explicit termination method
}

终结方法有两种合法用途:
1. 第一种用途是,当对象的所有者忘记调用前面段落中建议的显式终止方法时,终结方法可以充当“安全网(safety net)”。虽然这样做并不能保证终结方法会被及时地调用,但是在客户端无法通过调用显式的终止方法来正常结束操作的情况下(希望这种情形尽可能地少发生),迟一点释放关键资源总比永远不释放要好。但是如果终结方法发现资源还未被终止,则应该在日志中记录一条警告,因为这表示客户端代码中的一个Bug,应该得到修复。如果你正考虑编写这样的安全网终结方法,就要认真考虑清楚,这种额外的保护是否值得你付出这份额外的代价。
2. 第二种合理用途与 对象的本地对等体(native peer) 有关。本地对等体是一个本地对象(native object),普通对象通过本地方法(native method)委托给一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的Java对等体被回收的时候,它不会被回收。在本地对等体并不拥有关键资源的前提下,终结方法正是执行这项任务最合适的工具。如果本地对等体拥有必须被及时终止的资源,那么该类就应该具有一个显式的终止方法,如前所述。终止方法应该完成所有必要的工作以便释放关键的资源。终止方法可以是本地方法,或者它也可以调用本地方法。

值得注意的很重要一点是,“终结方法链(finalizer chaining)”并不会被自动执行。如果类(非Object)有终结方法,并且子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法。你应该在一个try块中终结子类,并在相应的finally块中调用超类的终结方法。这样做可以保证:即使子类的终结过程抛出异常,超类的终结方法也会得到执行。反之亦然。代码示例如下。

// Manual finalizer chaining
@Override protected void finalize() throws Throwable {
    try {
    ... // Finalize subclass state
    } finally {
        super.finalize();
    }
}

如果子类实现者覆盖了超类的终结方法,但是忘了手工调用超类的终结方法(或者有意选择不调用超类的终结方法),那么超类的终结方法将永远也不会被调用到。要防范这样粗心大意或者恶意的子类是有可能的,代价就是为每个将要终被的对象创建一个附加的对象。不是把终结方法放在要求终结处理的类中,而是把终结方法放在一个匿名的类(见第22条)中, 该匿名类的唯一作用就是终结它的外围实例(enclosing instance)。该匿名类的单个实例被称为终结方法守卫者(finalizer guardian),外围类的每个实例都会创建这样一个守卫者。外围实例在它的私有实例域中保存着一个对其终结方法守卫者的唯一引用,因此终结方法守卫者与外围实例可以同时启动终结过程。当守卫者被终结的时候,它执行外围实例所期望的终结行为,就好像它的终结方法是外围对象上的一个方法一样:

// Finalizer Guardian idiom
public class Foo {
    // Sole purpose of this object is to finalize outer Foo object
    private final Object finalizerGuardian = new Object() {
        @Override protected void finalize() throws Throwable {
            ... // Finalize outer Foo object
        }
    };
    ... // Remainder omitted
}

注意,公有类Foo并没有终结方法(除了它从Object中继承了一个无关紧要的之外),所以子类的终结方法是否调用super.finalize并不重要。对于每一个带有终结方法的非final公有类,都应该考虑使用这种方法。

总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。在这些很少见的情况下,既然使用了终结方法,就要记住调用 super.finalize。如果终结方法作为安全网,要记得记录终结方法的非法用法。最后,如果需要把终结方法与公有的非final类关联起来,请考虑使用终结方法守卫者,以确保即使子类的终结方法未能调用super.finalize,该终结方法也会被执行。


最后更新: 2020年1月16日

评论