Java 对象的生命周期


选自《Java 面向对象编程》一书,作者:孙卫琴 第 11 章 对象的生命周期......................................................................................................................................................................2 11.1 创建对象的方式....................................................................................................2 11.2 构造方法................................................................................................................4 11.2.1 重载构造方法.............................................................................................5 11.2.2 默认构造方法.............................................................................................7 11.2.3 子类调用父类的构造方法.........................................................................7 11.2.4 构造方法的作用域...................................................................................11 11.2.5 构造方法的访问级别...............................................................................12 11.3 静态工厂方法......................................................................................................12 11.3.1 单例(Singleton)类 ...............................................................................14 11.3.2 枚举类.......................................................................................................15 11.3.3 不可变(immutable)类与可变类..........................................................18 11.3.4 具有实例缓存的不可变类.......................................................................21 11.3.5 松耦合的系统接口...................................................................................24 11.4 垃圾回收..............................................................................................................25 11.4.1 对象的可触及性.......................................................................................26 11.4.2 垃圾回收的时间.......................................................................................27 11.4.3 对象的 finalize()方法简介.......................................................................28 11.4.4 对象的 finalize()方法的特点...................................................................28 11.4.5 比较 finalize()方法和 finally 代码块.......................................................31 11.5 清除过期的对象引用..........................................................................................31 11.6 对象的强、软、弱和虚引用..............................................................................33 11.7 小节......................................................................................................................39 PDF created with pdfFactory trial version www.pdffactory.com 第 11 章 对象的生命周期 在 Java 虚拟机管辖的运行时数据区,最活跃的就是位于堆区的生生息息的对象。在 Java 虚拟机的生命周期中,一个个对象被陆陆续续的创建,又一个个被销毁。在 对象生命周期的 开始阶段,需要为对象分配内存,并且初始化它的实例变量。当程序不再使用某个对象,那 么它就会结束生命周期,它的内存可以被 Java 虚拟机的垃圾回收器回收。 11.1 创建对象的方式 在 Java 程序中,对象可以被显式的或者隐含的创建,创建一个对象就是指构造一个类 的实例,前提条件是这个类已经被初始化,第 10 章(类的生命周期)已经对此作了详细介 绍。 有四种显式的创建对象的方式: (1)用 new 语句创建对象,这是最常用的创建对象的方式。 ( 2) 运用 反射手段,调用 java.lang.Class 或 者 java.lang.reflect.Constructor 类的 newInstance()实例方法。 (3)调用对象的 clone()方法。 (4)运用反序列化手段,调用 java.io.ObjectInputStream 对象的 readObject()方法,参见 第 16 章的 16.12 节(对象的序列化与反序列化)。 以下例程 11-1(Customer.java)演示了用前面三种方式创建对象的过程。 例程 11-1 Customer.java public class Customer implements Cloneable{ private String name; private int age; public Customer(){ this("unknown",0); System.out.println("call default constructor"); } public Customer(String name,int age){ this.name=name; this.age=age; System.out.println("call second constructor"); } public Object clone()throws CloneNotSupportedException{return super.clone();} public boolean equals(Object o){ if(this==o)return true; if(! (o instanceof Customer)) return false; final Customer other=(Customer)o; if(this.name.equals(other.name) && this.age==other.age) PDF created with pdfFactory trial version www.pdffactory.com return true; else return false; } public String toString(){return "name="+name+",age="+age;} public static void main(String args[])throws Exception{ //运用反射手段创建 Customer 对象 Class objClass=Class.forName("Customer"); Customer c1=(Customer)objClass.newInstance(); //会调用 Customer 类的默认构造方法 System.out.println("c1: "+c1); //打印 name=unknown,age=0 //用 new 语句创建 Customer 对象 Customer c2=new Customer("Tom",20); System.out.println("c2: "+c2); //打印 name=tom,age=20 //运用克隆手段创建 Customer 对象 Customer c3=(Customer)c2.clone(); //不会调用 Customer 类的构造方法 System.out.println("c2==c3 : "+(c2==c3)); //打印 false System.out.println("c2.equals(c3) : "+c2.equals(c3)); //打印 true System.out.println("c3: "+c3); //打印 name=tom,age=20 } } 以上程序的打印结果如下: call second constructor call default constructor c1: name=unknown,age=0 call second constructor c2: name=Tom,age=20 c2==c3 : false c2.equals(c3) : true c3: name=Tom,age=20 从以上打印结果看出,用 new 语句或 Class 对象的 newInstance()方法创建 Customer 对 象时,都会执行 Customer 类的构造方法,而用对象的 clone()方法创建 Customer 对象时,不 会执行 Customer 类的构造方法。在 Object 类中定义了 clone()方法,它的定义如下: protected Object clone() throws CloneNotSupportedException{ if (!(this instanceof Cloneable)) throw new CloneNotSupportedException(); … } Object 类的 clone()方法具有以下特点: (1)声明为 protected 类型,Object 的子类如果希望对外公开 clone()方法,必须扩大访 问权限,例如在以上 Customer 类中,把 clone()方法的访问级别改为 public。 ( 2 ) 如果 Java 类 没 有实现 Cloneable 接口,clone() 方法会抛出 CloneNotSupportedException 异常。Object 的子类如果允许客户程序调用其 clone()方法,那 么这个类必须实现 Cloneable 接口。 (3)Object 类在 clone()方法的实现中会创建一个复制的对象,这个对象与原来的对象 具有不同的内存地址,不过它们的属性值相同。在本例中,c3 由 c2 克隆而成,它们的内存 PDF created with pdfFactory trial version www.pdffactory.com 地址不一样,但属性值相同。 除了以上四种显式的创建对象的方式,在程序中还可以隐含的创建对象,包括以下几种 情况: (1)对于 java 命令中的每个命令行参数,Java 虚拟机会创建相应的 String 对象,并把 它们组织到一个 String 数组中,再把它作为参数传给程序入口 main(String args[])方法。 (2)程序代码中的 String 类型的直接数对应一个 String 对象,例如: String s1="Hello"; String s2="Hello"; //s2 和 s1 引用同一个 String 对象 String s3=new String("Hello"); System.out.println(s1==s2); //打印 true System.out.println(s1==s3); //打印 false 执行完以上程序,内存中实际上只有两个 String 对象,一个是直接数,由 Java 虚拟机 隐含的创建,还有一个通过 new 语句显式的创建。 (3)字符串操作符“+”的运算结果为一个新的 String 对象。例如: String s1="H"; String s2=" ello"; String s3=s1+s2; //s3 引用一个新的 String 对象 System.out.println(s3=="Hello"); //打印 false System.out.println(s3.equals("Hello")); //打印 true (4)当 Java 虚拟机加载一个类时,会隐含的创建描述这个类的 Class 实例,参见第 10 章的 10.2.1 节(类的加载)。 不管采取哪种方式创建对象,Java 虚拟机创建一个对象都包含以下步骤。 (1)给对象分配内存。 (2)将对象的实例变量自动初始化为其变量类型的默认值。 (3)初始化对象,给实例变量赋予正确的初始值。 对于以上第三个步骤,Java 虚拟机可采用三种方式来初始化对象,到底采用何种初始化 方式取决于创建对象的方式: l 如果对象是通过 clone()方法创建的,那么 Java 虚拟机把原来被克隆对象的实例变 量的值拷贝到新对象中。 l 如果对象是通过 ObjectInputStream 类的 readObject()方法创建的,那么 Java 虚拟机 通过从输入流中读入的序列化数据来初始化那些非暂时性(non-transient)的实例 变量。 l 在其他情况下,如果实例变量在声明时被显式初始化,那就把初始化值赋给实例变 量,接着再执行构造方法。这是最常见的初始化对象的方式。 11.2 构造方法 从上一节可以看出,在多数情况下,初始化一个对象的最终步骤是去调用这个对象的构 造方法。构造方法负责对象的初始化工作,为实例变量赋予合适的初始值。构造方法必须满 足以下语法规则: l 方法名必须与类名相同。 PDF created with pdfFactory trial version www.pdffactory.com l 不要声明返回类型。 l 不能被 static、final、synchronized、abstract 和 native 修饰。构造方法不能被子类继 承,所以用 final 和 abstract 修饰没有意义。构造方法用于初始化一个新建的对象, 所以用 static 修饰没有意义。多个线程不会同时创建内存地址相同的同一个对象, 因此用 synchronized 修饰没有必要。此 外 ,Java 语言不支持 native 类型的构造方法。 在以下 Sample 类中,具有 int 返回类型的 Sample(int x)方法只是个普通的实例方法,不 能作为构造方法: public class Sample { private int x; public Sample() { // 不带参数的构造方法 this(1); } public Sample(int x) { //带参数的构造方法 this.x=x; } public int Sample(int x) { //不是构造方法 return x++; } } 以上例子尽管能编译通过,但是把实例方法和构造方法同名,不 是好 的编程习惯,容易 引起混淆。例如以下 Mystery 类的 Mystery()方法有 void 返回类型,因此是普通的实例方法: public class Mystery { private String s; public void Mystery() { //不是构造方法 s = "constructor"; } void go() { System.out.println(s); } public static void main(String[] args) { Mystery m = new Mystery(); m.go(); } } 以上程序的打印结果为 null。因为用 new 语句创建 Mystery 实例时,调用的是 Mystery 类的默认构造方法,而不是以上有 void 返回类型的 Mystery()方法。关于默认构造方法的概 念,参见本章第 11.2.2 节(默认构造方法)。 11.2.1 重载构造方法 当通过 new 语句创建一个对象时,在不同的条件下,对象可能会有不同的初始化行为。 例如对于公司新进来的一个雇员,在一开始的时候,有可能他的姓名和年龄是未知的,也有 可能仅仅他的姓名是已知的,也有可能姓名和年龄都是已知的。如果姓名是未知的,就暂且 把姓名设为“无名氏”,如果年龄是未知的,就暂且把年龄设为-1。 可通过重载构造方法来表达对象的多种初始化行为。以下例程 11-2 的 Employee 类的构 造方法有三种重载形式。在 一 个 类的多个构造方法中,可 能 会 出现一些重复操作。为了提高 代码的可重用性,Java 语言允许在一个构造方法中,用 this 语句来调用另一个构造方法。 PDF created with pdfFactory trial version www.pdffactory.com 例程 11-2 Employee.java public class Employee { private String name; private int age; /** 当雇员的姓名和年龄都已知,就调用此构造方法 */ public Employee(String name, int age) { this.name = name; this.age=age; } /** 当雇员的姓名已知而年龄未知,就调用此构造方法 */ public Employee(String name) { this(name, -1); } /** 当雇员的姓名和年龄都未知,就调用此构造方法 */ public Employee() { this( "无名氏" ); } public void setName(String name){this.name=name; } public String getName(){return name; } public void setAge(int age){this.age=age;} public int getAge(){return age;} } 以下程序分别通过三个构造方法创建了三个 Employee 对象: Employee zhangsan=new Employee("张三",25); Employee lisi=new Employee("李四"); Employee someone=new Employee(); 在 Employee(String name)构造方法中,this(name,-1)语句用于调用 Employee(String name,int age) 构造方法。在 Employee() 构造方法中,this(" 无名氏") 语句用于调用 Employee(String name)构造方法。 用 this 语句来调用其他构造方法时,必须遵守以下语法规则: (1)假如在一个构造方法中使用了 this 语句,那么它必须作为构造方法的第一条语句 (不考虑注释语句)。以下构造方法是非法的: public Employee(){ String name="无名氏"; this(name); //编译错误,this 语句必须作为第一条语句 } (2)只能在一个构造方法中用 this 语句来调用类的其他构造方法,而不能在实例方法 中用 this 语句来调用类的其他构造方法。 (3)只能用 this 语句来调用其他构造方法,而不能通过方法名来直接调用构造方法。 以下对构造方法的调用方式是非法的: public Employee() { String name= "无名氏"; Employee(name); //编译错误,不能通过方法名来直接调用构造方法 } PDF created with pdfFactory trial version www.pdffactory.com 11.2.2 默认构造方法 默认构造方法是没有参数的构造方法,可分为两种:( 1 ) 隐含的默认构造方法(2)程 序显式定义的默认构造方法。 在 Java 语言中,每个类至少有一个构造方法。为了保证这一点,如果用户定义的类中 没有提供任何构造方法,那么 Java 语言将自动提供一个隐含的默认构造方法。该构造方法 没有参数,用 public 修饰,而且方法体为空,格式如下: public ClassName(){} //隐含的默认构造方法 在程序中也可以显式的定义默认构造方法,它可以是任意的访问级别。例如: protected Employee() { //程序显式定义的默认构造方法 this("无名氏"); } 如果类中显式定义了一个或多个构造方法,并且所有的构造方法都带参数,那么这个类 就失去了默认构造方法。在以下程序中,Sample1 类有一个隐含的默认构造方法,Sample2 类没有默认构造方法,Sample3 类有一个显式定义的默认构造方法: public class Sample1{} public class Sample2{ public Sample2(int a){System.out.println("My Constructor");} } public class Sample3{ public Sample3(){System.out.println("My Default Constructor");} } 可以调用 Sample1 类的默认构造方法来创建 Sample1 对象: Sample1 s=new Sample1(); //合法 Sample2 类没有默认构造方法,因此以下语句会导致编译错误: Sample2 s=new Sample2(); //编译出错 Sample3 类显式定义了默认构造方法,因此以下语句是合法的。 Sample3 s=new Sample3(); 11.2.3 子类调用父类的构造方法 父类的构造方法不能被子类继承。以下 MyException 类继承了 java.lang.Exception 类: public class MyException extends Exception{} // MyException 类只有一个隐含的默认构造方法 尽管在 Exception 类中定义了如下形式的构造方法: public Exception(String msg) 但 MyException 类不会继承以上 Exception 类的构造方法,因此以下代码是不合法的: //编译出错,MyException 类不存在这样的构造方法 Exception e=new MyException("Something is error"); 在子类的构造方法中,可以通过 super 语句调用父类的构造方法。例如: public class MyException extends Exception{ public MyException(){ //调用 Exception 父类的 Exception(String msg)构造方法 PDF created with pdfFactory trial version www.pdffactory.com super("Something is error"); } public MyException(String msg){ //调用 Exception 父类的 Exception(String msg)构造方法 super(msg); } } 用 super 语句来调用父类的构造方法时,必须遵守以下语法规则。 (1)在子类的构造方法中,不能直接通过父类方法名调用父类的构造方法,而是要使 用 super 语句,以下代码是非法的: public MyException(String msg){ Exception(msg); //编译错误 } (2)假如在子类的构造方法中有 super 语句,它必须作为构造方法的第一条语句,以 下代码是非法的: public MyException(){ String msg= "Something wrong"; super(msg); //编译错误,super 语句必须作为构造方法的第一条语句 } 在创建子类的对象时,Java 虚拟机首先执行父类的构造方法,然后再执行子类的构造方 法。在多级继承的情况下,将从继承树的最上层的父类开始,依次执行各个类的构造方法, 这可以保证子类对象从所有直接或间接父类中继承的实例变量都被正确的初始化。例如以下 Base 父类和 Sub 子类分别有一个实例变量 a 和 b,当构造 Sub 实例时,这两个实例变量都会 被初始化。 public class Base{ private int a; public Base(int a){ this.a=a;} public int getA(){return a;} } public class Sub extends Base{ private int b; public Base(int a,int b){super(a); this.b=b;} public int getB(){return b;} public static void main(String args[]){ Sub sub=new Sub(1,2); System.out.println("a="+sub.getA()+" b="+sub.getB()); //打印 a=1 b=2 } } 在以下例程 11-3(Son.java)中,Son 类继承 Father 类,Father 类继承 Grandpa 类。这 三个类都显式定义了默认的构造方法,此外还定义了一个带参数的构造方法。 例程 11-3 Son.java class Grandpa{ protected Grandpa(){ System.out.println("default Grandpa"); PDF created with pdfFactory trial version www.pdffactory.com } public Grandpa(String name){ System.out.println(name); } } class Father extends Grandpa{ protected Father(){ System.out.println("default Father"); } public Father(String grandpaName,String fatherName){ super(grandpaName); System.out.println(fatherName); } } public class Son extends Father{ public Son(){ System.out.println("default Son"); } public Son(String grandpaName,String fatherName,String sonName){ super(grandpaName,fatherName); System.out.println(sonName); } public static void main(String args[]){ Son s1= new Son("My Grandpa", "My Father", "My Son"); //① Son s2=new Son(); //② } } 执行以上 main()方法的第①条语句,打印结果如下: My Grandpa My Father My Son 此时构造方法的执行顺序如图 11-1 所示。 图 11-1 调用 Son 类的带参数的构造方法时所有构造方法的执行顺序 当子类的构造方法没有用 super 语句显式调用父类的构造方法,那么通过这个构造方法 创建子类对象时,Java 虚拟机会自动先调用父类的默认构造方法。执行以上 Son 类的 main() 方法的第②条语句,打印结果如下: Son(String grandpaName,String fatherName,String sonName) Father(String grandpaName,String fatherName) Grandpa(String name) Object() 执行顺序 PDF created with pdfFactory trial version www.pdffactory.com default Grandpa default Father default Son 此时构造方法的执行顺序如图 11-2 所示。 图 11-2 调用 Son 类的默认构造方法时所有构造方法的执行顺序 当子类的构造方法没有用 super 语句显式调用父类的构造方法,而父类又没有提供默认 构造方法时,将会出现编译错误。例如把例程 11-3 做适当修改,删除 Grandpa 类中显式定 义的默认构造方法: // protected Grandpa(){ // System.out.println("default GrandPa"); // } 这样,Grandpa 类就失去了默认构造方法,这时,在编译 Father 类的默认构造方法时, 因为找不到 Grandpa 类的默认构造方法而编译出错。如果把 Grandpa 类的默认构造方法的 protected 访问级别改为 private 访问级别,也会导致编译错误,因为 Father 类的默认构造方 法无法访问 Grandpa 类的私有默认构造方法。 在以下例子中,子类 Sub 的默认构造方法没有通过 super 语句调用父类的构造方法,而 是通过 this 语句调用了自身的另一个构造方法 Sub(int i),而在 Sub(int i)中通过 super 语句调 用了父类 Base 的 Base(int i)构造方法。这样,无论通过 Sub 类的哪个构造方法来创建 Sub 实例,都会先调用父类 Base 的 Base(int i)构造方法。 class Base{ Base(int i){System.out.println("call Base(int i)");} } public class Sub extends Base{ Sub(){this(0); System.out.println("call Sub()");} Sub(int i){super(i); System.out.println("call Sub(int i)");} public static void main(String args[]){ Sub sub=new Sub(); } } 执行以上 Sub 类的 main()方法的 new Sub()语句,打印结果如下: call Base(int i) call Sub(int i) call Sub() 此时构造方法的执行顺序如图 11-3 所示。 Son() Father() Grandpa() Object() 执行顺序 PDF created with pdfFactory trial version www.pdffactory.com 图 11-3 调用 Sub 类的默认构造方法时所有构造方法的执行顺序 在下面的例子中,Base 类中没有定义任何构造方法,它实际上有一个隐含的默认构造 方法: Base(){} Sub 类的 Sub(int i)构造方法没有用 super 语句显式调用父类的构造方法,因此当创建 Sub 实例时,会先调用 Base 父类的隐含默认构造方法。 class Base{} //具有隐含默认构造方法 public class Sub extends Base{ Sub(int i){System.out.println(i);} public static void main(String args[]){ System.out.println(new Sub(1)); //打印 1 } } 11.2.4 构造方法的作用域 构造方法只能通过以下方式被调用: l 当前类的其他构造方法通过 this 语句调用它。 l 当前类的子类的构造方法通过 super 语句调用它。 l 在程序中通过 new 语句调用它。 对于例程 11-4(Sub.java)的代码,请读者自己分析某些语句编译出错的原因。 例程 11-4 Sub.java class Base{ public Base(int i,int j){} public Base(int i){ this(i,0); //合法 Base(i,0); //编译出错 } } class Sub extends Base{ public Sub(int i,int j){ super(i,0); //合法 } void method1(int i,int j){ this(i,j); //编译出错 Sub(i,j); //编译出错 } Sub() Sub(int i) Base(int i) Object() 执行顺序 PDF created with pdfFactory trial version www.pdffactory.com void method2(int i,int j){ super(i,j); //编译出错 } void method3(int i,int j){ Base s=new Base(0,0); //合法 s.Base(0,0); //编译出错 } } 11.2.5 构造方法的访问级别 构造方法可以处于 public、protected、默认和 private 这四种访问级别之一。本节着重介 绍构造方法处于 private 级别的意义。 当构造方法为 private 级别,意味着只能在当前类中访问它:在当前类的其他构造方法 中可以通过 this 语句调用它,此外还可以在当前类的成员方法中通过 new 语句调用它。 在以下场合之一,可以把类的所有构造方法都声明为 private 类型。 (1)在这个类中仅仅包含了一些供其他程序调用的静态方法,没有任何实例方法。其 他程序无需创建该类的实例,就能访问类的静态方法。例如 java.lang.Math 类就符合这种情 况,在 Math 类中提供了一系列用于数学运算的公共静态方法,为了禁止外部程序创建 Math 类的实例,Math 类的惟一的构造方法是 private 类型的: private Math(){} 在第 7 章的 7.2 节(abstract 修饰符)提到过,abstract 类型的类也不允许实例化。也许 你会问,把 Math 类定义为如下 abstract 类型,不是也能禁止 Math 类被实例化吗? public abstract class Math{…} 如果一个类是抽象类,意味着它是专门用于被继承的类,可 以 拥 有子类,而且可以创建 具体子类的实例。而 JDK 并不希望用户创建 Math 类的子类,在这种情况下,把类的构造方 法定义为 private 类型更合适。 (2)禁止这个类被继承。当一个类的所有构造方法都是 private 类型,假如定义了它的 子类,那么子类的构造方法无法调用父类的任何构造方法,因此会导致编译错误。在第 7 章的 7.3.1 节(final 类)提到过,把一个类声明为 final 类型,也能禁止这个类被继承。这两 者的区别是: l 如果一个类允许其他程序用 new 语句构造它的实例,但不允许拥有子类,那就把 类声明为 final 类型。 l 如果一个类既不允许其他程序用 new 语句构造它的实例,又不允许拥有子类,那 就把类的所有构造方法声明为 private 类型。 由于大多数类都允许其他程序用 new 语句构造它的实例,因此用 final 修饰符来禁止类 被继承的做法更常见。 (3)这个类需要把构造自身实例的细节封装起来,不允许其他程序通过 new 语句创建 这个类的实例,这个类向其他程序提供了获得自身实例的静态方法,这种方法称为静态工厂 方法,本章第 11.3 节(静态工厂方法)对此作了进一步的介绍。 11.3 静态工厂方法 创建类的实例的最常见的方式是用 new 语句调用类的构造方法。在这种情况下,程序 可以创建类的任意多个实例,每执行一条 new 语句,都会导致 Java 虚拟机的堆区中产生一 PDF created with pdfFactory trial version www.pdffactory.com 个新的对象。假如类需要进一步封装创建自身实例的细节,并且控制自身实例的数目,那么 可以提供静态工厂方法。 例如 Class 实例是 Java 虚拟机在加载一个类时自动创建的,程序无法用 new 语句创建 java.lang.Class 类的实例,因为 Class 类没有提供 public 类型的构造方法。为了使程序能获得 代表某个类的 Class 实例,在 Class 类中提供了静态工厂方法 forName(String name),它的使 用方式如下: Class c=Class.forName("Sample"); //返回代表 Sample 类的实例 静态工厂方法与用 new 语句调用的构造方法相比,有以下区别。 (1)构造方法的名字必须与类名相同,这一特性的优点是符合 Java 语言的规范,缺点 是类的所有重载的构造方法的名字都相同,不 能 从 名 字上区分每个重载方法,容易引起混淆。 静态工厂方法的方法名可以是任意的,这一特性的优点是可以提高程序代码的可读性, 在方法名中能体现与实例有关的信息。例如以下例程 11-5 的 Gender 类有两个静态工厂方法 getFemale()和 getMale()。 例程 11-5 Gender.java public class Gender{ private String description; private static final female; private static final male; private Gender(String description){this.desciption=description;} public static Gender getFemale(){ if(female==null) female=new Gender("女"); return female; } public static Gender getMale(){ if(male==null) male=new Gender("男"); return male; } public String getDescription(){return description;} } 这一特性的缺点是与其他的静态方法没有明显的区别,使用户难以识别类中到底哪些静 态方法专门负责返回类的实例。为了减少这一缺点带来的负面影响,可 以在为静态工厂方法 命名时尽量遵守约定俗称的规范,当然这不是必须的。目前比较流行的规范是把静态工厂方 法命名为 valueOf 或者 getInstance: l valueOf:该方法返回的实例与它的参数具有同样的值。例如: Integer a=Integer.valueOf(100); //返回取值为 100 的 Integer 对象 从上面代码可以看出,valueOf()方法能执行类型转换操作,在本例中,把 int 类型的基 本数据转换为 Integer 对象。 l getInstance:返回的实例与参数匹配,例如: //返回符合中国标准的日历 Calendar cal=Calendar.getInstance(Locale.CHINA); PDF created with pdfFactory trial version www.pdffactory.com (2)每次执行 new 语句时,都会创建一个新的对象。而静态工厂方法每次被调用的时 候,是否会创建一个新的对象完全取决于方法的实现。 (3)new 语句只能创建当前类的实例,而静态工厂方法可以返回当前类的子类的实例, 这一特性可以在创建松耦合的系统接口时发挥作用,参见本章 11.3.5 节(松耦合的系统接 口)。 静态工厂方法最主要的特点是:每次被调用的时候,不一定要创建一个新的对象。利用 这一特点,静态工厂方法可用来创建以下类的实例: l 单例(Singleton)类:只有惟一的实例的类。 l 枚举类:实例的数量有限的类。 l 具有实例缓存的类:能把已经创建的实例暂且存放在缓存中的类。 l 具有实例缓存的不可变类:不可变类的实例一旦创建,其属性值就不会被改变。 在下面几小节,将结合具体的例子,介绍静态工厂方法的用途。 11.3.1 单例(Singleton)类 单例类是指仅有一个实例的类。在 系统中具有惟一性的组件可作为单例类,这种类的实 例通常会占用较多的内存,或者实例的初始化过程比较冗长,因此随意创建这些类的实例会 影响系统的性能。 熟悉 Struts 和 Hibernate 软件的读者会发现,Struts 框架的 ActionServlet 类就是单例类,此外,Hibernate 的 SessionFactory 和 Configuration 类也是 单例类。 以下例程 11-6 的 GlobalConfig 类就是个单例类,它用来存放软件系统的配置信息。这 些配置信息本来存放在配置文件中,在 GlobalConfig 类的构造方法中,会从配置文件中读取 配置信息,把它存放在 properties 属性中。 例程 11-6 GlobalConfig.java import java.io.InputStream; import java.io.FileInputStream; import java.io.IOException; import java.util.Properties; public class GlobalConfig { private static final GlobalConfig INSTANCE=new GlobalConfig(); private Properties properties = new Properies(); private GlobalConfig(){ try{ //加载配置信息 InputStream in=getClass().getResourceAsStream("myapp.properties"); properties.load(in); in.close(); }catch(IOException e){throw new RuntimeException("加载配置信息失败");} } public static GlobalConfig getInstance(){ //静态工厂方法 return INSTANCE; } public Properties getProperties() { return properties; } PDF created with pdfFactory trial version www.pdffactory.com } 实现单例类有两种方式: (1)把构造方法定义为 private 类型,提供 public static final 类型的静态变量,该变量 引用类的惟一的实例,例如: public class GlobalConfig { public static final GlobalConfig INSTANCE =new GlobalConfig(); private GlobalConfig() {…} … } 这种方式的优点是实现起来比较简洁,而且类的成员声明清楚的表明该类是单例类。 (2)把构造方法定义为 private 类型,提供 public static 类型的静态工厂方法,例如: public class GlobalConfig { private static final GlobalConfig INSTANCE =new GlobalConfig(); private GlobalConfig() {…} public static GlobalConfig getInstance(){return INSTANCE;} … } 这种方式的优点是可以更灵活的决定如何创建类的实例,在不改变 GlobalConfig 类的接 口的前提下,可以修改静态工厂方法 getInstance()的实现方式,比如把单例类改为针对每个 线程分配一个实例,参见例程 11-7。 例程 11-7 GlobalConfig.java package uselocal; public class GlobalConfig { private static final ThreadLocal threadConfig= new ThreadLocal(); private Properties properties = null; private GlobalConfig(){…} public static GlobalConfig getInstance(){ GlobalConfig config=threadConfig.get(); if(config==null){ config=new GlobalConfig(); threadConfig.set(config); } return config; } public Properties getProperties() {return properties; } } 以上程序用到了 ThreadLocal 类,关于它的用法参见第 13 章的 13.14 节(ThreadLocal 类)。 11.3.2 枚举类 枚举类是指实例的数目有限的类,比如表示性别的 Gender 类,它只有两个实例: Gender.FEMALE 和 Gender.MALE,参见例程 11-8。在创建枚举类时,可以考虑采用以下设 计模式: l 把构造方法定义为 private 类型。 PDF created with pdfFactory trial version www.pdffactory.com l 提供一些 public static final 类型的静态变量,每个静态变量引用类的一个实例。 l 如果需要的话,提供静态工厂方法,允许用户根据特定参数获得与之匹配的实例。 例程 11-8 是改进的 Gender 类的源程序,它采用了以上设计模式。 例程 11-8 Gender.java import java.io.Serializable; import java.util.*; public class Gender implements Serializable { private final Character sex; private final transient String description; public Character getSex() { return sex; } public String getDescription() { return description; } private static final Map instancesBySex = new HashMap(); /** * 把构造方法声明为 private 类型,以便禁止外部程序创建 Gender 类的实例 */ private Gender(Character sex, String description) { this.sex = sex; this.description = description; instancesBySex.put(sex, this); } public static final Gender FEMALE = new Gender(new Character('F'), "Female"); public static final Gender MALE = new Gender(new Character('M'), "Male"); public static Collection getAllValues() { return Collections.unmodifiableCollection(instancesBySex.values()); } /** * 按照参数指定的性别缩写查找 Gender 实例 */ public static Gender getInstance(Character sex) { Gender result = (Gender)instancesBySex.get(sex); if (result == null) { throw new NoSuchElementException(sex.toString()); } return result; } public String toString() { PDF created with pdfFactory trial version www.pdffactory.com return description; } /** * 保证反序列化时直接返回 Gender 类包含的静态实例 */ private Object readResolve() { return getInstance(sex); } } 在例程 11-8 的 Gender 类中,定义了两个静态 Gender 类型的常量:FEMALE 和 MALE, 它们被存放在 HashMap 中。Gender 类的 getInstance(Character sex)静态工厂方法根据参数返 回匹配的 Gender 实例。在其他程序中,既可以通过 Gender.FEMALE 的形式访问 Gender 实 例,也可以通过 Gender 类的 getInstance(Character sex)静态工厂方法来获得与参数匹配的 Gender 实例。 以下程序代码演示了 Gender 类的用法。 public class Person{ private String name; private Gender gender; public Person(String name,Gender gender){this.name=name;this.gender=gender;} //此处省略 name 和 gender 属性的相应的 public 类型的 get 和 set 方法 … public static void main(String args[]){ Person mary=new Person("Mary",Gender.FEMALE); } } 也许你会问:用一个 int 类型的变量也能表示性别,比 如 用 0 表 示 女 性,用 1 表 示 男 性, 这样不是会使程序更简洁吗?在以下代码中,gender 变量被定义为 int 类型: public class Person{ private String name; private int gender; public static final int FEMALE=0; public static final int MALE=1; public Person(String name,int gender){ if(gender!=0 && gender!=1)throw new IllegalArgumentException("无效的性别"); this.name=name; this.gender=gender; } //此处省略 name 和 gender 属性的相应的 public 类型的 get 和 set 方法 public static void main(String args[]){ Person mary=new Person("Mary", FEMALE); Person tom=new Person("Tom",-1); //运行时抛出 IllegalArgumentException } } 在以上 Person 类的构造方法中,gender 参数为 int 类型,编程人员可以为 gender 参数传 PDF created with pdfFactory trial version www.pdffactory.com 递任意的整数值,如果传递的 gender 参数是无效的,Java 编译器不会检查这种错误,只有 到运行时才会抛出 IllegalArgumentException。 假如使用 Gender 枚举类,在 Person 类的构造方法中,gender 参数为 Gender 类型,编 程人员只能把 Gender 类型的实例传给 gender 参数,否则就通不过 Java 编译器的类型检查。 由此可见,枚举类能够提高程序的健壮性,减少程序代码出错的机会。 假如枚举类支持序列化,那么必须提供 readResolve()方法,在该方法中调用静态工厂方 法 getInstance(Character sex)来获得相应的实例,这可以避免在每次反序列化时,都创建一个 新的实例。这条建议也同样适用于单例类。关于序列化和反序列化的概念参见第 16 章的 16.12 节(对象的序列化与反序列化)。 11.3.3 不可变(immutable)类与可变类 所谓不可变类,是指当创建了这个类的实例后,就不允许修改它的属性值。在 JDK 的 基本类库中,所有基本类型的包装类,如 Integer 和 Long 类,都是不可变类,java.lang.String 也是不可变类。以下代码创建了一个 String 对象和 Integer 对象,它们的值分别为“Hello” 和 10,在程序代码中无法再改变这两个对象的值,因为 Integer 和 String 类没有提供修改其 属性值的接口: String s=new String("Hello"); Integer i=new Integer(10); 用户在创建自己的不可变类时,可以考虑采用以下设计模式: l 把属性定义为 private final 类型。 l 不对外公开用于修改属性的 setXXX()方法。 l 只对外公开用于读取属性的 getXXX()方法。 l 在构造方法中初始化所有属性。 l 覆盖 Object 类的 equals()和 hashCode()方法,在 equals()方法中根据对象的属性值来 比较两个对象是否相等,并且保证用 equals()方法判断为相等的两个对象的 hashCode()方法的返回值也相等,这可以保证这些对象能正确的放到 HashMap 或 HashSet 集合中,第 15 章的 15.2.2 节(HashSet 类)对此做了进一步解释。 l 如果需要的话,提供实例缓存和静态工厂方法,允许用户根据特定参数获得与之匹 配的实例,参见本章第 11.3.4 节(具有实例缓存的不可变类)。 下面例程 11-9 的 Name 类就是不可变类,它仅仅提供了读取 sex 和 description 属性的 getXXX()方法,但没有提供修改这些属性的 setXXX()方法。 例程 11-9 Name.java public class Name { private final String firstname; private final String lastname; public Name(String firstname, String lastname) { this.firstname = firstname; this.lastname = lastname; } public String getFirstname(){ return firstname; } public String getLastname(){ PDF created with pdfFactory trial version www.pdffactory.com return lastname; } public boolean equals(Object o){ if (this == o) return true; if (!(o instanceof Name)) return false; final Name name = (Name) o; if(!firstname.equals(name.firstname)) return false; if(!lastname.equals(name.lastname)) return false; return true; } public int hashCode(){ int result; result= (firstname==null?0:firstname.hashCode()); result = 29 * result + (lastname==null?0:lastname.hashCode()); return result; } public String toString(){ return lastname+" "+firstname; } } 假定 Person 类的 name 属性定义为 Name 类型: public class Person{ private Name name; private Gender gender; … } 以下代码创建了两个 Person 对象,他们的姓名都是“王小红”,一个是女性,一个是男 性。在最后一行代码中,把第一个 Person 对象的姓名改为“王小虹”: Name name=new Name("小红","王"); Person person1=new Person(name,Gender.FEMALE); Person person2=new Person(name,Gender.MALE); name=new Name("小虹","王"); person1.setName(name); //修改名字 与不可变类对应的是可变类,可变类的实例的属性是允许修改的。如果把以上例程 11-9 的 Name 类的 firstname 属性和 lastname 属性的 final 修饰符去除,并且增加相应的 public 类 型的 setFirstname()和 setLastname()方法,Name 类就变成了可变类。以下程序代码本来的意 图也是创建两个 Person 对象,她们的姓名都是“王小红”,接着把第一个 Person 对象的姓名 改为“王小虹”: //假定以下 Name 类是可变类 Name name=new Name("小红","王"); Person person1=new Person(name,Gender.FEMALE); Person person2=new Person(name,Gender.MALE); name.setFirstname("小虹"); //试图修改第一个 Person 对象的名字 以上最后一行代码存在错误,因为它会把两个 Person 对象的姓名都改为“王小虹”。由 此可见,使用可变类更容易使程序代码出错。因为随意改变一个可变类对象的状态,有可能 PDF created with pdfFactory trial version www.pdffactory.com 会导致与之关联的其他对象的状态被错误的改变。 不可变类的实例在整个生命周期中永远保持初始化的状态,它没有任何状态变化,简 化 了 与 其 他 对象之间的关系。不可变类具有以下优点: l 不可变类能使程序更加安全,不容易出错。 l 不可变类是线程安全的,当多个线程访问不可变类的同一个实例时,无需进行线程 的同步。关于线程安全的概念,参见本书第 13 章的 13.8.4 节(线程安全的类)。 由此可见,应该优先考虑把类设计为不可变类,假使必须使用可变类,也应该把可变类 的尽可能多的属性设计为不可变的,即用 final 修饰符来修饰,并且不对外公开用于改变这 些属性的方法。 在创建不可变类时,假如它的属性的类型是可变类型,必要的情况下,必须提供保护性 拷贝,否则,这个不可变类的实例的属性仍然有可能被错误的修改。这条建议同样适用于可 变类中用 final 修饰的属性。 例如以下例程11-10的Schedule类包含学校的开学时间和放假时间信息,它是不可变类, 它的两个属性 start 和 end 都是 final 类型,表示不允许被改变,但是这两个属性都是 Date 类 型,而 Date 类是可变类。 例程 11-10 Schedule.java import java.util.Date; public final class Schedule{ private final Date start; //开学时间,不允许被改变 private final Date end; //放假时间,不允许被改变 public Schedule(Date start,Date end){ //不允许放假日期在开学日期的前面 if(start.compareTo(end)>0) throw new IllegalArgumentException(start +" after " +end); this.start=start; this.end=end; } public Date getStart(){return start;} public Date getEnd(){return end;} } 尽管以上 Schedule 类的 start 和 end 属性是 final 类型的,由于它们引用 Date 对象,在 程序中可以修改所引用 Date 对象的属性。以下程序代码创建了一个 Schedule 对象,接下来 把开学时间和放假时间都改为当前系统时间: Calendar c= Calendar.getInstance(); c.set(2006,9,1); Date start=c.getTime(); c.set(2007,1,25); Date end=c.getTime(); Schedule s=new Schedule(start,end); end.setTime(System.currentTimeMillis()); //修改放假时间 start=s.getStart(); start.setTime(System.currentTimeMillis()); //修改开学时间 为了保证 Schedule 对象的 start 属性和 end 属性值不会被修改,必须为这两个属性使用 保护性拷贝,参见例程 11-11。 PDF created with pdfFactory trial version www.pdffactory.com 例程 11-11 采用了保护性拷贝的 Schedule.java import java.util.Date; public final class Schedule { private final Date start; private final Date end; public Schedule(Date start,Date end){ //不允许放假日期在开学日期的前面 if(start.compareTo(end)>0)throw new IllegalArgumentException(start +" after " +end); this.start=new Date(start.getTime()); //采用保护性拷贝 this.end=new Date(end.getTime()); //采用保护性拷贝 } public Date getStart(){return (Date)start.clone();} //采用保护性拷贝 public Date getEnd(){return (Date)end.clone();} //采用保护性拷贝 } 通过采用保护性拷贝,其他程序无法获得与 Schedule 对象关联的两个 Date 对象的引用, 因此就无法修改这两个 Date 对象的属性值。 假如 Schedule 类中被 final 修饰的属性所属的类是不可变类,就无需 提供保护性拷贝,因为该属性所引用的实例的值永远不会被改变。这进一 步体现了不可变类的优点。 11.3.4 具有实例缓存的不可变类 不可变类的实例的状态不会变化,这样的实例可以安全的被其他与之关联的对象共享, 还可以安全的被多个线程共享。为了节省内存空间,优化程序的性能,应该尽可能的重用不 可变类的实例,避免重复创建具有相同属性值的不可变类的实例。 在 JDK1.5 的基本类库中,对一些不可变类,如 Integer 类作了优化,它具有一个实例缓 存,用来存放程序中经常使用的 Integer 实例。JDK1.5 的 Integer 类新增了一个参数为 int 类 型的静态工厂方法 valueOf(int i),它的处理流程如下: if(在实例缓存中存在取值为 i 的实例) 直接返回这个实例 else{ 用new 语句创建一个取值为 i 的 Integer 实例 把这个实例存放在实例缓存中 返回这个实例 } 在以下程序代码中,分别用 new 语句和 Integer 类的 valueOf(int i)方法来获得 Integer 实 例: Integer a=new Integer(10); Integer b=new Integer(10); Integer c=Integer.valueOf(10); Integer d= Integer.valueOf(10); System.out.println(a==b); //打印 false System.out.println(a==c); //打印 false System.out.println(c==d); //打印 true 以上代码共创建了三个 Integer 对象,参见图 11-4。每个 new 语句都会创建一个新的 Integer 对象。而 Integer.valueOf(10)方法仅仅在第一次被调用时,创建取值为 10 的 Integer 对象,第二次被调用时,直接从实例缓存中获得它。由此可见,在程序中用 valueOf()静态 PDF created with pdfFactory trial version www.pdffactory.com 工厂方法获得 Integer 对象,可以提高 Integer 对象的可重用性。 PDF created with pdfFactory trial version www.pdffactory.com 图 11-4 引用变量与 Integer 对象的引用关系 到底如何实现实例的缓存呢?缓存并没有固定的实现方式,完善的缓存实现不仅要考虑 何时把实例加入缓存,还要考虑何时把不再使用的实例从缓存中及时清除,以保证有效合理 的利用内存空间。一种简单的实现是直接用 Java 集合来作为实例缓存。本章 11.3.2 节的例 程 11-8 的 Gender 类中的 Map 类型的 instancesBySex 属性就是一个实例缓存,它存放了 Gender.MALE 和 Gender.FEMALE 这两个实例的引用。Gender 类的 getInstance()方法从缓存 中寻找 Gender 实例,由于 Gender 类既是不可变类,又是枚举类,因此它的 getInstance()方 法不会创建新的 Gender 实例。 以下例程 11-12 为本章 11.3.3 节介绍的不可变类 Name 增加了一些代码,使它拥有了实 例缓存和相应的静态工厂方法 valueOf()。Name 类的实例缓存中可能会加入大量 Name 对象, 为了防止耗尽内存,在实例缓存中存放的是 Name 对象的软引用(SoftReference)。如果一 个对象仅仅持有软引用,Java 虚拟机会在内存不足的情况下回收它的内存,本章第 11.6 节 对此作了进一步介绍。 例程 11-12 Name.java import java.util.Set; import java.util.HashSet; import java.util.Iterator; import java.lang.ref.*; public class Name { … //实例缓存,存放 Name 对象的软引用 private static final Set> names= new HashSet>(); public static Name valueOf(String firstname, String lastname){ //静态工厂方法 Iterator> it=names.iterator(); while(it.hasNext()){ SoftReference ref=it.next(); //获得软引用 Name name=ref.get(); //获得软引用所引用的 Name 对象 if(name!=null && name.firstname.equals(firstname) && name.lastname.equals(lastname)) return name; } //如果在缓存中不存在 Name 对象,就创建该对象,并把它的软引用加入到实例缓存 Name name=new Name(firstname,lastname); names.add(new SoftReference(name)); return name; 堆区 Integer 对象(10) Integer 对象(10) Integer 对象(10) 变量 a 变量 b 变量 c 变量 d PDF created with pdfFactory trial version www.pdffactory.com } public static void main(String args[]){ Name n1=Name.valueOf("小红","王"); Name n2=Name.valueOf("小红","王"); Name n3=Name.valueOf("小东","张"); System.out.println(n1); System.out.println(n2); System.out.println(n3); System.out.println(n1==n2); //打印 true } } 在程序中,既可以通过 new 语句创建 Name 实例,也可以通过 valueOf()方法创建 Name 实例。在程序的生命周期中,对于程序不需要经常访问的 Name 实例,应该使用 new 语句 创建它,使它能及时结束生命周期;对于经常需要访问的 Name 实例,那就用 valueOf()方法 来获得它,因为该方法能把 Name 实例放到缓存中,使它可以被重用。 从例程 11-12 的 Name 类也可以看出,在有些情况下,一个类可以同 时提供 public 的构造方法和静态工厂方法。用户可以根据实际需要,灵活 的决定到底以何种方式获得类的实例。 另外要注意的是,没有必要为所有的不可变类提供实例缓存。随意创建大量实例缓存, 反而会浪费内存空间,降低程序的运行性能。通常,只有满足以下条件的不可变类才需要实 例缓存: l 不可变类的实例的数量有限。 l 在程序运行过程中,需要频繁访问不可变类的一些特定实例。这些实例拥有与程 序本身同样长的生命周期。 11.3.5 松耦合的系统接口 一个类的静态工厂方法可以返回子类的实例,这一特性有助于创建松耦合的系统接口。 如果系统规模比较简单,静态工厂方法可以直接作为类本身的静态方法;如果系统规模比较 大,根据创建精粒度对象模型的原则,可 以 把 创建特定类的实例的功能专门由一个静态工厂 类来负责。第 1 章的 1.6 节的例程 1-11 的 ShapeFactory 就是一个静态工厂类,它负责构造 Shape 类的实例。ShapeFacory 类有一个静态工厂方法: public static Shape getShape(int type){…} 以上方法声明的返回类型是 Shape 类型,实 际上返回的是 Shape 子类的实例。对于 Shape 类的使用者 Panel 类,只用访问 Shape 类,而不必访问它的子类: //获得一个 Circle 实例 Shape shape=ShapeFactory.getInstance(ShapeFactory.SHAPE_TYPE_CIRCLE); 在分层的软件系统中,业务逻辑层向客户层提供服务,静态工厂类可以进一步削弱这两 个层之间的松耦合关系。例如在以下图 11-5 中,业务逻辑层向客户层提供 ServiceIFC 接口, 在该接口中声明了所提供的各种服务,它有三个实现类 ServiceImpl1、ServiceImpl2 和 ServiceImpl3 类。ServiceFactory 静态工厂类负责构造 ServiceIFC 的实现类的实例,它的定义 如下。 public ServiceFactory{ private static final String serviceImpl; PDF created with pdfFactory trial version www.pdffactory.com static{ //读取配置信息,根据配置信息设置服务实现类的类型,假定为 ServiceImpl1 serviceImpl="ServiceImpl1"; } public static ServiceIFC getInstance(){ Class.forName(serviceImpl).newInstance(); } } 图 11-5 静态工厂模型 当客户层需要获得业务逻辑层的服务时,先从静态工厂类 ServiceFactory 中获得 ServiceIFC 接口的实现类的实例,然后通过接口访问服务: ServiceIFC service=ServiceFactory.getInstance(); service.service1(); 在客户层只会访问 ServiceIFC 接口,至于业务逻辑层到底采用哪个实现类的实例提供 服务,这对客户层是透明的。 11.4 垃圾回收 当对象被创建,就会在 Java 虚拟机的堆区中拥有一块内存,在 Java 虚拟机的生命周期 中,Java 程序会陆陆续续的创建无数对象,假如所有的对象都永久占有内存,那么内存有可 能很快被消耗光,最后引发内存空间不足的错误。因此必须采取一种措施来及时回收哪些无 用对象的内存,以保证内存可以被重复利用。 在一些传统的编程语言,如 C 语言中,回收内存的任务是由程序本身负责的。程序可以 显式的为自己的变量分配一块内存空间,当这些变量不再有用,程 序 必须显式的释放变量所 占用的内存。把直接操纵内存的权利赋予给程序,尽管给程序带来了很多灵活性,但是也会 导致以下弊端: l 程序员有可能因为粗心大意,忘记及时释放无用变量的内存,从而影响程序的健壮 性。 l 程序员有可能错误的释放核心类库所占用的内存,导致系统崩溃。 在 Java 语言中,内存回收的任务由 Java 虚拟机来担当,而不是由 Java 程序来负责。在 PDF created with pdfFactory trial version www.pdffactory.com 程序的运行时环境中,Java 虚拟机提供了一个系统级的垃圾回收器线程,它负责自动回收那 些无用对象所占用的内存,这种内存回收的过程被称为垃圾回收(Garbage Collection)。 垃圾回收有以下优点: l 把程序员从复杂的内存追踪、监测和释放等工作中解放出来,减轻程序员进行内存 管理的负担。 l 防止系统内存被非法释放,从而使系统更加健壮和稳定。 垃圾回收具有以下特点: l 只有当对象不再被程序中的任何引用变量引用,它的内存才可能被回收。 l 程序无法迫使垃圾回收器立即执行垃圾回收操作。 l 当垃圾回收器将要回收无用对象的内存时,先调用该对象的 finalize()方法,该方法 有可能使对象复活,导致垃圾回收器取消回收该对象的内存。 11.4.1 对象的可触及性 在 Java 虚拟机的垃圾回收器看来,堆区中的每个对象都可能处于以下三个状态之一: l 可触及状态:当一个对象(假定为 Sample 对象)被创建后,只要程序中还有引用 变量引用它,那么它就始终处于可触及状态。 l 可复活状态:当程序不再有任何引用变量引用 Sample 对象,那么它就进入可复活 状态。在这个状态中,垃圾回收器会准备释放它占用的内存,在释放之前,会调用 它以及其他处于可复活状态的对象的 finalize()方法,这些 finalize()方法有可能使 Sample 对象重新转到可触及状态。 l 不可触及状态:当 Java 虚拟机执行完所有可复活对象的 finalize()方法后,假如这 些方法都没有使 Sample 对象转到可触及状态,那么 Sample 对象就进入不可触及状 态。只有当对象处于不可触及状态,垃圾回收器才会真正回收它的内存。 图 11-6 显示了对象的状态转换图。 图 11-6 对象的状态转换图 在本书第 1 章的 1.3.1 节曾经提到,对象的状态是指某一瞬间其所有 属性的取值。本节谈到的对象的状态有不同的含义,按照是否可以被垃圾 回收来划分对象的状态。 以下 method()方法先后创建了两个 Integer 对象: public static void method(){ Integer a1=new Integer(10); //① Integer a2=new Integer(20); //② a1=a2; //③ 可触及状态 可复活状态 不可触及状态 new 语句 生命周期的开始 生命周期的终止 回收内存 不再被引用 当前对象或其他对象的 finalize()方法 当前对象或其他对象的 finalize()方法 PDF created with pdfFactory trial version www.pdffactory.com } public static void main(String args[]){ method(); System.out.println("End"); } 当程序执行完第③行时,取值为 10 的 Integer 对象不再被任何变量引用,因此转到可复 活状态,取值为 20 的 Integer 对象处于可触及状态,它被变量 a1 和 a2 引用,参见图 11-7。 图 11-7 两个Integer 对象的状态 当程序退出 method()方法并返回到 main()方法,在 method()方法中定义的局部变量 a1 和 a2 都结束生命周期,堆区中取值为 20 的 Integer 对象也将转到可复活状态。 如果从对象 A 到对象 B 存在关联关系,实际上就是指对象 A 的某个实例变量引用对象 B。第 6 章的 6.7.5 节(区分对象的属性与继承)介绍了 Book 类与 Category 类的单向关联, 以及 Category 类的自身双向关联,参见 6.7.5 节的图 6-10。在 6.7.5 节的例程 6-5 中, CategoryTester 类的 create()方法创建了三个 Category 对象和一个 Book 对象,当 create()方法 执行完毕,退回到 main()方法,Book 对象被 main()方法中的 mathBook 局部变量引用,参见 图 11-8。 图 11-8 Book 对象、Category 对象以及 mathBook 变量之间的关系 从图 11-8 可以看出,尽管在程序中仅仅持有 Book 对象的引用,但是其他三个 Category 对象也都是可触及的。以下程序代码演示如何从 Book 对象依次导航到其他三个 Category 对 象: //由 Book 对象导航到取值为“Math”的 Category 对象 Category categoryMath=mathBook.getCategory(); //由取值为“Math”的 Category 对象导航到取值为“Science”的 Category 对象 Category categoryScience=categoryMath.getParentCategory(); //由取值为“Science”的 Category 对象导航到取值为“Computer”的 Category 对象 Category categoryComputer=(Category)categoryScience.getChildCategories().iterator().next(); 11.4.2 垃圾回收的时间 当一个对象处于可复活状态,垃圾回收线程何时执行它的 finalize()方法,何时使它转到 不可触及状态,何时回收它的内存,这对于程序来说都是透明的。程序只能决定一个对象何 时不再被任何引用变量引用,使得它成为可以被回收的垃圾。这就像每个居民只要把无用的 堆区 堆区 Integer 对象(10) 可复活状态 Integer 对象(20) 可触及状态 变量 a1 变量 a2 Category 对象(Science) Category 对象(Math) Book 对象 Category 对象(Computer) mathBook 变量 PDF created with pdfFactory trial version www.pdffactory.com 物品(相当于无用的对象)放在指定的地方,清洁工人就会来把它收拾走,但是,垃圾什么 时候被收走,居民是不知道的,也无须对此了解。 站在程序的角度,如果一个对象不处于可触及状态,就可以称它为无用对象,程 序 不 会 持 有 无 用对象的引用,不 会再使用它,这样的对象可以被垃圾回收器回收。站在程序的角度, 一个对象的生命周期从被创建开始,到不再被任何变量引用(即变为无用对象)结束。在本 书其他章节提到对象的生命周期,如果未作特别说明,都是沿用这个含义。 垃圾回收器作为低优先级线程独立运行。在 任何时候,程 序 都 无 法 迫 使 垃圾回收器立即 执行垃圾回收操作。在程序中可以调用 System.gc()或者 Runtime.gc()方法提示垃圾回收器尽 快执行垃圾回收操作,但是这也不能保证调用完该方法后,垃圾回收线程就立即执行回收操 作,而且不能保证垃圾回收线程一定会执行这一操作。这就像当小区内的垃圾成堆时,居民 无法立即把环保局的清洁工人招来,令其马上清除垃圾。居民所能做的是给环保局打电话, 催促他们尽快来处理垃圾。这种做法仅仅提高了清洁工人尽快来处理垃圾的可能性,但仍然 存在清洁工人过了很久才来或者永远不来清除垃圾的可能性。 11.4.3 对象的 finalize()方法简介 当垃圾回收器将要释放无用对象的内存时,先调用该对象的 finalize()方法。如果在程序 终止之前垃圾回收器始终没有执行垃圾回收操作,那么垃圾回收器将始终不会调用无用对象 的 finalize()方法。在 Java 的 Object 祖先类中提供了 protected 类型的 finalize()方法,因此任 何 Java 类都可以覆盖 finalize()方法,在这个方法中进行释放对象所占的相关资源的操作。 Java 虚拟机的垃圾回收操作对程序完全是透明的,因此程序无法预料某个无用对象的 finalize()方法何时被调用。另外,除非垃圾回收器认为程序需要额外的内存,否则它不会试 图释放无用对象的内存。换句话说,以下情况是完全可能的:一个程序只占用了少量内存, 没有造成严重的内存需求,于是垃圾回收器没有释放那些无用对象的内存,因此这些对象的 finalize()方法还没有被调用,程序就终止了。 程序即使显式调用 System.gc()或 Runtime.gc()方法,也不能保证垃圾回收操作一定执行, 因此不能保证无用对象的 finalize()方法一定被调用。 11.4.4 对象的 finalize()方法的特点 对象的 finalize()方法具有以下特点: l 垃圾回收器是否会执行该方法、以及何时执行该方法,都是不确定的。 l finalize()方法有可能使对象复活,使它恢复到可触及状态。 l 垃圾回收器在执行 finalize()方法时,如果出现异常,垃圾回收器不会报告异常,程 序继续正常运行。 下面结合一个具体的例子来解释 finalize()方法的特点。以下例程 11-13 的 Ghost 类是一 个带实例缓存的不可变类,它的 finalize()方法能够把当前实例重新加入到实例缓存 ghosts 中。 例程 11-13 Ghost.java import java.util.Map; import java.util.HashMap; public class Ghost { private static final Map ghosts=new HashMap(); private final String name; public Ghost(String name) { this.name=name; } PDF created with pdfFactory trial version www.pdffactory.com public String getName(){return name;} public static Ghost getInstance(String name){ Ghost ghost =ghosts.get(name); if (ghost == null) { ghost=new Ghost(name); ghosts.put(name,ghost); } return ghost; } public static void removeInstance(String name){ ghosts.remove(name); } protected void finalize()throws Throwable{ ghosts.put(name,this); System.out.println("execute finalize"); //throw new Exception("Just Test"); } public static void main(String args[])throws Exception{ Ghost ghost=Ghost.getInstance("IAmBack"); //① System.out.println(ghost); //② String name=ghost.getName(); //③ ghost=null; //④ Ghost.removeInstance(name); //⑤ System.gc(); //⑥ //把 CPU 让给垃圾回收线程 Thread.sleep(3000); //⑦ ghost=Ghost.getInstance("IAmBack"); //⑧ System.out.println(ghost); //⑨ } } 运行以上 Ghost 类的 main()方法,一种可能的打印结果为: Ghost@3179c3 execute finalize Ghost@3179c3 以上程序创建了三个对象:一个 Ghost 对象、一个常量字符串“IAmBack”,以及一个 HashMap 对象。当程序执行完 main()方法的第③行,内存中引用变量与对象之间的关系如 图 11-9 所示。 PDF created with pdfFactory trial version www.pdffactory.com 图 11-9 Ghost 对象与其他对象以及引用变量的关系 当执行完第④行,ghost 变量被置为 null,此时 Ghost 对象依然被 ghosts 属性间接引用, 因此仍然处于可触及状态。当执行完第⑤行,Ghost 对象的引用从 HashMap 对象中删除, Ghost 对象不再被程序引用,此时进入可复活状态,即变为无用对象。 第⑥行调用 System.gc()方法,它能提高垃圾回收器尽快执行垃圾回收操作的可能性。 假如垃圾回收器线程此刻获得了 CPU,它将调用 Ghost 对象的 finalize()方法,该方法把 Ghost 对象的引用又加入到 HashMap 对象中,Ghost 对象又回到可触及状态,垃圾回收器放弃回收 它的内存。执行完第⑧行,ghost 变量又引用这个 Ghost 对象。 假如对 finalize()作一些修改,使它抛出一个异常: protected void finalize()throws Throwable{ ghosts.put(name,this); System.out.println("execute finalize"); throw new Exception("Just Test"); } 程序的打印结果不变,由此可见,当垃圾回收器执行 finalize()方法时,如果出现异常, 垃圾回收器不会报告异常,也不会导致程序异常中断。 假如在程序运行中,垃圾回收器始终没有执行垃圾回收操作,那么Ghost 对象的 finalize() 方法就不会被调用。读者不妨把第⑥行的 System.gc()和第⑦行的 Thread.sleep(3000)方法注 释掉,这样更加可能导致 finalize()方法不会被调用,此时程序的一种可能的打印结果为: Ghost@3179c3 Ghost@310d42 从以上打印结果可以看出,由于 Ghost 对象的 finalize()方法没有被执行,因此这个 Ghost 对象在程序运行期间始终没有被复活。当 程 序 第 二 次 调用 Ghost.getInstance("IAmBack")方法 时,该方法创建了一个新的 Ghost 对象。 值得注意的是,以上例子仅仅用于演示 finalize()方法的特性,在实际应用中,不提倡用 finalize()方法来复活对象。可以把处于可触及状态的对象比作活在阳间的人,把不处于这个 状态的对象(无用对象)比作到了阴间的人。程序所能看见和使用的是阳间的人,假如阎王 经常悄悄的让几个阴间的人复活,使他们在程序毫不知情的情况下溜回阳间,这只会扰乱程 序的正常执行流程。 PDF created with pdfFactory trial version www.pdffactory.com 11.4.5 比较 finalize()方法和 finally 代码块 在 Object 类中提供了 finalize()方法,它的初衷是用于在对象被垃圾回收之前,释放所 占用的相关资源,这和 try-catch-finally 语句的 finally 代码块的用途比较相似。但由于垃圾 回收器是否会执行 finalize()方法、以及何时执行该方法,都是不确定的,因此在程序中不能 用 finalize()方法来完成同时具有以下两个特点的释放资源的操作: l 必须执行。 l 必须在某个确定的时刻执行。 具有以上特点的操作更适合于放在 finally 代码块中。此外,可以在类中专门提供一个 用于释放资源的公共方法,最典型的就是 java.io.InputStream 和 java.io.OutputStream 类的 close()方法,它们用于关闭输入流或输出流。当程序中使用了一个输入流,在结束使用前应 该确保关闭输入流: InputStream in; try{ InputStream in=new FileInputStream("a.txt"); … }catch(IOException e){ … }finally{ try{in.close();}catch(IOException e){…} } 在多数情况下,应该避免使用 finalize()方法,因为它会导致程序运行结果的不确定性。 在某些情况下,finalize()方法可用来充当第二层安全保护网,当用户忘记显式释放相关资源, finalize()方法可以完成这一收尾工作,尽管 finalize()方法不一定会被执行,但是有可能会释 放资源总比永远不会释放资源更安全。 可以用自动洗衣机的关机功能来解释 finalize()方法的用途,自 动 洗衣机向用户提供了专 门的关机按钮,这相当于 AutoWasher 类的 close()方法,假如用户忘记关机,相当于忘记调 用 AutoWasher 对象的 close()方法,那么自动洗衣机会在洗衣机停止工作后的 1 个小时内自 动关机,这相当于调用 finalize()方法。当 然 ,这个例子不是太贴切,因为如果用户忘记关机, 洗衣机的自动关机操作总是会被执行。 11.5 清除过期的对象引用 在程序中,如果不需要再用到一个对象,就应该及时清除对这个对象的引用,使得它变 为无用对象,它的内存可以被回收。 程序通过控制引用变量的生命周期,从而间接的控制对象的生命周期。例如把一个变量 定义为 final 类型的静态变量: private static final FEMALE=new Gender("女"); 以上 Gender 对象的生命周期取决于 FEMALE 变量的生命周期,而 FEMALE 静态变量 的生命周期取决于代表 Gender 类的 Class 对象的生命周期,在 Gender 类不会被卸载的情况 下,它的 Class 对象会常驻内存,直到程序运行结束,因此 Gender 对象一旦被创建,也会 常驻内存,直到程序运行结束。 再例如以下局部变量 sb 和 s 分别引用一个 StringBuffer 对象和一个 String 对象: public void method(){ PDF created with pdfFactory trial version www.pdffactory.com StringBuffer sb=new StringBuffer("Hello"); sb.append(" World"); String s=sb.toString(); System.out.println(s); } 局部变量的生命周期很短暂,当方法执行完毕,局部变量就结束生命周期。因此当 method()方法执行完毕,StringBuffer 对象和 String 对象就会结束生命周期。在多数情况下, 把引用变量显式的置为 null 是没有必要的,属于多此一举的代码: public void method(){ StringBuffer sb=new StringBuffer("Hello"); sb.append(" World"); String s=sb.toString(); System.out.println(s); sb=null; //没有必要 s=null; //没有必要 } 不过,在某些情况下,当程序通过数组来使用内存,此时必须十分小心的清除过期的对 象引用,否则会导致潜在的内存泄漏的错误。例如以下例程 11-14(Stack.java)是堆栈的一 种简单实现方式,它的对象数组 elements 用来存放对象,堆栈的容量可以自动增长。 例程 11-14 Stack.java import java.util.EmptyStackException; public class Stack { private Object[] elements; //存放对象 private int size=0; private int capacityIncrement=10; //堆栈的容量增长的步长 public Stack(int initialCapacity,int capacityIncrement) { this(initialCapacity); this.capacityIncrement=capacityIncrement; } public Stack(int initialCapacity) { elements=new Object[initialCapacity]; } public void push(Object object){ ensureCapacity(); elements[size++]=object; } public Object pop(){ if(size==0) throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity(){ //增加堆栈的容量 if(elements.length==size){ Object[] oldElements=elements; elements=new Object[elements.length+capacityIncrement]; PDF created with pdfFactory trial version www.pdffactory.com System.arraycopy(oldElements,0,elements,0,size); } } } 以上程序看上去是可行的,可 以 正 常 的 完成对象的入栈和出栈操作。下 面的程序代码先 向堆栈压入 1000 个 Integer 对象,然后又一一取出它们: Stack stack=new Stack(1000); for(int a=0;a<1000;a++) stack.push(new Integer(a)); for(int a=0;a<1000;a++) System.out.println(stack.pop()); 当一个 Integer 对象从堆栈中取出后,假如程序中不再有其他变量引用它,这个 Integer 对象应该变为无用对象,但是由于 Stack 类的 pop()方法没有及时清除对这个 Integer 对象的 引用,导致这个 Integer 对象不能被垃圾回收。 为了避免这一问题,应该对 pop()方法作如下修改: public Object pop(){ if(size==0) throw new EmptyStackException(); Object object=elements[--size]; elements[size]=null; //清除过期的对象引用 return object; } 11.6 对象的强、软、弱和虚引用 在 JDK1.2 以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个 对象。也就是说,只有对象处于可触及状态,程序才能使用它。这就像在日常生活中,从商 店购买了某样物品后,如果有用,就一直保留它,否则就把它扔到垃圾箱,由清洁工人收走。 一般说来,如果物品已经被扔到垃圾箱,想再把它捡回来使用就不可能了。 但有时候情况并不这么简单,你可能会遇到类似鸡肋一样的物品,食之无味,弃之可惜。 这种物品现在已经无用了,保留它会占空间,但是立刻扔掉它也不划算,因为也许将来还会 派用场。对于这样的可有可无的物品,一种折衷的处理办法是:如果家里空间足够,就先把 它保留在家里,如果家里空间不够,即使把家里所有的垃圾清除,还是无法容纳那些必不可 少的生活用品,那么再扔掉这些可有可无的物品。 从 JDK1.2 版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象 的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。 1.强引用 本章前文介绍的引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强 引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用 的对象来解决内存不足问题。 2.软引用(SoftReference) 如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃 圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器 PDF created with pdfFactory trial version www.pdffactory.com 没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被 垃圾回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。 3.弱引用(WeakReference) 如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱 引用与软引用的区别 在于:只具有弱引用的对象拥有更短暂的生命周期。在 垃圾回收器线程扫描它所管辖的内存 区域的过程中,一 旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它 的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具 有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被 垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 4.虚引用(PhantomReference) “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象 的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可 能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在 于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象 时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的 引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是 否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用 的对象的内存被回收之前采取必要的行动。 在本书中,“引用”既可以作为动词,也可以作为名词,读者应该根 据上下文来区分“引用”的含义。 在 java.lang.ref 包 中提供了三个类:SoftReference 类 、 WeakReference 类和 PhantomReference 类,它们分别代表软引用、弱引用和虚引用。ReferenceQueue 类表示引用 队列,它可以和这三种引用类联合使用,以便跟踪 Java 虚拟机回收所引用的对象的活动。 以下程序创建了一个 String 对象、ReferenceQueue 对象和 WeakReference 对象: //创建一个强引用 String str = new String("hello"); //创建引用队列, 为范型标记,表明队列中存放 String 对象的引用 ReferenceQueue rq = new ReferenceQueue(); //创建一个弱引用,它引用“hello”对象,并且与 rq 引用队列关联 //为范型标记,表明 WeakReference 会弱引用 String 对象 WeakReference wf = new WeakReference(str, rq); 以上程序代码执行完毕,内存中引用与对象的关系如图 11-10 所示。 PDF created with pdfFactory trial version www.pdffactory.com 图 11-10 “hello”对象同时具有强引用和弱引用 在图 11-10 中,带实线的箭头表示强引用,带虚线的箭头表示弱引用。从图中可以看出, 此时“hello”对象被 str 强引用,并且被一个 WeakReference 对象弱引用,因此“hello”对 象不会被垃圾回收。 在以下程序代码中,把引用“hello”对象的 str 变量置为 null,然后再通过 WeakReference 弱引用的 get()方法获得“hello”对象的引用: String str = new String("hello"); //① ReferenceQueue rq = new ReferenceQueue(); //② WeakReference wf = new WeakReference(str, rq); //③ str=null; //④取消“hello”对象的强引用 String str1=wf.get(); //⑤假如“hello”对象没有被回收,str1 引用“hello”对象 //假如“hello”对象没有被回收,rq.poll()返回 null Reference ref=rq.poll(); //⑥ 执行完以上第④行后,内存中引用与对象的关系如图 11-11 所示,此时“hello”对象仅 仅具有弱引用,因此它有可能被垃圾回收。假如它还没有被垃圾回收,那么接下来在第⑤行 执行 wf.get()方法会返回“hello”对象的引用,并且使得这个对象被 str1 强引用。再接下来 在第⑥行执行 rq.poll()方法会返回 null,因为此时引用队列中没有任何引用。ReferenceQueue 的 poll()方法用于返回队列中的引用,如果没有则返回 null。 图 11-11 “hello”对象只具有弱引用 在以下程序代码中,执行完第④行后,“hello”对象仅仅具有弱引用。接下来两次调用 堆区 “hello”对象 WeakReference 对象 ReferenceQueue 对象 str wf rq 堆区 “hello”对象 WeakReference 对象 ReferenceQueue 对象 str=null wf rq PDF created with pdfFactory trial version www.pdffactory.com System.gc()方法,催促垃圾回收器工作,从而提高“hello”对象被回收的可能性。假如“ hello” 对象被回收,那么 WeakReference 对象的引用被加入到 ReferenceQueue 中,接下来 wf.get() 方法返回 null,并且 rq.poll()方法返回 WeakReference 对象的引用。图 11-12 显示了执行完第 ⑧行后内存中引用与对象的关系。 String str = new String("hello"); //① ReferenceQueue rq = new ReferenceQueue(); //② WeakReference wf = new WeakReference(str, rq); //③ str=null; //④ //两次催促垃圾回收器工作,提高“hello”对象被回收的可能性 System.gc(); //⑤ System.gc(); //⑥ String str1=wf.get(); //⑦ 假如“hello”对象被回收,str1 为 null Reference ref=rq.poll(); //⑧ 图 11-12 “hello”对象被垃圾回收,弱引用被加入到引用队列 在以下例程 11-15 的 References 类中,依次创建了 10 个软引用、10 个弱引用和 10 个虚 引用,它们各自引用一个 Grocery 对象。从程序运行时的打印结果可以看出,虚引用形同虚 设,它所引用的对象随时可能被垃圾回收,具有弱引用的对象拥有稍微长的生命周期,当垃 圾回收器执行回收操作时,有可能被垃圾回收,具有软引用的对象拥有较长的生命周期,但 在 Java 虚拟机认为内存不足的情况下,也会被垃圾回收。 例程 11-15 References.java import java.lang.ref.*; import java.util.*; class Grocery{ private static final int SIZE = 10000; //属性 d 使得每个 Grocery 对象占用较多内存,有 80K 左右 private double[] d = new double[SIZE]; private String id; public Grocery(String id) { this.id = id; } public String toString() { return id; } public void finalize() { System.out.println("Finalizing " + id); } } public class References { 堆区 “hello”对象 WeakReference 对象 ReferenceQueue 对象 str=null str1=null wf rq ref PDF created with pdfFactory trial version www.pdffactory.com private static ReferenceQueue rq = new ReferenceQueue(); public static void checkQueue() { Reference inq = rq.poll(); //从队列中取出一个引用 if(inq != null) System.out.println("In queue: "+inq+" : "+inq.get()); } public static void main(String[] args) { final int size=10; //创建 10 个 Grocery 对象以及 10 个软引用 Set> sa = new HashSet>(); for(int i = 0; i < size; i++) { SoftReference ref= new SoftReference(new Grocery("Soft " + i), rq); System.out.println("Just created: " +ref.get()); sa.add(ref); } System.gc(); checkQueue(); //创建 10 个 Grocery 对象以及 10 个弱引用 Set> wa = new HashSet>(); for(int i = 0; i < size; i++) { WeakReference ref= new WeakReference(new Grocery("Weak " + i), rq); System.out.println("Just created: " +ref.get()); wa.add(ref); } System.gc(); checkQueue(); //创建 10 个 Grocery 对象以及 10 个虚引用 Set> pa = new HashSet>(); for(int i = 0; i < size; i++) { PhantomReferenceref = new PhantomReference(new Grocery("Phantom " + i), rq); System.out.println("Just created: " +ref.get()); pa.add(ref); } System.gc(); checkQueue(); } } 在 Java 集合中有一种特殊的 Map 类型:WeakHashMap,在这种 Map 中存放了键对象 的弱引用,当一个键对象被垃圾回收,那么相应的值对象的引用会从 Map 中删除。 WeakHashMap 能够节约存储空间,可用来缓存那些非必须存在的数据。关于 Map 接口的一 般用法,可参见本书第 15 章的 15.4 节(Map)。 以下例程 11-16 的 MapCache 类的 main()方法创建了一个 WeakHashMap 对象,它存放 了一组 Key 对象的弱引用,此外 main()方法还创建了一个数组对象,它存放了部分 Key 对 象的强引用。 PDF created with pdfFactory trial version www.pdffactory.com 例程 11-16 MapCache.java import java.util.*; import java.lang.ref.*; class Key { String id; public Key(String id) { this.id = id; } public String toString() { return id; } public int hashCode() { return id.hashCode(); } public boolean equals(Object r) { return (r instanceof Key) && id.equals(((Key)r).id); } public void finalize() { System.out.println("Finalizing Key "+ id); } } class Value { String id; public Value(String id) { this.id = id; } public String toString() { return id; } public void finalize() { System.out.println("Finalizing Value "+id); } } public class MapCache { public static void main(String[] args) throws Exception{ int size = 1000; // 或者从命令行获得 size 的大小 if(args.length > 0)size = Integer.parseInt(args[0]); Key[] keys = new Key[size]; //存放键对象的强引用 WeakHashMap whm = new WeakHashMap(); for(int i = 0; i < size; i++) { Key k = new Key(Integer.toString(i)); Value v = new Value(Integer.toString(i)); if(i % 3 == 0) keys[i] = k; //使 Key 对象持有强引用 whm.put(k, v); //使 Key 对象持有弱引用 } //催促垃圾回收器工作 System.gc(); //把 CPU 让给垃圾回收器线程 Thread.sleep(8000); } } 以上程序的部分打印结果如下: PDF created with pdfFactory trial version www.pdffactory.com Finalizing Key 998 Finalizing Key 997 Finalizing Key 995 Finalizing Key 994 Finalizing Key 992 Finalizing Key 991 Finalizing Key 989 Finalizing Key 988 Finalizing Key 986 Finalizing Key 985 Finalizing Key 983 从打印结果可以看出,当执行 System.gc()方法后,垃圾回收器只会回收那些仅仅持有 弱引用的 Key 对象。id 可以被 3 整数的 Key 对象持有强引用,因此不会被回收。 11.7 小节 对象是程序所处理数据的最主要的载体,数据以实例变量的形式存放在对象中。每个对 象在生命周期的开始阶段,Java 虚拟机都需要为它分配内存,然后对它的实例变量进行初始 化。用 new 语句创建类的对象时,Java 虚拟机会从最上层的父类开始,依次执行各个父类 以及当前类的构造方法,从而保证来自于对象本身以及从父类中继承的实例变量都被正确的 初始化。 当一个对象不被程序的任何引用变量引用,对象就变成无用对象,它占用的内存就可以 被垃圾回收器回收。每个对象都会占用一定的内存,而内存是有限的资源,为了合理的利用 内存,在决定对象的生命周期时,应该遵循以下原则: l 重用已经存在的对象,尤其是需要经常访问的不可变类的对象,在这种情况下,程 序 可 通 过类的静态工厂方法来获得已经存在的对象,而不是通过 new 语句来创建 新的对象。 l 当程序不需要再使用一个对象,应该及时清除对这个对象的引用,使它的内存可以 被回收。 在垃圾回收器的眼里,对象的生命周期开始于在内存中拥有立足之地,结束于它的内存 被回收。由于无用对象何时被垃圾回收器回收,这对程序是透明的,因此在 Java 程序的眼 里,对象的生命周期开始于在内存中拥有立足之地,并且能通过引用变量引用它,结束于没 有任何引用变量引用它。 从 JDK1.2 版本开始,对象的引用可分为 4 个级别:强引用、软引用、弱 引用和虚引用。 如果一个对象不允许被垃圾回收,则应该持有强引用;如果一个对象可以被垃圾回收,但是 在没有被回收之前仍然可以使用,则应该持有软引用或弱引用。一个仅持有虚引用的对象在 任何时候都可能被垃圾回收,虚引用与引用队列联合使用,可用来跟踪垃圾回收的过程。 PDF created with pdfFactory trial version www.pdffactory.com
还剩38页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

shijie15

贡献于2012-07-19

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf