[Effective Java]第二章 创建和销毁对象

[Effective Java]第二章 创建和销毁对象
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将追究法律责任!原文链接:http://www.cnblogs.com/jiangzhengjun/p/4254987.html

第一章      前言

略...

第二章      创建和销毁对象

1、            考虑用静态工厂方法代替构造器

条)调用私有构造器,如果要抵御这种攻击,可以修改构造器,让他在要求创建第二个实例的时候抛出异常。

第二种方法中公有的成员是个静态工厂方法:

public class Elvis {

private static final Elvis INSTANCE = new Elvis();

private Elvis() { }

public static Elvis getInstance() { return INSTANCE; }

}

,简单的说就是防止私有域导出到序列化流中而受到攻击)声明所有实例域都是瞬时(transient)的,并提供一个readResolve方法,否则,每次序列化时,都会创建一个新的实例,正确的作法:

且已达到最大允许容量则会删除最老的项

if (num.intValue() < 3 && size() > MAX_ENTRIES) {

System.out.println("超容 - " + this);

return true;

}

return false;

}

public static void main(String[] args) {

CacheLinkedHashMap lh = new CacheLinkedHashMap();

for (int i = 1; i <= 5; i++) {

lh.put("K_" + Integer.valueOf(i), Integer.valueOf(i));

}

System.out.println(lh);

// 放入时会删除最早放入的 k_1 项

lh.put("K_" + Integer.valueOf(11), Integer.valueOf(0));

System.out.println(lh);

}

}

输出:

{K_1=1, K_2=2, K_3=3, K_4=4, K_5=5}

超容 - {K_1=1, K_2=2, K_3=3, K_4=4, K_5=5, K_11=0}

{K_2=2, K_3=3, K_4=4, K_5=5, K_11=0}

说到这里,我们再来看看LinkedHashMap的另一特性 —— 可以按照我们访问的顺序来重新排序(即访问的项放到链表最后),平时我们构造的LinkedHashMap 是按照存放的顺序来排的,如果要按照我们访问(调用get或者是修改时,即put已存在的键时相当于修改,如果放入的不是存在的项则还是放在链的最后)的顺序来重排集合,则需使用LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)来构造,并将accessOrder设置为true:

public class OrderLinkedHashMap {

public static void main(String[] args) {

//按存入顺序连接

LinkedHashMap lh = new LinkedHashMap();

init(lh);

print(lh);//{0=0, 1=1, 2=2, 3=3, 4=4}

lh.get(0);//不将访问过的项放到链表最后

print(lh);//{0=0, 1=1, 2=2, 3=3, 4=4}

lh = new LinkedHashMap(10, 0.75f, true);//按访问顺序

init(lh);

print(lh);//{0=0, 1=1, 2=2, 3=3, 4=4}

lh.get(0);//会将访问过的项放到链表最后

print(lh);//{1=1, 2=2, 3=3, 4=4, 0=0}

lh.put(1, 11);//会将访问过的项放到链表最后

print(lh);//2=2, 3=3, 4=4, 0=0, 1=11,

}

static void init(Map map) {

for (int i = 0; i < 5; i++) {

map.put(i, i);

}

}

static void print(Map map) {

Iterator it = map.entrySet().iterator();

while (it.hasNext()) {

Entry entry = (Entry) it.next();

System.out.print(entry.getKey() + "=" + entry.getValue() + ", ");

}

System.out.println();

}

}

内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调(即将回调实例存储到某个容器中),却没有显示地取消注册,那么除非你采取某些动作,否则它们就会积聚。确保立即被垃圾回收的最佳方法是只保存它的弱引用,例如,只将它们保存成WeakHashMap中的键。

内存泄漏剖析工具:Heap Profiler

7、            避免使用终结方法

终结方法(finalize)通常是不可预测的,也是限危险的,一般情况下是不必要的,它不是C++中的析构函数。为什么呢?在C++中所有的对象运用delete()一定会被销毁,而JAVA里的对象并非总会被垃圾回收器回收,所以调用的时机是不确定的。C++的析构器也可以被用来回收其他非内存资源,而在Java中,一般用try-finally块来完成类似的工作。

终结方法的缺点是不能保证会被及时地执行。

Java语言规范不仅不保证终结方法会被及时地执行,而且根本就不保证它们会被执行。当一个程序终止的时候,某些已经无法访问的对象上的终结方法却根本没有被执行,这是完全有可能的。

不要被System.gc和System.runFinalization这两个方法所诱惑,它们确实增加了终结方法被执行的机会,但是它们并不保证终结方法一定会被执行。唯一声称保证终结方法被执行的方法是System.runFinalizersOnExit,以及它臭名昭著的孪生兄弟Runtime.runFinalizersOnExit,这两个方法都有致命的缺陷(它可能对正在使用的对象调用终结方法,而其他线程正在操作这些对象,从而导致不正确的行为或死锁),已经被废弃了。注,runFinalizersOnExit(true)只是在JVM退出时才开始调用那些还没有调用的对象上的finalize方法(默认情况下JVM退出时不会调用这些方法),而不像前面的gc与runFinalization在调用稍后执行finalize方法(也可能不执行,因为垃圾收集器并未开始工作)。

如果未被捕获的异常在终结过程中被抛出来,那么该异常将被忽略,并且该对象的终结过程也会终止,并且不会打印异常栈轨迹信息,但该对象仍可以被垃圾收集器收集。

还有一点,使用终结方法有一个非常严重的性能损失。换句话说,用终结方法创建和销毁对象慢了很多。

如果类对象中封闭的资源确实需要终止,我们首先需提供一个显示的终止方法(如InputStream、OutputStream、Connection、Timer上的close方法),并通常与try-finally结构结合起来使用,以确保及时终止(另外要注意的是,该实例应该记录下自己是否已经被关闭了,如果用户已显示地关闭了,则在终结方法中不得再关闭),而不是将它们的释放工作放在finalize终结方法中执行。

当然终结方法不是一无事处的,它有两种合法用途。第一种用途是,当对象的所有者忘记调用前面段落中建议的显示终止方法时,终结方法可以充当“安全网”,我们可以在finalize方法中再进行一次释放资源的工作。这样做并不能保证终结方法会被及时调用或甚至不会被调用,但是在客户端无法通过调用显示的终止方法(或者是根本未调用或忘记调用)来正常结束操作的情况下,这样迟一点释放关键资源总比永远不释放要好。但是如果终结方法发现资源还未被终止,则应该在日志中记录一条警告(最好再调用一次释放资源的方法),因为这表示客户端代码中的一个Bug,应该得到修复,如果你正考虑编写这样的安全网终结方法,就要认真考虑清楚,这种的保护是否值得你付出这份额外的代价。

显示终止方法模式的类(如InputStream、OutputStream、Connection、Timer)都具有终结方法,当它们的终止方法未能被调用的情况下,可以再次在终结方法中显示调用它们的关闭方法,这样终结方法充当了安全网;第二种就是可以回收那些并不重要的本地资源(即本地方法所分配的资源)。

“终结方法链(父类的终结方法)”并不会被自动被执行。如果类(不是Object)有终结方法,并且子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法。你应该在一个try块中终结子类,并在相应的finally块中调用超类的终结方法。这样做可以保证:即使子类的终结过程抛出异常,超类的终结方法也会得到执行,如下面示例:

protected void finalize() throws Throwable{

try{

…// 子类回收动作

}finally{

super.finalize();// 调用父类的终结方法

}

}

如果子类实现者覆盖了超类的终结方法,但是忘了手式调用超类的终结方法,那么超类的终结方法永远也不会被调用到。要防范这样的粗心大意,我们可以为每个将被终结的对象创建一个附加的对象。不是把终结方法放在要求终结处理的类中,而是把终结方法放在一个匿名的类中,该匿名类的唯一作用就是终结它的外围实例。该匿名类的单个实例被称为终结方法守卫者,外围类的每个实例都会创建这样一个守卫者。外围实例在它的私有实例哉中保存着一个对其终结方法守卫者的唯一引用,因些终结方法守卫都与外围实例可以同时启动终结过程。当守卫都被终结的时候,它执行外围实例所期望的终结行为,就好像它的终结方法是外围对象上的一个方法一样:

class Foo{

//终结守卫者

private final Object finalizerGuardian = new Object(){

protected void finalize() throws Throwable{

System.out.println("Foo gc");

}

};

}

class Sub extends Foo{

// 即使没有在子类的终结方法中调用父类的终结方法,父类也会终结

protected void finalize() throws Throwable {

System.out.println("Sub gc");

}

}

注意,公有类Foo并没有终结方法(除了它从Object中继承了一个无关紧要的的之外),所以子类的终结方法是否调用super.finalize并不重要。

总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。当然如果使用了终结方法,就要记得调用super.finalize。如果用终结方法作为安全网,要记得记录终结方法的非法调用。最后,如果需要把终结方法与公有的非final类关联起来,请考虑使用终结方法守卫者,以确保即使子类的终结方法未能调用super.finalize,该终结方法也会被执行。

补充

虽然终结一个对象时,它不会去自动调用父类的终结方法,除非手工调用super.finalize,但是父类里的成员域一定会被调用终结方法,这就是为什么终结守卫者安全的原因。另外,回收的顺序是不确定的,不会像C++中的那样,先调用子类析构函数,再调用父类析构函数。还有一点要注意的是,即使对象的finalize 已经运行了,不能保证该对象被销毁。因为对象可以重生。

对于任何给定对象,Java 虚拟机最多只调用一次 finalize 方法。

垃圾收集器的工作过程大致是这样的:一旦垃圾收集器准备好释放无用对象占用的存储空间,它首先调用那些对象的finalize()方法,然后才真正回收对象的内存。

与 Java 不同,C++ 支持局部对象(存储在栈中)和全局对象(存储在堆中),C++ 能对栈中的对象自动析构,但对于堆中的对象就要手动分配内存与释放。在 Java 中,所有对象都驻留在堆内存,而内存的回收则由垃圾收集器统一回收。

finalize可以用来保护非内存资源被释放,即使我们定义了其它的方法来释放非内存资源,但是其它人未必会调用该方法来释放。在finalize里面可以检查一下,如果没有释放就释放好了,晚释放总比不释放好,这样好比“双保险”。通常我们可以在finalize方法中释放容易被忽略的资源,并且这些都是非常重要的资源。

上一篇:摆花 (DP动态规划)


下一篇:linux下配置yun源