第2章 创建和销毁对象

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


类可以通过静态工厂与设计模式来提供它的客户端,而不是通过构造器。提供静态工厂方法而不是公有的构造器,这样做具有几大优势。


静态工厂方法与构造器不同的第一大优势在于,它们有名称。如果构造器的参数本身没有确切地描述正被返回的对象,那么具有适当名称的静态工厂更容易

使用,产生的客户端代码也更于阅读。


静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新对象。这使得不可变类可以使用预先构建好的实例,或者将构建好的

实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。


静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象。


静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不必存在。这种灵活的静态工厂方法构成了服务提供者框架的基础。


服务提供者框架中有三个重要的组件:服务接口(Service Interface),这是提供者实现的;提供者注册API(Provider Registration API),这是系统用来注册实现,

让客户端访问它们的;服务访问(Service Access API),是客户端用来获取服务的实例的。服务访问API一般允许但是不要求客户端指定某种选择提供者的条件。

如果没有这样的规定,API就会返回默认实现的一个实例。服务访问API是“灵活的静态工厂”,它构成了服务提供者框架的基础。


服务提供者框架的第四个组件是可选的:服务提供者接口(Service Provider Interface),这些提供者负责创建其服务实现的实例。如果没有服务提供者接口,实现

就按照类名称注册,并通过反射方式进行实例化。对于JDBC来说,Connection就是它的服务接口,DriverManager.registerDriver是提供者注册API,

DriverManager.get Connection是服务访问API,Driver就是服务提供者接口。


静态工厂方法的第四大优势在于,在创建参数化类型实例的时候,它们使代码变得更加简洁。


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



静态工厂方法的第二个特点在于,它们与其他的静态方法实际上没有任何区别。


下面是静态工厂方法的一些惯用名称:

valueOf——不太严格地讲,该方法返回的实例与它的参数具有相同的值。这样的静态工厂方法实际上是类型转换方法。

of——valueOf的一种更为简洁的替代,在EnumSet中使用并流行起来。

getInstance——返回的实例是通过方法的参数来描述,但是不能够说与参数具有同样的值。对于Singleton来说,该方法没有参数,并返回唯一的实例。

newInstance——像getInstance一样,但newInstance能够确保返回的每个实例都与所有的其他实例不同。

getType——像getInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。

newType——像newInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。


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


builder像个构造器一样,可以对其参数强加约束条件。build方法可以检验这些约束条件,将参数从builder拷贝到对象之后,并在对象域而不是builder域

中对它们进行检验,这一点很重要。如果违反了任何约束条件,build方法就应该抛出IllegalStateException。异常的详细信息应该显示违反了哪个约束条件。

对多个参数强加约束的另一种方法是,用多个setter方法对某个约束条件必须持有的所有参数进行检查。如果该约束条件没有得到满足,setter方法就会抛出

IllegalException。这有个好处,就是一旦传递了无效的参数,立即就会发现约束条件失败,而不是等着调用build方法。

与构造器相比,builder的微略优势在于,builder可以有多个可变参数。构造器就像方法一样,只能有一个可变参数,因为builder利用单独的方法来设置每个

参数,你想要多少个可变参数,它们就可以有多少个,直到每个setter方法都有一个可变参数。

Builder模式十分灵活,可以利用单个builder构建多个对象。builder的参数可以在创建对象器件进行调整,也可以随着不同的对象而改变。builder可以自动填

充某些域,例如每次创建对象时自动增加序列号。

带有Builder实例的方法通常利用有限制的通配符类型来约束构建器的类型参数。

Java中传统的抽象工厂实现是Class对象,用newInstance方法充当build方法的一部分。这种用法隐含着许多问题。newInstance方法总是企图调用类的无参

构造器,你也不会收到编译时错误。相反,客户端代码必须运行时处理InstantiationException或者IllegalAccessException,这样既不雅观也不方便。

newInstance方法还会传播由无参构造器抛出的任何异常,即使newInstance缺乏相应的throws子句。换句话说,Class.newInstance破坏力编译时的异常检查。

上面讲过的Builder接口弥补了这些不足。

Builder模式的确也有它自身的不足。为了创建对象,必须先创建它的构建器。虽然创建构建器的开销在实践中可能不那么明显,但是在某些十分注重性能的情况

下,可能就成问题了。Builder模式还比重叠构造器模式更加冗长,因此它只在有很多参数的时候才使用,比如4个或者更多个参数。但是记住,将来你可能需要

添加参数。如果一开始就使用构造器或者静态工厂,等到类需要多个参数时才添加构造器,就会无法控制,那些过时的构造器或者静态工厂显得十分不协调。

因此,通常最好一开始就使用构建器。

简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是种不错的选择,特别是当大多数参数都是可选的时候。与使用传统的

重叠构造器模式相比,使用Builder模式的客户端代码将更易于阅读和编写,构建器也比JavaBean更加安全。


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


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

实例。

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

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

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


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


有时候,你可能需要编写只包含静态方法和静态域的类。这些类的名声很不好,因为有些人在面向对象的语言中滥用这样的类来编写过程化的程序。尽管如此,

它们也确实有它们特有的用处。我们可以利用这种类,以java.lang.Math或者java.util.Arrays的方式,把基本类型的值或者数组类型上的相关方法组织起来。我们

也可以通过java.util.Collections的方式,把实现特定接口的对象上的静态方法(包括工厂方法)组织起来。最后,还可以利用这种类把final类上的方法组织起来,

以取代扩展该类的做法。

这样的工具类(utility class)不希望被实例化,实例对它没有任何意义。然而,在缺少 显示构造器的情况下,编译器会自动提供一个公有的、无参的缺省构造器。

对于用户而言,这个构造器与其他的构造器没有任何区别。在已发行的API中常常可以看到一些被无意识地实例化的类。

企图通过将类做出抽象来强制该类不可被实例化,这是行不通的。该类可以被子类化,并且该子类也可以被实例化。这样做甚至会误导用户,以为这种类是

专门为了继承而设计的。然而,有一些简单的习惯用法可以确保类不可被实例化。由于只有当类不包含显示的构造器时,编译器才会生成缺省的构造器,因此我们

只要让这个类包含私有构造器,它就不能被实例化了。

由于显示的构造器是私有的,所以不可以在该类的外部访问它。AssertionError不是必需的,但是它可以避免不小心在类的内部调用构造器。它保证该类在任何

情况下都不会被实例化。这种习惯用法有点违背直觉,好像构造器就是专门设计成不能被调用一样。因此,明智的做法就是在代码中增加一条注释。

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

可调用了。


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


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


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

中的错误总是有益的。

当程序员第一次被类似这样的问题困扰的时候,他们往往会过分小心:对于每一个对象引用,一旦程序不再用到它,就把它清空。其实这样做既没必要,也不是

我们所期望的,因为这样做会把程序代码弄得很乱。清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束

其生命周期如果你是在最紧凑的作用域范围内定义每一个变量,这种情况就会自然而然地发生。

那么,何时应该清空引用呢?Stack类的哪方面特性使它易于遭受内存泄漏的影响呢?简而言之,问题在于,Stack类自己管理内存。存储池包含了elements数组

(对象引用单元,而不是对象本身)的元素。数组活动区域(同前面的定义)中的元素是已分配的,而数组其余部分的元素则是自由的。但是垃圾回收器并不知道这一点;

对于垃圾回收器而言,elements数组中的所有对象引用都同等有效。只有程序员知道数组的非活动部分是不重要的。程序员可以把这个情况告知垃圾回收器,做法

很简单:一旦数组元素变成了非活动部分的一部分,程序员就手工清空这些数组元素。

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

内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。对于这个问题,

有几种可能的解决方案。如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存

中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。

更为常见的情形则是,“缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时地

清除掉没用的项。这项清除工作可以由一个后台线程来完成,

或者也可以在给缓存添加新条目的时候顺便进行清理。LinkedHashMap类利用它的removeEldestEntry方法可以很容易地实现后一种方案。对于更加复杂的缓存,

必须直接使用java.lang.ref。

内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显示地取消注册,那么除非你采取某些动作,否则

它们就会积聚。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用。

由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在很多年。往往只有通过仔细检查代码,或者借助于Heap剖析工具(Heap Profiler)才能发现

内存泄漏问题。因此,如果能够在内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,那是最好不过的了。


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


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

其可用之处。

终结方法的缺点在于不能保证会被及时地执行。从一个对象变得不可到达开始,到它的终结方法被执行,所花费的这段时间是任意长的。这意味着,注重时间

的任务不应该由终结方法来完成。

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

那么终结方法有什么方法呢?它们有两种合法用途。第一种用途是,当对象的所有者忘记调用前面段落中建议的显示终止方法时,终结方法可以充当"安全网"。

虽然这样做并不能保证终结方法会被及时地调用,但是在客户端无法通过调用显示的终止方法来正常结束操作的情况下,迟一点释放关键资源总比永远不释放

要好。但是如果终结方法发现还未被终止,则应该在日志中记录一条警告,因为这表示客户端代码中的一个Bug,应该得到修复。如果你正考虑编写这样的

安全网终结方法,就要认真考虑清楚,这种额外的保护是否值得你付出这份额外的代价。

显示终止方法模式的实例中所示的四个类(FileInputStream、FileOutputStream、Timer和Connection),都具有终结方法,当它们的终止方法未能被调用的

情况下,这些终结方法充当了安全网。

终结方法的第二种合理用途与对象的本地对等体(native peer)有关。本地对等体是一个本地对象(native object),普通对象通过本地方法(native method)委托给

一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的java对等体被回收的时候,它不会被回收。在本地对等体并不拥有关键

资源的前提下,终结方法正是执行这项任务最合适的工具。


总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。在这些很少见的情况下,既然使用了终结方法,就要记住调用

super.finalize。如果用终结方法作为安全网,要记得记录终结方法的非法用法。最后,如果需要把最终方法与公有的非final类关联起来,请考虑使用终结方法

守卫者,以确保即使子类的终结方法未能调用super.finalize,该终结方法也会被执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值