《Effective Java》学习笔记(一)——创建和销毁对象

《Effective Java》学习笔记

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

优点:

  1. 具名——静态工厂方法与名称
  2. 环保——不必在每次调用的时候都创建一个新对象;
  3. 多子——可以返回原返回类型的任何子类型的对象;

常见的静态工厂方法名:

  • valueOf/of——类型转换,返回的实例和入参具有相同的值;
  • getInstance——返回一个预先创建好的实例;
  • newInstance——返回一个新的实例;
  • getType——像getInstance一样,但是在工厂方法处于不同的类中的时候使用;
  • newType——像newInstance一样,但是在工厂方法处于不同的类中的时候使用。

当一个类没有提供静态工厂方法的时候,我们才需要使用工厂模式。

缺点:

  1. 静态工厂方法的主要缺点在于,类如果不含公有的或者受保护的构造器,就不能被子类化;
  2. 静态工厂方法的第二个缺点在于,它们与其他的静态方法实际上没有任何区别;

遇到多个构造器参数时要考虑用构建器

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

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

JavaBeans模式在构造过程中可能处于不一致的状态,且阻止了把类做成不可变的可能。

第三种就是构建者模式(Builder模式):不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象。然后客户端在builder对象上调用类似setter的方法,来设置每个相关的可选参数。最后客户端调用无参的build方法来生成不可变的对象。这个builder是它构建的类的静态成员类。

不足:为了创建对象,必须先创建它的构建器;Builder模式还比重叠构造器模式更加冗长,因为它只在有很多参数的时候才使用。

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

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

传统单例实现存在的一个问题是一旦实现了序列化接口,那么它们不再保持单例了,因为readObject()方法一直返回一个新的对象就像java的构造方法一样,可以通过使用readResolve()方法来避免:

1
2
3
4
//readResolve to prevent another instance of Singleton
private Object readResolve(){
return INSTANCE;
}

一个使用枚举类型来实现的单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 需要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等
class Resource{
}

public enum SomeThing {
INSTANCE;
private Resource instance;
private SomeThing() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}

获取资源的方式很简单,只要SomeThing.INSTANCE.getInstance()即可获得所要实例。因为在枚举中明确了构造方法为私有,在我们访问枚举实例时会执行构造方法,同时,每个枚举实例都是static final类型的,也就表明只能被实例化一次。

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

工具类(utility class)不希望被实例化,实例没有任何意义。在缺少显式构造器的时候,编译器会自动提供一个公有的、无参的缺省构造器。所以我们只要让这个类包含私有构造器,它就不能被实例化了。当然副作用就是,这个类就不能被子类化了。所有的构造器都必须显式或隐式地调用超类构造器,在这种情形下,子类就没有可访问的超类构造器课调用了。

避免创建不必要的对象

对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。

除了重用不可变的对象之外,也可以重用哪些已知不会被修改的可变对象。

当你应该重用现有对象的时候,请不要创建新的对象。

消除过期的对象引用

过期引用是指永远也不会再被解除的引用。如果一个对象引用被无意识地保留起来了,那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。即使只有少量的几个对象引用被无意识地保留下来,也会有许许多多的对象被排除在垃圾回收机制之外,从而对性能造成潜在的重大影响。

这类问题的修复方法是:一旦对象引用已经过期,只需清空这些引用即可。

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

清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。

除了过期引用,内存泄漏的另一个常见来源是缓存。可能的解决方案:

  • 如果正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期以后,它们就会自动被删除。只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
  • “缓存项的生命周期是否有意义”并不容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时地清除掉没用的项。这项清除工作可以由一个后台线程(Timer或者ScheduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新条目的时候顺便进行清理。

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

避免使用终结方法

终结方法的缺点在于不能保证会被及时地执行。从一个对象变得不可到达开始,到它的终结方法被执行,所花费的这段时间是任意长的。Java语言规范不仅不保证终结方法会被及时地执行,而且根本就不保证它们会被执行。所以,不应该依赖终结方法来更新重要的持久状态。

如果类的对象中封装的资源(例如文件或者线程)确实需要终止,应该提供一个显式的终止方法,并要求该类的客户端在每个实例不再有用的时候调用这个方法。显式终止方法的典型例子InputStream、OutputStream和java.sql.Connection上的close方法。显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。