EffectiveJava笔记-1.创建和销毁对象
核心内容
创建、销毁对象主要包含以下4个方面的内容:
- 何时以及如何创建对象
- 何时以及如何避免创建对象
- 如何却奥他们能够适时地销毁
- 如何管理对象销毁之前必须进行的各种清理动作
解决方案及编码原则
第1条:使用 静态工厂方法 代替 构造器
类提供一个公有的 静态工厂方法,例如我们看下 Boolean类的示例
public static Boolean valueOf(boolean b) {
return b ? TRUE : FALSE;
}
注:静态工厂方法 不等价于 设计模式中的工厂模式
优势:
- 第一大优势:相比于构造器,他们有方法名,可以更加准确清晰的描述返回的对象。可读性更高,构造器的参数必须不同,限制更严格,所以此方式更灵活
- 第二大优势:相比于构造器,不必在每次调用他们的时候都创建一个新对象。静态方法返回的对象,内部实现可以缓存起来,比如:Boolean.valueOf(boolean)
- 第三大优势:相比于构造器,可以返回类型的任何子类型对象。通过方法,返回值可以设置为 接口 类型,返回其所有子类型
灵活的静态工厂方法构成了 服务提供者框架(Service Provider Framework),例如:JDBA(java数据库连接,Java Database Connectivity)API.
服务提供者框架:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把他们从多个实现中解耦出来。
包含三个重要的组件(1、2、3)和一个可选组件(4):
- 服务接口(Service Interface):有提供者实现的
- 提供者注册API(Provider Registration API):系统用来注册实现,让客户端访问他们的
- 服务访问API(Service Access API):客户端用来获取服务实例的。
- 服务提供者接口(Service Provider Interface):如果没有接口,那么实现就需要按照类名称注册,然后通过反射方式进行实例化。
例如:对于JDBC来说:
- Connection是它的服务接口
- DriverManager.registerDriver是提供者注册API,
- DriverManager.getConnection是服务访问API,
- Driver是服务提供者接口
对于 服务访问API 可以利用适配器(Adapter)模式,返回比提供者需要的更丰富的服务接口。给一个具体的例子:
//Service Interface
public interface Service{
...//方法定义
}
//Service Provider Interface
public interface Provider{
Service newService();
}
//Noninstantiable class for service registration and access
public class Services{
//阻止实例化
private Services(){}
//创建服务接口的映射关系
private static final Map<String, Provider> providers = new ConcurrentHashMap<String, Provider>();
public static final String DEFAULT_PROVIDER_NAME = "<def>";
//Provider registration API ,提供者注册API
public static void registerDefaultProvder(Provider p){
registerProvider(DEFAULT_PROVIDER_NAME, p);
}
public static void registerProvider(String name, Provder p){
providers.put(name, p);
}
//Service access API
public static Service newInstance(){
return newInstance(DEFAULT_PROVIDER_NAME);
}
public static Service newInstance(String name){
Provider p = providers.get(name);
if(p == null){
throw new IllegalArgumentException("No provder registered with name:" + name);
}
reutrn p.newService();
}
}
缺点:
- 类如果不含有工友或者受保护的构造器,就不能被子类化
- 它们于其他的静态方法没有任何区别,辨识度不高
静态工厂方法的一些惯用名称:
- valueOf
- of
- getInstance
- newInstance
- getType
- newType
第2条:遇到多个构造器参数时需要考虑用 构建器(Builder)
多个参数,大家一般会想到两种方式:
- 多个构造器组成,维护成本高
- 使用 pojo,先new对象,再set,数据可能出于不一致的状态,线程不安全
在这种时候,我们尽量使用 Builder 模式,可以达到以上两种的优点,示例代码如下:
//Bulder模式
public class Test {
//必选参数
private int test1;
private int test2;
//可选参数
private int test3;
private int test4;
public static class Builder{
private int test1;
private int test2;
private int test3 = 0;
private int test4 = 0;
public Builder(int test1, int test2){
this.test1 = test1;
this.test2 = test2;
}
public Builder test3(int test3){
this.test3 = test3;
return this;
}
public Builder test4(int test4){
this.test4 = test4;
return this;
}
public Test build(){
return new Test(this);
}
}
public Test(Builder builder) {
this.test1 = builder.test1;
this.test2 = builder.test2;
this.test3 = builder.test3;
this.test4 = builder.test4;
}
public static void main(String[] args) {
Test test = new Test.Builder(1, 2)
.test3(3).test4(4)
.build();
}
}
当大多数参数都是可选的时候,使用Bulder模式,更易于阅读和编写,同时也比JavaBean更加安全。
第3条:用私有构造器*或者枚举类型强化 Singleton 属性
Singleton 指仅被实例化一次的类。
第一种方式:构造器保持私有,到处公有的静态成员。
public class TestSingleton {
public static final TestSingleton INSTANCE = new TestSingleton();
private TestSingleton(){
///...
}
}
上边这种方式,可以通过反射来获取,可以在构造器中进行判断,在创建实例时抛出异常。
public class TestSingleton {
public static final TestSingleton INSTANCE = new TestSingleton();
private TestSingleton(){
throw new AssertionError();
}
}
为了用一种更优化的方式,解决上边的问题,就有了下边第二种方式
第二种方式:提供 静态工厂方法 到处共享实例
public class TestSingleton {
private static final TestSingleton INSTANCE = new TestSingleton();
private TestSingleton(){
///...
}
public static TestSingleton getInstance(){
return INSTANCE;
}
}
如果上边的类,要变成可序列化的
- 仅仅声明 "implement Serializable" 是不够的。
- 声明所有实例域都是瞬时(transient)的
- 同时提供一个 readResolve方法。
否则,每次反序列化一个序列化的实例时,都会创建一个新的实例。
在java 1.5发行版本起,可以使用枚举的方式来实现:
public enum EnumSingleton {
INSTANCE;
}
效果一样的前提下,代码更简洁、并且无偿提供了序列化机制,达到绝对防止多次实例化。
第4条:避免创建不必要的对象
通过前边提到的 3 种方法,去实现避免创建不必要的对象,会极大的提升性能。
延迟初始化是不建议的方式,复杂度增加
对象池的方式也不是一种好的方式,除非是对象是非常重量级,比如:数据库连接池。
注:因重用对象而付出的代价要远远大于因创建重复对象而付出的代价。
第5条:消除过期的对象引用
public class TestStack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INTIAL_CAPACITY = 16;
public TestStack() {
elements = new Object[DEFAULT_INTIAL_CAPACITY];
}
public void push(Object element) {
ensureCapaticy();
elements[size++] = element;
}
public Object pop(){
if (size == 0){
throw new EmptyStackException();
}
return elements[--size];
}
private void ensureCapaticy() {
if (elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
在上边的代码中,pop方法里边只是返回了数组中的数据,这些数据即使没人用了,也不会被回收,从而导致了内存泄漏。这类内存泄漏
通常是"无意识的对象保持"。
如何解决这类问题,只需要在引用已经过期后,只需要清空这些引用即可,对于pop的微调一下:
public Object pop(){
if (size == 0){
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
针对上边的改造方式,会让大家以后过于谨慎,其实没必要,本身这种方式也是一种例外,不是一种规范行为。
最好的方法是:让包含该引用的变量结束其生命周期。
通过上边的例子可以看出:只要类是自己管理内存,大家就应该警惕内存泄漏问题。一旦元素被释放掉,就应该将其清空。
内存泄漏另一个常见来源是缓存,为了保证垃圾正常被回收,使用 WeakHashMap来保存其引用。
还有另外一个常见来源监听器和回调,最佳方法同上,只保存他们的弱引用(weak reference)。
第6条:避免使用终结(finalizer)方法
终结方法(finalizer)方法通常是不可预测的,也比较危险。会导致行为不稳定,性能下降,可移植性差。
缺点:
- 不能保证会被及时的执行。比如:用终结方法来关闭已经打开的文件,是严重的错误用法,而是try finally替代
- 同时也不能保证它们会被执行
注意:不应该依赖终结(finalizer)方法来更新重要的持久状态。 - 有非常严重的性能损失
如果类的对象中封装的资源(例如:文件、线程)确实需要终止,只需提供一个显示的终止方法。 - 显示的终止方法有以下要求:
1、每个实例不再有用的时候调用
2、该实例必须在自己的私有域,记录下自己是否已经被终止,以便其他方法在调用前抛出 IllegalStateException 异常
典型的例子:InputStream、OutputStream、java.util.Connection上的 close 方法。
通常情况下,显示的终止方法 与 try-finally结构 结合起来使用,以确保及时终止。在 finally 子句内部调用显示的终止方法,保证在异常情况下,也会被执行。
Foo foo = new Foo(...);
try{
...
} finally{
foo.terminate();
}
带来的好处:
- 充当"安全网",任何情况下都会被执行
- 与对象的本地对等体(natice peer)有关。
本地对等体:一个本地对象(native object),普通对象通过本地方法(native method),委托给一个本地对象。所以,当 它的java对等体被回收时,它(本地对象)不会被回收,这种情况下,就可以使用终止方法来执行回收。
还有一个需要注意的点是“终结方法链”,它并不会被自动执行。如果类有终结方法,并且子类覆盖了终结方法,子类的终结方法必须手动调用超累的终结方法。
在子类调用的代码中,也还是应该结合 try-finally 结构来调用,以保证父类的终结方法被正常调用。
最后如果需要把终结方法和公有非final类关联起来,请考虑使用 结方法守卫者,以确保即使子类的终结方法未能调用super.finalizer,该终结方法也被执行。