首页 > 代码库 > [Effective Java]第二章 创建和销毁对象
[Effective Java]第二章 创建和销毁对象
第一章 前言
略...
第二章 创建和销毁对象
1、 考虑用静态工厂方法代替构造器
创建对象方法:一是最常用的公有构造器,二是静态工厂方法。下面是一个Boolean的简单示例:
public static Boolean valueOf(boolean b) {
return (b ? Boolean.TRUE : Boolean.FALSE);
}
l 静态工厂方法与构造器不同的第一大优势在于,它们有名称。
作用不同的公有构造器只能通过参数来区别(因为一个类只有一个带有指定签名的构造器,所以多个构造器只能使用不同的参数列表来区分),如果使用静态的工厂方法,则方法名会很清楚地表达方法的作用。
l 静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新对象。
不可变类完全可以使用预先构建好的实例,而不必每次使用时都创建一个对象。另外,将构建好的实例缓存起来重复使用,从而避免创建不必要的重复对象。Boolean.valueOf(boolean)方法就使用了这项技术——它从来不创建对象。
l 静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象。
这样我们在选择返回对象的类时就有了更大的灵活性。这种灵活性的一种应用是,API可以返回对象,同时又不会使对象的类变成公有的,比如我们完全可以先定义一个产品接口类,然后采用私有的内部类去实现这个接口,静态工厂方法返回这个类的实例,这样就隐藏了具体的实现。另外,使用静态工厂方时,要求客户端通过接口来引用被返回的对象,而不是通过它的实现类来引用被返回的对象,这是一种良好的编程习惯。
公有静态工厂方法所返回的对象的类不仅可以是private,而且通过静态工厂方法的参数,还可以随着每次的返回不同的类的实例,只要是已声明返回类型的子类型。这样的好处是,可以在以后的版本中删除这个类重新实现也不会影响到已使用的客户。
静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可能不必存在。这种灵活的静态工厂方法构成了服务提供者框架的基础,例如JDBC API。服务提供者框架是指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把他们从多个实现中解耦出来。
服务提供者框架有三个重要组件:服务接口(Service Interface),这是提供者实现的;提供者注册API(Provider Registration API),这是系统用来注册实现,让客户端访问它们的;服务访问API(Service Access API),是客户端用来获取服务的实例的方法接口。服务访问API一般允许但是不要求客户端指定某种选择提供者的条件。如果没有这样的规定,API就会返默认实现的一个实例。服务访问API是“灵活的静态工厂”,它构成了服务提供者框架的基础。
服务提供者框架的第四个组件是可选的:服务提供者接口(Service Provider Interface)(即工厂方法模式中的工厂接口),这些提供者负责创建其服务实现的实例。如果没有服务提供者接口,实现就按照类名称注册,并通过反射方式进行实例化。对于JDBC来说,Connection就是它的服务接口,DriverManager.registerDriver是提供者注册API,DriverManager.getConnection是服务访问API,Driver就是服务提供者接口。
下面看看这四个组件的应用:
// 服务接口,就是我们的业务接口。(相当于Connection接口,由Sun提供)
public interface Service {
// ...
}
// 服务提供都接口,即业务工厂接口。(相当于Driver接口,由第三方厂家实现)
public interface Provider {
Service newService();
}
// 服务提供者注册与服务提供者接口(好比DriverManager)
public class Services {
private Services() {}
// 服务名与服务映射,即注册容器
private static final Map providers = new ConcurrentHashMap();
public static final String DEFAULT_PROVIDER_NAME = "def";
// 服务提供者注册API,即注册工厂实现,相当于DriverManager.registerDriver
public static void registerDefaultProvider(Provider p) {
registerProvider(DEFAULT_PROVIDER_NAME, p);
}
public static void registerProvider(String name, Provider p) {
providers.put(name, p);
}
// 服务访问API,向外界提供业务实现,相当于DriverManager.getConnection
public static Service newInstance() {
return newInstance(DEFAULT_PROVIDER_NAME);
}
public static Service newInstance(String name) {
Provider p = (Provider) providers.get(name);
if (p == null) {
throw new IllegalArgumentException(
"NO provider registered with name:" + name);
}
return p.newService();
}
}
静态工厂方法的第四大优势在于,在创建参数化类型实例的时候,它们使代码变得更加简洁。比如要创建一个参数化的HashMap,我们需要如下做:
Map<String,List<String>> m= new HashMap<String, List<String>>();
这么长的类型参数实在是不太好,而且随着类型参数变得越来越长,也越来越复杂。但如果有了静态工厂方法,编译器就可以替你推导出类型,new时不需要提供参数类型。例如,假设HashMap提供了这个静态工厂:
public static <k,v> HashMap<k,v> newInstance(){
return new HashMap<k,v>();
}
那么你就可以使用以下简洁的代码来代替上面这段繁琐的声明:
Map<String,List<String>> m= HashMap.newInstance();
但可惜的是,到现在发行的版本1.6止还未加入,不过我们可以把这些方法放在自己的工具类中。
静态工厂方法的一些惯用名称:
valueOf——不太严格地讲,该方返回的实例与它的参数具有相同的值。这样的静态工厂方法实际上是类型转换方法。
of——valueOf的一种更为简洁的替换,在EnumSet中使用并流行起来。
getInstance——返回的实例是通过方法的参数来描述的,但是不能够说与参数具有同样的值。对于Singleton来说,该方法没有参数,并返回唯一值。
newInstance——像getInstance一样,但newInstance能够确保返回每个实例都与把有其他实例不同。
getType——像getInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。
newType——像newInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。
2、 遇到多个构造器参数时要考虑构造器
如果实例化时需要多个参数时,且这些参数中只有少数几个是必须的,而很多是可选的,这时我们一般考虑使用构造器的方式,而不是使用静态工厂方法。
对于此情况,我们可以使用重叠构造器模式——你提供一个只有必要参数的构造器,第二构造器有一个可选参数,第三个有两个可选参数,依此类推,最后一个构造器包含所有可选参数。
public class NutritionFacts {
private final int servingSize; // 必选参数
private final int servings; // 必选参数
private final int calories; // 可选参数
private final int fat; // 可选参数
private final int sodium; // 可选参数
private final int carbohydrate; // 可选参数
// 第一个构造器带上所有必选参数
public NutritionFacts(int servingSize, int servings) {
// 调用另一个构造器
this(servingSize, servings, 0);// 第三个参数为默认值
}
// 第二个构造器在第一个构造器的基础上加上一个可先参数
public NutritionFacts(int servingSize, int servings,
int calories) {
// 第四个参数为默认值
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
当你想要创建实例时,就利用参数列表最短的构造器。虽然重叠构造器模式可行,但是当有许多参数的时候,客户端代码会行难编写,并且难以阅读,随着参数的增加,它很快就失去控制。
遇到许多构造器参数时,还有第二种代替办法,即JavaBean模式,在这种模式下先调用一个无参数构造器来创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可先参数:
public class NutritionFacts {
private int servingSize = -1; //必选,没有默认值
private int servings = -1; //必选,没有默认值
private int calories = 0; //可选,有默认值
private int fat = 0; //可选,有默认值
private int sodium = 0; //可选,有默认值
private int carbohydrate = 0; //可选,有默认值
public NutritionFacts() {}
// set方法
public void setServingSize(int val) { servingSize = val; }
public void setServings(int val) { servings = val; }
public void setCalories(int val) { calories = val; }
public void setFat(int val) { fat = val; }
public void setSodium(int val) { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }
}
这种模式弥补了重叠构造器模式的不足,他创建实例很容易,代码阅读也很容易。但遗憾的是,JavaBean模式自身有着很严重的缺点。因为构造过程中被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态,并且类也无法仅仅通过检验构造器参数的有效性来保证一致。另外,JavaBeans模式阻止了把一个类做成不可变的可能,这就需要应用中确保线程安全。
幸运的是,还有第三替代方法,既能保证重叠构造器模式那样的安全性,也能保证像JavaBeans模式那么好的可读性,这就是Builder模式的一种形式——不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造(或者静态工厂),得到一个builder对象,然后客户端在builder对象上调用类似于setter方法,来设置每个样的可选参数,这个builder是它构建的类的静态成员类,下面是示例:
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// 必输参数
private final int servingSize;
private final int servings;
// 可选参数 - 初始化成默认值
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val)
{ calories = val; return this; }
public Builder fat(int val)
{ fat = val; return this; }
public Builder carbohydrate(int val)
{ carbohydrate = val; return this; }
public Builder sodium(int val)
{ sodium = val; return this; }
// 构造产品
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
// 构造器需要一个builder对象
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
calories(100).sodium(35).carbohydrate(27).build();
}
}
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是种不错的选择,特别是当大多数参数都是可选的时候。与使用传统的重叠构造器模式相比,使用Builder模式的客户端代码将更易于阅读和编写,构建器也比JavaBeans更安全。
3、 用私有构造器或者枚举类型强化Singleton属性
Singleton指仅仅被实例化一次的类。在Java1.5发行版本之前,实例Singleton有两种方法,这两种方法都要把构造器设置成私有的,并导出公有的静态成员,以便允许客户端能够访问该类的唯一实例。在第一种方法中,公有静态成员是个final域:
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
}
私有构造器仅被调用一次,用来实例化仅有的静态final域INSTANCE。由于没有公有的或受保护的构造器,所以保证了实例的全局唯一性:一旦Elvis类被实例化,只会存在一个Elvis实例。客户端任何行为都不会改变这一点,但要提醒一点:享有特权的客户端可以借助于AccessibleObject.setAccessible方法,通过反射机制(第53条)调用私有构造器,如果要抵御这种攻击,可以修改构造器,让他在要求创建第二个实例的时候抛出异常。
第二种方法中公有的成员是个静态工厂方法:
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public static Elvis getInstance() { return INSTANCE; }
}
对于静态方法getInstance的所有调用,都会返回同一个对象引用,所以永远不会创建其他实例(上述提醒依然适用)。
第一种可以在以前的VM上效率要高一点,但在现在的JVM实现几乎都能够将静态工厂方法的调用内联化了,所以不存在性能的差异了,再说第二种方式的主要好处在于:组成类的成员的声明很清楚地表明了这个类是一个Singleteon。
另外,如果一个Singleton类实现了Serializable是不够的,为了维护并保证Singleton,必须(原因请见76,简单的说就是防止私有域导出到序列化流中而受到攻击)声明所有实例域都是瞬时(transient)的,并提供一个readResolve方法,否则,每次序列化时,都会创建一个新的实例,正确的作法:
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
}
从Java1.5版本起,实例Singleton还有第三种方法。只需写一个包含单个元素的枚举类型:
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {
System.out.println("Whoa baby, I‘m outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
这种方法在功能上与公有域方法相近,但是它更加简洁,无偿地提供了化机制,绝对防止多次实例化,即使是在相对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为了实现Singleton的最佳方法。
上面前面两种是懒汉式单例,还有其他的设计方法,请参考XXX
4、 通过私有构造器强化不可实例化的能力
有时候,你可能需编写只包含静态方法和静态域的类。这些类的名声很不好,因为有些人在面向对象的语言中滥用这样的类来编写过程化的程序。尽管如此,它们也确实有它们特有的用处。比如java.util.Arrays、java.util.Collections,这样的工具类不希望被实例化(这与单例是不一样的),实例对它没有任何意义,它们里的成员都是静态的,所有构造器也定义成了private,但这样是否就能确保不能实例化了呢?如果采用反射就不一定了。那怎样才能做完全不能实例化呢?有的人可能企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的,该类可以被子类化,并且该子类也可以被实例化。这样做甚至会误导用户,以为这种类是专门为了继承设计的。正确的作法:
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();//抛异常,防止内部实例化与外部通过反射来实例化
}
}
5、 避免创建不必要的对象
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。如果对象是不可变的,它就始终可以被重用。
反例: String s = new String("stringette");
该语句每次被执行的时候都创建一个新的String实例,但是这些创建对象的动作全都是不必要的,传递给String构造器的参数("stringette")本身就是一个String实例,功能方面等同于构造器创建的对象。如果这种用法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建出很多不必要的String实例。
改进:String s = "stringette";
改进后,只用一个String实例,而不是每次执行的时候都创建一个新的实例,而且,它可以保证,对于所有在同一虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。
关于字符串对象创建细节请看:《String,到底创建了多少个对象?》
对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。例如,静态工厂方法Boolean.valueOf(String)几乎总是比构造器Boolean(String)好,构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做。
除了重用不可变的对象之外,也可以重用那此已知不会被修改的可变对象,即当一个对象创建后,以后不会去改变其内部状态,此时也不会去创建新的对象,而是直接利用以前创建的对象。比如某方法里定义了一个大的对象,而这个对象一但创建完后,就可以共以后方法调用使用时,最好将它定义为成员域,而不是局部变量。
例如,Map接口的keySet方法就是每次返回的是keySet实例,当创建好KeySet视图对象后,它会将它存储到keySet成员域中以便下次使用:
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
它返回的Map对象的Set视图,其中包含该Map中所有的键。粗看起来,好像每次调用keySet都应该创建一个新的Set实例,但是,对于一个给定的Map对象,实际上每次调用keySet都返回同样的Set实例,虽然被返回的Set实例一般是可改变的,但是所有返回的对象在功能上是等同的。
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱:
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
这段程序算出的答案是正确的,但是比实际情况要更慢一些,只因为打错了一个字符。变量sum被声明成Long而不是long,这就意味着程序构造了大约2^31个多的Long实例。
不要错误地认为“创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象,而不是不创建对象”,相反,由于小对象的构造器只做很少量的工作,所以,小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常也是件好事。
反之,通过维护自己的对象池来创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。
另外,在1.5版本里,对基本类型的整形包装类型使用时,要使用形如 Byte.valueOf来创建包装类型,因为-128~127的数会缓存起来,所以我们要从缓冲池中取,Short、Integer、Long也是这样。
6、 消除过期的对象引用
Java中会有内存泄漏,听起来似乎是很不正常的,因为Java提供了垃圾回收器针对内存进行自动回收,但是Java还是会出现内存泄漏的。什么是Java中的内存泄漏:在Java语言中,内存泄漏就是存在一些被分配的对象,这些对象有两个特点:这些对象可达,即在对象内存的有向图中存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象了。如果对象满足这两个条件,该对象就可以判定为Java中的内存泄漏,这些对象不会被GC回收,然而它却占用内存,这就是Java语言中的内存泄漏。Java中的内存泄漏和C++中的内存泄漏还存在一定的区别,在C++里面,内存泄漏的范围更大一些,有些对象被分配了内存空间,但是却不可达,由于C++中没有GC,这些内存将会永远收不回来,在Java中这些不可达对象则是被GC负责回收的,因此程序员不需要考虑这一部分的内存泄漏。二者的图如下:
INCLUDEPICTURE "mhtml:file://G:\\新建文件夹\\12_Java内存模型%20-%20风中的索莉逖亚%20-%20CSDN博客.mht!http://docs.google.com/File?id=dc9d7w83_68ckf7x437_b" \* MERGEFORMATINET
下面看内存泄漏的例子:
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];//size~elements.length间的元素为过期元素
}
/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size){
Object[] oldElements = elements;
elements = new Object[2 * elements.length +1];
System.arraycopy(oldElements, 0, elements, 0, size);
}
}
}
从栈中弹出来的对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收,这是因为,栈内部维护着对象这些对象的过期引用,过期引用是指永远也不会被解除的引用,在本例中,凡是在elements数组的“活动部分”之外的任何引用都是过期的,活动部分是指定elements中下标小于size的那些元素。
这类问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可。对于上面例子,只要一个单元被弹出栈,指向它的引用就过期了。pop方法的修改如下:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result =elements[--size];
elements[size] = null;//消除过期引用,只要外界不再引用即可回收
return result;
}
清空对象引用应该是一种例外,而不是一种规范行为:我们不必对每个对象引用一旦程序不再用到它就把它清空,这样做即没必要,也不是我们所期望的,因为这样做会把程序代码弄得很乱。消除过期引用最好的方法是让引用结束其任命周期,如果你在小的作用域内定义的每一个变量,退出作用域就会自动结束。
内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,
从而使得它不再有用之后很长一段时间内仍然留在缓存中。对于这个问题,有几种可能的解决方案。如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap(弱键映射,允许垃圾回收器回收无外界引用指向象Map中键)代表缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的任命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用外。
另外,随着时间的推移,早些存入的项会变得越来越没有价值,在这种情况下,缓存应该时不时地清除掉没用的项。我们可以使用的是LinkedHashMap来实现,可以在给缓存添加新条目的时候顺便进行清理,如果要实现这种功能,我们需继承LinkedHashMap并重写它的removeEldestEntry方法(默认返回false,即不会删除最旧项),put 和 putAll 将调用此方法,下面是自己写的测试项:
public class CacheLinkedHashMap extends LinkedHashMap {
//允许最大放入的个数,超过则可能删除最旧项
private static final int MAX_ENTRIES = 5;
@Override
// 是否删除最旧项(最先放入)实现
protected boolean removeEldestEntry(Map.Entry eldest) {
Integer num = (Integer) eldest.getValue();// 最早项的值
//如果老的项小于3且已达到最大允许容量则会删除最老的项
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方法中释放容易被忽略的资源,并且这些都是非常重要的资源。
[Effective Java]第二章 创建和销毁对象