EffectiveJava笔记-1.创建和销毁对象

核心内容

创建、销毁对象主要包含以下4个方面的内容:

  • 何时以及如何创建对象
  • 何时以及如何避免创建对象
  • 如何却奥他们能够适时地销毁
  • 如何管理对象销毁之前必须进行的各种清理动作

解决方案及编码原则

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

类提供一个公有的 静态工厂方法,例如我们看下 Boolean类的示例

public static Boolean valueOf(boolean b) {
    return b ? TRUE : FALSE;
}

注:静态工厂方法 不等价于 设计模式中的工厂模式
优势:

  1. 第一大优势:相比于构造器,他们有方法名,可以更加准确清晰的描述返回的对象。可读性更高,构造器的参数必须不同,限制更严格,所以此方式更灵活
  2. 第二大优势:相比于构造器,不必在每次调用他们的时候都创建一个新对象。静态方法返回的对象,内部实现可以缓存起来,比如:Boolean.valueOf(boolean)
  3. 第三大优势:相比于构造器,可以返回类型的任何子类型对象。通过方法,返回值可以设置为 接口 类型,返回其所有子类型

灵活的静态工厂方法构成了 服务提供者框架(Service Provider Framework),例如:JDBA(java数据库连接,Java Database Connectivity)API.

服务提供者框架:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把他们从多个实现中解耦出来。
包含三个重要的组件(1、2、3)和一个可选组件(4):

  1. 服务接口(Service Interface):有提供者实现的
  2. 提供者注册API(Provider Registration API):系统用来注册实现,让客户端访问他们的
  3. 服务访问API(Service Access API):客户端用来获取服务实例的。
  4. 服务提供者接口(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();
	}
}

缺点:

  1. 类如果不含有工友或者受保护的构造器,就不能被子类化
  2. 它们于其他的静态方法没有任何区别,辨识度不高

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

  1. valueOf
  2. of
  3. getInstance
  4. newInstance
  5. getType
  6. 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,该终结方法也被执行。

posted @ 2022-04-25 01:05  fight-ing  阅读(72)  评论(0编辑  收藏  举报
Fork me on GitHub