前言:
读这本书第1条规则的时候就感觉到这是一本很好的书,可以把我们的Java功底提升一个档次,我还是比较推荐的。本博客是针对《Effective Java》这本书第2章所写的一篇读书笔记。博客中也有会一些个人对某个模块的理解和深入探究,希望与大家一起进步。
概述:
本章的主题是创建和销毁对象:何时以及如何创建对象,何时以及如何避免创建对象,如何确保它们能够适时地销毁,以及如何管理对象销毁之前必须进行的各种清理动作。
相关内容:
1.考虑用静态工厂方法代替构造器
思路解析:
如果有人问你,如何去初始化一个类的对象实例时,你的第一反应可能就是去实现类的构造器。那如果我告诉你,这样的做法,其实并不是最好的方法,你可能会感觉意想不到,不会啊,老师和书上都是这样写的啊!怎么可能不去实现类的构造器就可以初始化一个类的对象实例呢?
办法还真有!如果你学习过一些设计模式(这里是废话了,如果你学习过设计模式,那你肯定早就知道是怎么回事了),比如单件模式。单件模式会涉及到一个这样的问题:如何让我们类的对象实现只被初始化一次呢?你可以使用类的构造器小小地实践一下,不管是打日志还是Debug,我想答案是一样的,不可能完成!这时,你就需要了解和学习用静态工厂方法来代替构造器了,你可以模仿以下实现。
public static ClassA getInstance() { return new ClassA; }看到以上代码,可能你会有所抱怨,不对啊,上面的方法没有达成我们需求中的效果啊,你一定是在耍我!
先不要着急,上面的方法的确只是一个思路过程,如果你想让你的对象只对被实例化一次,你可以模仿以下代码:
public static ClassA getInstance() { if(mInstance == null) { mInstance = new ClassA(); } return mInstance; }我想实现了以上代码的你可能会很开心,这样的确可以只实例化一个对象了。如果你的好奇心足够强大的话,我想你还应该尝试一下,使用构造器来重新实例一次或N次。还是可以实例化很多个对象实例,对不对?别担心,我没有骗你,是要利用以上的静态工厂方法,不过你还有一件小事没有去完成,那就是屏蔽默认的构造方法。像下面这样的:
private ClassA() { // do something!!! }没错,就是这样,你可以在私有的构造器中添加一些你想添加的,这都没有关系,但是请保证它的私有性。可能看到这里,你没有明白一个静态的工厂方法会给我们带来什么样的好处。下面就是对静态工厂方法的优点介绍。
静态工厂方法的优点:
a.它们有名称:你可以使用对方法的合理命名来提示用户,这一次被创建出来的是什么样的对象。
b.不必在每次调用它们的时候都创建一个新的对象:这一点上面的单件模式也有提到,这一优点的优势在于单件模式本身实在的需求和节约资源开支。
c.可以返回任何子类型的对象:这种灵活性的一种应用是,API可以返回对象,同时又不会使对象的类变成公有的。以这种方式隐藏实现类会使API变得非常简洁。
d.轻松创建相同参数的不同对象:想像一下,如果我们想要创建两个不同的对象,而这两个对象在创建的过程中都接受一个int型的参数,请问要如何创建?静态工厂方法就是答案!
静态工厂方法的缺点:
a.类如果不含有仅有的或是受保护的构造器,就不能被子类化:这里你可能会困惑,为什么这样会构成一个缺点?仔细一想,你肯定会原来如此。
b.它们与其他的静态方法实际上没有任何区别。
2.遇到多个构造器参数时要考虑用构建器
对于这一点我还是保留对大家的信任,大家都已经熟练掌握了它的使用和好处。它会使用在这样一个必要的环境下:如果你想构造一个类的对象实例,而这个类在构造器上需要传递很多参数,这里我们假设有10个。是不是想想就觉得很可怕,它充斥着我们的大脑,并抗争着说这样很麻烦,我们不要这样来做吧。而看起来麻烦还是小事,只要你耐心一点,总能渡过,不过如果你要把两个或N个参数给弄混了,那我想后果一定很“精彩”。
在这种情境下,我们可以考虑一下使用构建器。别被构建器的名称给吓到了,如果我换一个说法,相信你就会明白了。那就是把合适的参数封装成一个类,在类中使用setter和getter来实现需求。
这样的做法不仅可以让代码更简洁,还不容易让参数混淆,真是一个好东西啊。
3.用私有构造器或者枚举类型强化Singleton属性
在第1条中我们说到了关于单件模式的一些使用场景,下面会有一些额外介绍。如果你对单件模式还有一些想要了解的,可以在Java设计模式中进行了解。
对于如何使用私有构造器优化Singleton属性我们在第1条和Java设计模式的《Java设计模式——单件模式》中都有介绍,这里不再赘述。
关于如何使用枚举来强化Singleton属性,我们可以像下面这样:
public enum Elvis { INSTANCE; public void leaveTheBuilding(){...} }这种方法和公有域方法相近,但是它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
4.避免创建不必要的对象
从节省资源的角度考虑,我们应该尽量避免创建不必要的对象。看到这里,你可能会说上面提到的单件模式算不算这个避免创建不必要的对象呢?我说它是,又不全是。因为它不单是因为要避免创建不必要的对象而设计的,更多的是它要实现的是只能有一个对象。
你是否会像这样来创建一个字符串:
String s = new String("abcdefghi");
可能你会说你没有,可是你可能很少会想到这样的一种方式来初始化一个字符串对象的实例。但是我想要说的是最好不要像上面那样创建,因为它在一个多次创建中会产生很多不必要的实例。
你可以这样来改进它,使之更加合理:
String s = "abcdefghi";在Java的机制中,上面的这个版本只用了一个String实例,对于所有在同一台虚拟机中运行的代码,只要它们包含的字符串字面常量,该对象就会被重用。
5.消除过期的对象引用
在项目优化的时候,我们经常可能要去做内存泄漏的检测。不要以为Java已经有了垃圾回收的机制,我们就可以坐享其成,不再去考虑内存管理的事情了。如果你想了解更多内容,可以去看《Android开发中,可能会导致内存泄漏的问题》这篇文章。
下面请看这个例子:Can you spot the "memory leak"?
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); } } }
你能看出这段代码的问题所在吗?
我们试想一下,如果我们的栈在先增加元素,然后再收缩。那么,这时从栈中pop的对象不会被当作垃圾回收,即使用栈的程序不再引用它们,它们也不会被回收。这是因为,栈内部维护着这些对象的过期引用。这么说,可能你还不清楚什么是过期引用。换句话说吧,就是说我们的栈还在,这些被pop的对象曾经是属于这个栈的,它们都还持有这个活动栈的引用,那么Java的垃圾回收机制就不会对它们怎么样了。
这个就有一点像Android中,初始化了很多持有生命周期较长的Context的对象,而这些对象由于始终持有Context的引用,所以不会被Java回收机制回收。
这类问题的解决方法也很简单:一旦对象引用已经过期,只需清空这些引用即可。根据这一点,改进的程序如下:
public Object pop() { if (size == 0) { throw new EmptyStackException(); } Object result = elements[--size]; elements[size] = null; return result; }清空过期引用的另一个好处是,如果它们以后又被错误地解除引用,程序就会立即抛出一个NullPointerException的异常,而不是悄悄地错误运行下去。