Effective Java 第二章 创建和销毁对象
1. 用静态工厂方法代替构造器
//Example
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
此方法有5个优势
- 方法有名称。相较于构造器,可以更加确切地描述返回的对象。
- 不用每次调用的时候都创建一个新的对象。
- 可以返回原返回类型的任何子类类型的对象。
- 方法返回的的对象的类可以随着每次调用而发生变化。(通过修改静态工厂方法的参数值)
- 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。(服务提供者框架、通过反射)。
此方法有两个缺点
- 类如果不含公有的或者受保护的构造器,就不能被子类化。但是这会因祸得福,程序员会倾向于使用复合(composition),而不是继承,这正是不可变类型需要的。如果你的类设计就是用来继承的,那就不能把构造器设置成私有的了。
- 静态工厂方法的第二个缺点在于,程序员很难发现他们。API文档中并不会把他们标识出来。
总结:
静态工厂和公有构造器各有用处,大多数时候静态工厂更加合适。因此切忌第一反应是提供公有构造器,而不考虑静态工厂。
2. 遇到多个构造器参数时要考虑使用构建器
类中编写静态成员类Builder来实现建造者模式。
//建造者(Builder)模式
//一个账号,账号、密码是必选参数,性别、email是可选参数
public class AccountFacts{
private final int accountNumber;
private final int password;
private final int sex;
private final String email;
public static class Builder{
//必选参数
private int accountNumber;
private int password;
//可选参数,要设置默认值
private int sex = 1;//默认是男性
private String email = "null";
public Builder(int accountNumber,int password){
this.accountNumber = accountNumber;
this.password= password;
}
public Builder sex(int val){
sex = val;
return this;
}
public Builder email(String val){
email = val;
return this;
}
public AccountFacts build(){
return new AccountFacts(this);
}
}
private AccountFacts(Builder builder){
accountNumber=builder.accountNumber;
password=builder.password;
sex=builder.sex;
email=builder.email;
}
}
客户端代码
//账号是1234,密码是123,性别是男性,email没有填写
AccountFacts zcyAccount = new AccountFacts.Builder(1234,123).sex(1).build();
Builder模式也适用于类层次结构。但是代码比较长,这里就不做分享了。建议阅读书中的细节。
相较于构造器,builder的优势在于,它支持多个可变参数。同时可以多次调用某一个方法来传入多个参数到一个域中。
劣势在于,为了创建对象,要先创建它的构建器,有一部分开销,影响性能。同时,代码比较冗长。
总结:
如果类的构造器或者静态工厂有多个参数,Builder模式是个不错的选择。
3. 用私有构造器或者枚举类型强化Singleton属性
Singleton:指仅仅被实例化一次的类。
私有构造器(单例模式-饿汉)
public class BBA{
private BBA(){...};//私有构造器
private static final BBA INSTANCE = new BBA();//静态、不可变
public static BBA getInstance(){
return INSTANCE;
}
}
私有构造器(单例模式-懒汉)
public class BBA{
private BBA(){...};//私有构造器
private static volatile BBA instance;//只声明,不创建对象
public static BBA getInstance(){
//调用getInstance()时再创建对象
if(instance == null){
sychronized(A.class){
if(instance==null){
instance = new BBA();
}
}
}
return instance;
}
}
枚举
//声明一个包含单个元素的枚举类型
public enum Elvis{
INSTANCE;
}
总结:
单例模式拥有灵活性,缺点:在序列化的时候必须声明所有实例域都是瞬时(transient)的。否则每次反序列化一个序列化的实例时,都会创建一个新的实例。
单元素的枚举类型更加简介,并且无偿提供了序列化机制。即使在面对复杂的序列化和反射攻击时,都能绝对防止多次实例化。虽然目前还没有被广泛使用,但是单元素的枚举类型经常成为实现Singleton的最佳方法。
4. 通过私有构造器强化不可实例的能力
只包含静态方法或者静态域的类,例如java.lang.Math或者java.util.Arrays这些工具类,它们不希望被实例化,因为实例化对它们来说没有任何意义。
在实操过程中,通过把类做成抽象类来强制该类不可被实例化是不行的。
原因如下:
- 该类可以被子类化,并且该子类可以被实例化
- 这样子做会误导用户,以为这种类是专门为了被继承而设计的
解决方案:让类包含一个私有构造器。(这样子的话编译器就不会自动生成缺省的构造器)
//不可被实例化的类
public class UtilityClass{
private UtilityClass(){
throw new AssertionError();
}
}
当然,这么做也有副作用,它使得一个类不能被子类化。
5. 优先考虑依赖注入来引用资源
有许多类会依赖一个或多个底层的资源。即这个类可能会依赖其他的类的实例。
最好的采取做法是,当创建一个新的实例时,就将该实例所依赖的资源传入到构造器中。这就是依赖注入的一种形式。
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
}
优点:依赖注入的对象资源具有不可变性,因此多个客户端可以共享依赖对象。同时极大提升了灵活性、可重用性和可测试性。
缺点:依赖注入会导致大型项目结构凌乱,可以通过使用依赖注入框架解决,如 Dagger、Guice、Spring。
6. 避免创建不必要的对象
-
一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个相同功能的新对象。
反例:String s = new String("BMW");
改进:String s = "BMW";
-
尽量调用静态工厂方法,而不是使用构造器。因为构造器每次调用都会创建一个新的对象,而静态工厂方法不会这样做。
-
有些对象创建成本比其他对象高很多的时候,建议缓存下来重用。
class Test{ //省略构造函数 public int calculateThatValue(){ int value;//假设这是一个若赋值了就不变的,且要经过很复杂的运算才能算出来的值 //假设这里是很复杂的一段代码,需要运行很长时间 return value; } }
改进:
class Test{ private final int VALUE_COSTLONGTIME;//假设这是一个不变的,且要经过很复杂的运算才能算出来的值 //省略构造函数 //静态方法块,在类第一次被加载时候执行 static{ int value; //假设这里是很复杂的一段代码,需要运行很长时间 VALUE_COSTLONGTIME = value;//把计算好的值赋值给final常量 } }
-
当对象重用与否不明显时,考虑使用适配器(也叫视图)来避免创建不必要对象。
适配器是指这样一个对象:把功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。
例如HashMap的keySet方法实现:public Set<K> keySet() { Set<K> ks = keySet; return (ks != null ? ks : (keySet = new KeySet())); }
可以看到,第一行实现代码是把keySet赋值给ks,return 后面是一个三元表达式,当ks不为空时返回ks,否则创建一个KeySet返回。(也就是keySet为空时创建一个KeySet对象返回)
keySet在HashMap中的定义:transient volatile Set<K> keySet = null;
其中,volatile关键字修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。 -
避免无意识的使用自动装箱。
基本类型优先,当心无意思的自动装箱
例: long 类型声明成Long
总结:
不要错误理解成尽可能避免创建对象。小对象的创建和回收动作是非常廉价的。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是一件好事。
同时,通过维护自己的对象池来避免创建对象也不是一种好的做法。除非池中的对象非常重量级(例如数据库连接池)。得益于现代JVM高度优化的垃圾回收器,维护对象池消耗的性能很容易就会超过直接创建。
7. 消除过期的对象引用
以下是内存泄漏的三个常见来源:
-
无意识的对象保持
下面程序中的,从栈中弹出来的对象不会被当作垃圾回收。即使程序不再引用这些对象,它们也不会被回收。栈内部会维护着这些对象的过期引用。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.copyOf(elements,2*size+1); } }
可以看到,凡是在elements数组的“活动部分”之外的任何引用都是过期的。活动部分是指elements中下标小于size的那些元素。
修复方法:
//一旦对象引用过期,清空这个引用。 //弹出栈的元素就是过期的,把它清空 public Object pop(){ if(size==0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null;//解除过期引用 return result ; }
要注意的是:
1.清空对象应该是一种例外,而不是一种规范行为。 用最紧凑的作用域范围来定义每一个变量时,这些对象引用会被包含它的变量结束生命周期,而不用手动结束。
2.**只要类是自己管理内存,程序员就应该警惕内存泄露问题。**一旦元素被释放掉,它包含的任何对象引用都应该被清空。 -
缓存
当把对象引用放到缓存中,很容易被遗忘,这会导致它不再用之后很长一段时间内仍然存留在内存中。
解决方式: 在缓存之外存在对某个项的键的引用。 -
监听器和其他回调
总结:
由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在多年。往往只有通过仔细检查代码,或者借助于Heap剖析工具,才能发现内存泄漏的问题。因此,如果能够在使用内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,那是最好不过的了。
8. 避免使用终结方法和清除方法
终结方法(finalizer)不可预测并且危险,一般情况下是不必要的。
Java9中用清除方法(cleaner)代替了终结方法。清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢的,一般情况下也是不必要的。
- 终结方法和清楚方法的缺点在于不能保证被及时执行。这意味着,注重时间(time-critical)的任务不应该由终结方法或者清除方法来完成。
- Java语言规范不仅不能保证终结方法或者清除方法会被及时地执行,而且根本就不保证它们会被执行。因此,永远不应该依赖终结方法或者清除方法来更新重要的持久状态。
例如,如果忽略在终结过程中被抛出来的未被捕获的异常,该对象的终结过程也会终止。未被捕获的异常会使对象处于破坏的状态(corrupt state),如果另一个线程企图使用这种被破坏的对象,则可能发生任何不确定的行为。 - 使用终结方法和清除方法有非常严重的性能损失。
- 终结方法有严重的安全问题。构造器抛出的异常,本足以防止对象继续存在。有了终结方法的存在,这一点就做不到了。因此要编写一个空的final的finalize方法。(当然,如果是final类就不需要了,因为这种攻击方式是通过运行恶意子类的终结方法实现的,final方法没有子类,自然不用处理。)
Q: 如果类的对象中封装的资源(例如文件或者线程)确实需要终止,应该怎么做才能不用编写终结方法或者清楚方法呢?
A: 让类实现AutoCloseable接口。客户端在每个实例不再需要的时候调用close方法,一般利用try-with-resources确保终止,即使遇到异常也是如此 。
终结方法和清除方法的好处:
- 当资源所有者忘记调用close方法的时候,可以充当“安全网”。虽然不能保证及时清理资源,但迟一点清理总比不清理好。当然,如果要编写这样的安全网终结方法,就要认真考虑清楚,这种保护是否值得付出这样的代价。
- 处理对象的本地对等体(native peer),它由普通对象通过本地方法委托给本地对象(native object)。因而,不会被垃圾回收器回收。如果它没有关键资源或者不影响性能,可以交给终结方法或者清除方法,如果有关键资源或者性能无法接受,应该具有一个close方法。
总结:
除非是作为安全网,或者是为了终止非关键的本地资源,否则不要使用清除方法,对于在Java9之前的发行版本,尽量不要使用终结方法。若使用了终结方法或者清除方法,则要注意它的不确定性和性能后果。
9. try-with-resources 优先于 try-finally
try块和finally块都有可能抛出异常,若同时抛出异常,则finally块中的第二个异常将会覆盖try块的异常,使我们无法追踪到程序错误的根源,例如下面这段代码示例:
@Test
public void tryFinallyTest() throws Exception {
try {
throw new Exception("try block Exception");
} finally {
throw new Exception("finally block Exception");
}
}
从执行结果中可以看出,try块中的异常没有显示。
解决方案:
自java7开始加入了try-with-resource语法并引入了AutoCloseable接口,只要相关资源的类或接口实现或继承了这个接口,就可以不显式调用close()方法,自动关闭资源,并且不会有上述"异常覆盖"的情况发生。
我们同时用两种形式的try语句来进行测试,throwException()方法累计抛出的异常。
public class TryTest implements AutoCloseable {
private int count = 0;
@Test
public void tryResourceTest() throws Exception {
try(TryTest test = new TryTest()) {
test.throwException();
}
}
@Test
public void tryFinallyTest() throws Exception {
TryTest test = new TryTest();
try {
test.throwException();
} finally {
test.close();
}
}
public void throwException() throws Exception {
count++;
throw new Exception("the No." + count + " Exception occurs");
}
@Override
public void close() throws Exception {
throwException();
}
}
try-finally语句块的执行结果
try-with-resources语句块的执行结果
我们可以成功看到我们真正想要看到的第一个异常信息,第二个异常被标注为抑制。
总结:
因此在处理必须关闭的资源时,我们始终要优先考虑用 try-with-resources,而不是用try-finally,这样得到的代码将更加简洁、清晰,产生的异常也更有价值。