文章目录
本章关注于创建与销毁对象:何时并如何创建对象,何时并如何避免创建对象,如何确保对象适时适当地被销毁,并且如何在销毁对象之前做好清理工作。
Item 1: 考虑使用静态工厂方法替代构造方法
一 静态工厂方法拥有名字
传统的构造方法名字必须和类名一致,如果可以接受不同个数的参数来初始化,那么必须声明多个构造方法且构造方法的名字一样,只能依靠参数来区分要创建的对象。
而使用静态工厂方法,构造对象时可以为接受不同参数的对象起不同且容易区分的适当的名字。
二 静态工厂方法不用每次调用都创建一个新的对象
例如Boolean.valueof(bool)
方法就使用了这一技巧,它从不新建对象。如果相同的对象经常被请求,且创建代价较大,这种方法能极大地提升性能。
静态工厂方法在重复请求时返回相同对象的能力也让类能够在任意时刻严格保持对实例的控制。实例控制允许类确保它是单例的,或者不可实例化的。并且,它允许不可变值类确保不会存在两个相等的实例。
三 静态工厂方法可以返回构造方法返回类型的子类型对象
四 静态工厂方法可以因输入参数的不同而调用不同的返回方法
五 编写包含该静态工厂方法的类时,返回对象的类可以不存在。
缺点1:类如果没有public或protected构造方法,就不能被子类化
缺点2:静态工厂方法难以被程序员发现
在API文档中,静态工厂方法没有像构造方法那样被明确地标识出来。对于提供了静态工厂方法而不是构造方法的类来说,想要查明如何被实例化是较难的。因此我们在命名静态工厂方法时最好遵循一定的规范。下面是一些惯用的名称:
-
from —— 类型转换方法,只接受单个参数,例如:
Date d = Date.from(instant)
-
of —— 聚合方法,带有多个参数。例如:
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
-
valueOf —— 比from和of更繁琐的替代方法,例如:
BigInteger prim = BigInteger.valueOf(Integer.MAX_VALUE);
-
instance 或 getInstance —— 返回一个实例,可接收参数,例如:
Walker luke = Walker.getInstance(options);
-
create 或 newInstance —— 和instance或者getInstance一样,不同之处在于每次调用都返回一个新的实例。
-
getType ——
FileStore fs = Files.getFileStore(path)
-
newType ——
BufferedReader br = Files.newBufferedReader(path);
-
type ——
List<Something> sts = Collections.list(ste);
总之,静态工厂方法和公有构造方法都各有用处,我们需要理解它们的各自长处。静态工厂经常更加合适,因此切忌第一反应就是提供公有构造方法,而不先考虑静态工厂方法。
Item 2: 当构造方法需要多个参数时,考虑使用Builder构建器
静态工厂方法和构造器有一个共同的局限性,即当构造一个对象需要大量的可选参数时,会让构造代码很难编写和阅读。多个类型相同的参数会导致微妙的错误,如果不小心颠倒参数的顺序,编译器可能也不会报错,但是程序在运行时就会产生错误行为。
使用Builder模式,可以使客户端代码更易于阅读和编写,构建器也比JavaBeans模式更安全。
Item 3:用私有构造器或枚举类型强化Singleton属性
Singleton是指仅仅被实例化一次的类。实现Singleton常用的几种方式:
1.公有静态final成员
将构造方法设为private,只允许通过公有静态final成员INSTANCE
获取类的实例
public class Elvis{
public static final Elvis INSTANCE = new Elvis();
prviate Elvis(){};
...
}
但这种方法无法防止通过反射机制来调用私有构造方法。当然,如果要防止反射,可以让构造器在被要求创建第二个实例时抛出异常。
2.静态工厂方法
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {}
public static Elvis getInstance() {
return INSTANCE;
}
3.枚举
上面两种方法实现的Singleton类在序列化,然后反序列化的时候,会创建一个新的实例,通过枚举,可以防止这一问题:
public enum Elvis {
INSTANCE;
...
}
枚举无偿提供了序列化机制,并且可以绝对防止多次实例化,无论是使用序列化还是反射。单元素的枚举类型通常是实现Singleton的最佳方法。
Item4: 用私有构造器强调类的不可实例化
一些工具类,只提供静态方法供外部调用,完全没有被实例化的必要,就应该显式地为这些类提供私有构造方法,以确保该类不会被实例化
public class UtilityClass{
private UtilityClass(){
//可以考虑抛异常,但不是必要的
throw new AssertionError();
}
}
Item5: 优先考虑依赖注入来引用资源
有许多类会依赖一个或多个底层的资源。例如,检查拼写需要依赖词典,因此,有不少人把类实现成如下的静态工具类:
public class SpellChecker {
private static final Lexincon dictionary = new Lexincon();
private SpellChecker(){ }
public static boolean isValid(String word){
return dictionary.isValide(word);
}
}
同样的,也可以实现为Singleton类:
public class SpellChecker {
private final Lexincon dictionary = new Lexincon();
private SpellChecker(){ }
public static SpellChecker INSTANCE = new SpellChecker();
public boolean isValid(String word){
return dictionary.isValide(word);
}
}
但是以上两种方式都不理想,因为他们都是假定只有一本词典,无法满足多本词典的需求。静态工具类和Singleton类不适合需要引用底层资源的类。
满足该需求的最简单模式是,当创建一个新的实例,就将该资源传到构造器中,这就是依赖注入( dependency injection
)的一种形式:词典是拼写检查的一个依赖,在创建拼写检查时就将词典注入其中。
public class SpellChecker {
private final Lexincon dictionary;
public SpellChecker(Lexincon lexincon){
this.dictionary = Objects.requireNonNull(lexincon);
}
public boolean isValid(String wordq
return dictionary.isValide(word);
}
}
Item6: 避免创建不必要的对象
一个极端的反面例子:
String s = new String("bikini");
这样创建对象是完全不必要的,因为传递给String构造器的参数 bikini
本身就是一个String实例,功能等同于通过String构造器创建的实例。如果这种用法是在一个循环中,或者一个被频繁调用的方法,就会创建成百上千个不必要的String实例。
改进后的版本如下:
String s = "bikini";
只用了一个String实例,而不是每次执行的时候都会创建一个新的实例。而且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要他们包含相同的字符串字面常量,该对象就会被重用。
优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱
通过维护自己的对象池来避免创建对象并不是一种好的做法,除非对象池中的对象是非常重量级的
Item7: 消除过期的对象引用
一
在支持垃圾回收的语言中,内存泄漏是很隐蔽的。如果一个对象引用被无意识地保留了,那么垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其它对象。这类问题的修复很简单:一旦对象引用过期,只需清空这些引用即可。
二
内存泄漏的另一个来源是缓存。一旦把对象引用放到缓存中,它就很容易被遗忘掉。对于这个问题,有几种可能的解决方案:
比如用WeakHashMap
替代常用的HashMap,或者更复杂的情况可以使用java.lang.ref
但是在Android中,不建议使用java.lang.ref来缓存对象,而是使用
LruCache
替代
三
内存泄漏的第三个常见来源是监听器和其它回调。比如实现了一个API,客户端注册了改API的回调,却没有在适当的时候显式地取消注册。
Item8: 避免使用终结方法和清除方法
终结方法(finalize
)通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定、性能降低,以及可移植性问题。
一 不可预测
在Java 9 中用清除方法代替了终结方法。清洁方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的。
终结方法和清除方法的缺点在于不能保证会被及时执行。从一个对象变得不可到达开始,到它的终结方法被执行,所花费的时间是任意长的。例如,用终结方法或清除方法来关闭已经打开的文件,就是一个严重的错误,如果系统没有及时运行终结方法或清除方法就会导致大量文件仍然保留在打开的状态。
Java语言规范不仅不保证终结方法或清除方法会被及时执行,而且根本不保证他们会被执行。当一个程序被终止的时候,某些已经无法访问的对象上的终结方法根本没有被执行,这是完全可能的。结论是:永远不要依赖终结方法或者清除方法来更新重要的持久状态。
二 性能损失
给一个对象添加终结方法或清洁方法,其创建和销毁所用的时长比没有添加时增加了几倍甚至是几十倍。主要是因为终结方法阻止了有效的垃圾回收,如果使用清除方法,其速度会快一些,但仍然会带来性能损失。
三 安全隐患
终结方法攻击(finalizer attach
)
阻止对象被垃圾回收
1. 替代终结方法和清除方法
如果类的对象中封装的资源(例如文件或者线程)确实需要终止,只需让类实现AutoCloseable
,并要求客户端在每个实例不再需要的时候调用close
方法,一般是使用 try-with-resources
来确保终止。值得一提的是,该实例必须在一个私有域中记录下“该对象已经不再有效”,如果在对象已经终止之后再调用其他方法,就必须检查这个域,并抛出IllegalStateException
异常。
2. 终结方法和清除方法的用处
一 充当"安全网"
当资源所有者忘记调用其close方法时,终结方法或者清除方法可以充当"安全网"。虽然这样做不能保证他们被及时运行,但是在客户端无法正常结束操作的情况下,迟一点释放资源总比永远不释放要好。
如果考虑编写这样的安全网终结方法,就要认真考虑清楚,这种保护是否值得付出这样的代价。 有些Java类,如 FileInputStream、FileOutPutStream、ThreadPoolExecutor 和 java.sql.Connection
都具有充当安全网的终结方法。
二 终止非关键本地资源
本地对等体(native peer) : 指通过调用本地方法(native method)将Java对象委托给一个本地对象的对象。
因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当Java对象被回收的时候,它不会被回收。如果本地对等体没有关键资源,并且性能也可以接受,那么清除方法或者终结方法正是执行这项任务的最合适的工具。如果本地对等体拥有必须被及时终止的资源,或者性能无法接受,那么该类就应该有一个close方法,如前所述。
Item9: try-with-resources优先于try-finally
要使用try-with-resources
构造的资源,必须先实现AutoCloseable
接口。
在处理必须关闭的资源时,始终优先考虑用try-with-resources
,而不是try-finally
。