您好,欢迎来到欧得旅游网。
搜索
您的当前位置:首页java对象的生命周期、

java对象的生命周期、

来源:欧得旅游网


第11章 对象的生命周期

在Java虚拟机管辖的运行时数据区,最活跃的就是位于堆区的生生息息的对象。在Java虚拟机的生命周期中,一个个对象被陆续地创建,又一个个被销毁。在对象生命周期的开始阶段,需要为对象分配内存,并且初始化它的实例变量。当程序不再使用某个对象时,它就会结束生命周期,它的内存可以被Java虚拟机的垃圾回收器回收。

11.1 创建对象的方式

在Java程序中,对象可以被显式地或者隐含地创建。创建一个对象就是指构造一个类的实例,前提条件是这个类已经被初始化,第10章(类的生命周期)已经对此做了详细介绍。

有4种显式地创建对象的方式:

󰁺 用new语句创建对象,这是最常用的创建对象的方式。

󰁺 运用反射手段,调用java.lang.Class或者java.lang.reflect.Constructor类的

newInstance()实例方法。 󰁺 调用对象的clone()方法。

󰁺 运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法,

参见第16章的16.12节(对象的序列化与反序列化)。

例程11-1(Customer.java)演示了用前面3种方式创建对象的过程。

例程11-1 Customer.java

public class Customer implements Cloneable{ private String name; private int age;

public Customer(){ this(\"unknown\

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;

Java面向对象编程 final Customer other=(Customer)o; if(this.name.equals(other.name) && this.age==other.age) return true; else return false; } public String toString(){return \"name=\"+name+\ 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\ 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()方法具有以下特点: 󰁺 声明为protected类型,Object的子类如果希望对外公开clone()方法,就必须扩大访问权限,例如在以上Customer类中,把clone()方法的访问级别改为public。 󰁺 如果Java类没有实现Cloneable接口,clone()方法会抛出CloneNotSupportedException异常。Object的子类如果允许客户程序调用其306 Java Object-Oriented Programming 第11章 对象的生命周期 clone()方法,那么这个类必须实现Cloneable接口。 󰁺 Object类在clone()方法的实现中会创建一个复制的对象,这个对象与原来的对象具有不同的内存地址,不过它们的属性值相同。在本例中,c3由c2克隆而成,它们的内存地址不一样,但属性值相同。 除了以上4种显式地创建对象的方式以外,在程序中还可以隐含地创建对象,包括以下几种情况: 󰁺 对于java命令中的每个命令行参数,Java虚拟机都会创建相应的String对象,并把它们组织到一个String数组中,再把该数组作为参数传给程序入口main(String args[])方法。 󰁺 程序代码中的String类型的直接数对应一个String对象,例如: String s1=\"Hello\"; String s2=\"Hello\"; String s3=new String(\"Hello\"); System.out.println(s1==s2); System.out.println(s1==s3); //s2和s1引用同一个String对象 //打印true //打印false 执行完以上程序,内存中实际上只有两个String对象,一个是直接数,由Java虚拟机隐含地创建,还有一个通过new语句显式地创建。 󰁺 字符串操作符“+”的运算结果为一个新的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 󰁺 当Java虚拟机加载一个类时,会隐含地创建描述这个类的Class实例,参见第10章的10.2.1节(类的加载)。 不管采取哪种方式创建对象,Java虚拟机创建一个对象都包含以下步骤。 (1)给对象分配内存。 (2)将对象的实例变量自动初始化为其变量类型的默认值。 (3)初始化对象,给实例变量赋予正确的初始值。 对于以上第三个步骤,Java虚拟机可采用3种方式来初始化对象,到底采用何种初始化方式取决于创建对象的方式。 󰁺 如果对象是通过clone()方法创建的,那么Java虚拟机把原来被克隆对象的实例变量的值拷贝到新对象中。 󰁺 如果对象是通过ObjectInputStream类的readObject()方法创建的,那么Java虚拟机通过从输入流中读入的序列化数据来初始化那些非暂时性(non-transient)的实例变量。 󰁺 在其他情况下,如果实例变量在声明时被显式初始化,那么就把初始化值赋给实例变量,接着再执行构造方法。这是最常见的初始化对象的方式。 Java Object-Oriented Programming 307 Java面向对象编程 11.2 构造方法 从上一节可以看出,在多数情况下,初始化一个对象的最终步骤是去调用这个对象的构造方法。构造方法负责对象的初始化工作,为实例变量赋予合适的初始值。构造方法必须满足以下语法规则: 󰁺 方法名必须与类名相同。 󰁺 不要声明返回类型。 󰁺 不能被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实例时,调用的是308 Java Object-Oriented Programming 第11章 对象的生命周期 Mystery类的默认构造方法,而不是以上有void返回类型的Mystery()方法。关于默认构造方法的概念,参见本章第11.2.2节(默认构造方法)。 11.2.1 重载构造方法 当通过new语句创建一个对象时,在不同的条件下,对象可能会有不同的初始化行为。例如对于公司新来的一个雇员,在一开始的时候,有可能他的姓名和年龄是未知的,也有可能仅仅他的姓名是已知的,也有可能姓名和年龄都是已知的。如果姓名是未知的,就暂且把姓名设为“无名氏”,如果年龄是未知的,就暂且把年龄设为-1。 可通过重载构造方法来表达对象的多种初始化行为。例程11-2的Employee类的构造方法有3种重载形式。在一个类的多个构造方法中,可能会出现一些重复操作。为了提高代码的可重用性,Java语言允许在一个构造方法中,用this语句来调用另一个构造方法。 例程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;} } 以下程序分别通过3个构造方法创建了3个Employee对象。 Employee zhangsan=new Employee(\"张三\Employee lisi=new Employee(\"李四\"); Employee someone=new Employee(); 在Employee(String name)构造方法中,this(name,-1)语句用于调用Employee(String name,int age)构造方法。在Employee()构造方法中,this(\"无名氏\")语句用于调用Employee(String name)构造方法。 Java Object-Oriented Programming 309 Java面向对象编程 用this语句来调用其他构造方法时,必须遵守以下语法规则。 󰁺 假如在一个构造方法中使用了this语句,那么它必须作为构造方法的第一条语句(不考虑注释语句)。以下构造方法是非法的: public Employee(){ String name=\"无名氏\"; this(name); //编译错误,this语句必须作为第一条语句 } 󰁺 只能在一个构造方法中用this语句来调用类的其他构造方法,而不能在实例方法中用this语句来调用类的其他构造方法。 󰁺 只能用this语句来调用其他构造方法,而不能通过方法名来直接调用构造方法。以下对构造方法的调用方式是非法的: public Employee() { String name= \"无名氏\"; Employee(name); //编译错误,不能通过方法名来直接调用构造方法 } 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(); //合法 310 Java Object-Oriented Programming 第11章 对象的生命周期 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)构造方法 super(\"Something is error\"); } public MyException(String msg){ //调用Exception父类的Exception(String msg)构造方法 super(msg); } } 用super语句来调用父类的构造方法时,必须遵守以下语法规则。 󰁺 在子类的构造方法中,不能直接通过父类方法名调用父类的构造方法,而是要使用super语句,以下代码是非法的: public MyException(String msg){ Exception(msg); //编译错误 } 󰁺 假如在子类的构造方法中有super语句,它必须作为构造方法的第一条语句,以下代码是非法的: public MyException(){ String msg= \"Something wrong\"; super(msg); //编译错误,super语句必须作为构造方法的第一条语句 } 在创建子类的对象时,Java虚拟机首先执行父类的构造方法,然后再执行子类的Java Object-Oriented Programming 311 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类。这3个类都显式定义了默认的构造方法,此外还定义了一个带参数的构造方法。 例程11-3 Son.java class Grandpa{ protected Grandpa(){ System.out.println(\"default Grandpa\"); } 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); 312 Java Object-Oriented Programming 第11章 对象的生命周期 } public static void main(String args[]){ Son s1= new Son(\"My Grandpa\ //① Son s2=new Son(); //② } } 执行以上main()方法的第①条语句,打印结果如下: My Grandpa My Father My Son 此时构造方法的执行顺序如图11-1所示。 如果子类的构造方法没有用super语句显式调用父类的构造方法,那么通过这个构造方法创建子类对象时,Java虚拟机会自动先调用父类的默认构造方法。执行以上Son类的main()方法的第②条语句,打印结果如下: default Grandpa default Father default Son 此时构造方法的执行顺序如图11-2所示。 所有构造方法的执行顺序 所有构造方法的执行顺序 图11-1 调用Son类的带参数的构造方法时 图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)中通过superJava Object-Oriented Programming 313 Java面向对象编程 语句调用了父类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所示。 在下面的例子中,Base类中没有定义任何构造方法,它实际上有一个隐含的默认构造方法: Base(){} Sub类的Sub(int i)构造方法没有用super语句显式调用父类的构造方法,因此当创建Sub实例时,会先调用Base父类的隐含默认构造方法。 图11-3 调用Sub类的默认构造方法时所有构造方法的执行顺序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 构造方法的作用域 构造方法只能通过以下方式被调用: 󰁺 当前类的其他构造方法通过this语句调用它。 󰁺 当前类的子类的构造方法通过super语句调用它。 󰁺 在程序中通过new语句调用它。 对于例程11-4(Sub.java)的代码,请读者自己分析某些语句编译出错的原因。 例程11-4 Sub.java class Base{ public Base(int i,int j){} public Base(int i){ 314 Java Object-Oriented Programming 第11章 对象的生命周期 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); //编译出错 } 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和默认这4种访问级别之一。本节着重介绍构造方法处于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类型,也能禁止这个类被继承。这两者的区别是: Java Object-Oriented Programming 315 Java面向对象编程 󰁺 如果一个类允许其他程序用new语句构造它的实例,但不允许拥有子类,那么就把类声明为final类型。 󰁺 如果一个类既不允许其他程序用new语句构造它的实例,又不允许拥有子类,那么就把类的所有构造方法声明为private类型。 由于大多数类都允许其他程序用new语句构造它的实例,因此用final修饰符来禁止类被继承的做法更常见。 (3)这个类需要把构造自身实例的细节封装起来,不允许其他程序通过new语句创建这个类的实例。这个类向其他程序提供了获得自身实例的静态方法,这种方法称为静态工厂方法,本章第11.3节(静态工厂方法)对此做了进一步的介绍。 11.3 静态工厂方法 创建类的实例的最常见的方式是用new语句调用类的构造方法。在这种情况下,程序可以创建类的任意多个实例,每执行一条new语句,都会导致Java虚拟机的堆区中产生一个新的对象。假如类需要进一步封装创建自身实例的细节,并且控制自身实例的数目,那么可以提供静态工厂方法。 例如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 Gender female=new Gender(\"女\"); private static final Gender male=new Gender(\"男\"); private Gender(String description){this.description=description;} public static Gender getFemale(){ return female; } 316 Java Object-Oriented Programming 第11章 对象的生命周期 public static Gender getMale(){ return male; } public String getDescription(){return description;} } 这一特性的缺点是与其他的静态方法没有明显的区别,使用户难以识别类中到底哪些静态方法专门负责返回类的实例。为了减少这一缺点带来的负面影响,可以在为静态工厂方法命名时尽量遵守约定俗成的规范,当然这不是必需的。目前比较流行的规范是把静态工厂方法命名为valueOf或者getInstance。 󰁺 valueOf:该方法返回的实例与它的参数具有同样的值,例如: Integer a=Integer.valueOf(100); //返回取值为100的Integer对象 从上面代码可以看出,valueOf()方法能执行类型转换操作,在本例中,把int类型的基本数据转换为Integer对象。 󰁺 getInstance:返回的实例与参数匹配,例如: //返回符合中国标准的日历 Calendar cal=Calendar.getInstance(Locale.CHINA); (2)每次执行new语句时,都会创建一个新的对象。而静态工厂方法每次被调用的时候,是否会创建一个新的对象完全取决于方法的实现。 (3)new语句只能创建当前类的实例,而静态工厂方法可以返回当前类的子类的实例,这一特性可以在创建松耦合的系统接口时发挥作用,参见本章11.3.5节(松耦合的系统接口)。 静态工厂方法最主要的特点是:每次被调用的时候,不一定要创建一个新的对象。利用这一特点,静态工厂方法可用来创建以下类的实例。 󰁺 单例类:只有惟一的实例的类。 󰁺 枚举类:实例的数量有限的类。 󰁺 具有实例缓存的类:能把已经创建的实例暂且存放在缓存中的类。 󰁺 具有实例缓存的不可变类:不可变类的实例一旦创建,其属性值就不会被改变。 在下面几节,将结合具体的例子,介绍静态工厂方法的用途。 11.3.1 单例(singleton)类 单例类是指仅有一个实例的类。在系统中具有惟一性的组件可作为单例类,这种类的实例通常会占用较多的内存,或者实例的初始化过程比较冗长,因此随意创建这些类的实例会影响系统的性能。 Tips 熟悉Struts和Hibernate软件的读者会发现,Struts框架的ActionServlet类就是单例类,此外,Hibernate的SessionFactory和Configuration类也是单例类。 例程11-6的GlobalConfig类就是个单例类,它用来存放软件系统的配置信息。这些配置信息本来存放在配置文件中,在GlobalConfig类的构造方法中会从配置文件中Java Object-Oriented Programming 317 Java面向对象编程 读取配置信息,并把它存放在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; } } 实现单例类有两种方式: (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。 318 Java Object-Oriented Programming 第11章 对象的生命周期 例程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。在创建枚举类时,可以考虑采用以下设计模式: 󰁺 把构造方法定义为private类型。 󰁺 提供一些public static final类型的静态变量,每个静态变量引用类的一个实例。 󰁺 如果需要的话,提供静态工厂方法,允许用户根据特定参数获得与之匹配的实例。 例程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类的实例 */ Java Object-Oriented Programming 319 Java面向对象编程 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() { 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); } } 320 Java Object-Oriented Programming 第11章 对象的生命周期 也许你会问:用一个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参数传递任意的整数值,如果传递的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); 用户在创建自己的不可变类时,可以考虑采用以下设计模式: 󰁺 把属性定义为private final类型。 󰁺 不对外公开用于修改属性的setXXX()方法。 󰁺 只对外公开用于读取属性的getXXX()方法。 󰁺 在构造方法中初始化所有属性。 Java Object-Oriented Programming 321 Java面向对象编程 󰁺 覆盖Object类的equals()和hashCode()方法。在equals()方法中根据对象的属性值来比较两个对象是否相等,并且保证用equals()方法判断为相等的两个对象的hashCode()方法的返回值也相等,这可以保证这些对象能正确地放到HashMap或HashSet集合中,第15章的15.2.2节(HashSet类)对此做了进一步解释。 󰁺 如果需要的话,提供实例缓存和静态工厂方法,允许用户根据特定参数获得与之匹配的实例,参见本章第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(){ 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{ 322 Java Object-Oriented Programming 第11章 对象的生命周期 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对象的姓名都改为“王小虹”。由此可见,使用可变类更容易使程序代码出错。因为随意改变一个可变类对象的状态,有可能会导致与之关联的其他对象的状态被错误地改变。 不可变类的实例在实例的整个生命周期中永远保持初始化的状态,它没有任何状态变化,简化了与其他对象之间的关系。不可变类具有以下优点: 󰁺 不可变类能使程序更加安全,不容易出错。 󰁺 不可变类是线程安全的,当多个线程访问不可变类的同一个实例时,无须进行线程的同步。关于线程安全的概念,参见本书第13章的13.8.4节(线程安全的类)。 由此可见,应该优先考虑把类设计为不可变类,假使必须使用可变类,也应该把可变类尽可能多的属性设计为不可变的,即用final修饰符来修饰,并且不对外公开用于改变这些属性的方法。 在创建不可变类时,假如它的属性的类型是可变类型,在必要的情况下,必须提供保护性拷贝,否则,这个不可变类实例的属性仍然有可能被错误地修改。这条建议同样适用于可变类中用final修饰的属性。 例如例程11-10的Schedule类包含学校的开学时间和放假时间信息,它是不可变类,它的两个属性start和end都是final类型,表示不允许被改变,但是这两个属性都是Date类型,而Date类是可变类。 Java Object-Oriented Programming 323 Java面向对象编程 例程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。 例程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对象的属性值。 324 Java Object-Oriented Programming 第11章 对象的生命周期 Tips 如果Schedule类中被final修饰的属性所属的类是不可变类,就无须提供保护性拷贝,因为该属性所引用的实例的值永远不会被改变。这进一步体现了不可变类的优点。 11.3.4 具有实例缓存的不可变类 不可变类的实例的状态不会变化,这样的实例可以安全地被其他与之关联的对象共享,还可以安全地被多个线程共享。为了节省内存空间,优化程序的性能,应该尽可能地重用不可变类的实例,避免重复创建具有相同属性值的不可变类的实例。 在JDK 1.5的基本类库中,对一些不可变类,如Integer类做了优化,它具有一个实例缓存,用来存放程序中经常使用的Integer实例。JDK 1.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 以上代码共创建了3个Integer对象,参见图11-4。每个new语句都会创建一个新的Integer对象。而Integer.valueOf(10)方法仅在第一次被调用时,创建取值为10的Integer对象,在第二次被调用时,直接从实例缓存中获得它。由此可见,在程序中用valueOf()静态工厂方法获得Integer对象,可以提高Integer对象的可重用性。 到底如何实现实例的缓存呢?缓存并没有固定的实现方式,完善的缓存实现不仅要考虑何时把实例加入缓存,还要考虑何时把不再使用的实例从缓存中及时清除,以保证有效合理地利用内存空间。一种简单的实现是直接用Java集合来作为实例缓存。本章11.3.2节的例程11-8的Gender 类中的Map类型的instancesBySex属性就图11-4 引用变量与Integer对象的引用关系Java Object-Oriented Programming 325 Java面向对象编程 是一个实例缓存,它存放了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; } 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实例,应该使用326 Java Object-Oriented Programming 第11章 对象的生命周期 new语句创建它,使它能及时结束生命周期;对于程序需要经常访问的Name实例,那就用valueOf()方法来获得它,因为该方法能把Name实例放到缓存中,使它可以被重用。 Tips 从例程11-12的Name类也可以看出,在有些情况下,一个类可以同时提供public的构造方法和静态工厂方法。用户可以根据实际需要,灵活地决定到底以何种方式获得类的实例。 另外要注意的是,没有必要为所有的不可变类提供实例缓存。随意创建大量实例缓存,反而会浪费内存空间,降低程序的运行性能。通常,只有满足以下条件的不可变类才需要实例缓存。 󰁺 不可变类的实例的数量有限。 󰁺 在程序运行过程中,需要频繁访问不可变类的一些特定实例。这些实例拥有与程序本身同样长的生命周期。 11.3.5 松耦合的系统接口 一个类的静态工厂方法可以返回子类的实例,这一特性有助于创建松耦合的系统接口。如果系统规模比较简单,静态工厂方法可以直接作为类本身的静态方法;如果系统规模比较大,根据创建精粒度对象模型的原则,可以使创建特定类的实例的功能专门由一个静态工厂类来负责。第1章的1.6节的例程1-11的ShapeFactory就是一个静态工厂类,它负责构造Shape类的实例。ShapeFactory类有一个静态工厂方法: public static Shape getShape(int type){…} 以上方法声明的返回类型是Shape类型,实际上返回的是Shape子类的实例。对于Shape类的使用者Panel类,只用于访问Shape类,而不必访问它的子类。 //获得一个Circle实例 Shape shape=ShapeFactory.getInstance(ShapeFactory.SHAPE_TYPE_CIRCLE); 在分层的软件系统中,业务逻辑层向客户层提供服务,静态工厂类可以进一步削弱这两个层之间的松耦合关系。如图11-5所示,业务逻辑层向客户层提供ServiceIFC接口,在该接口中声明了所提供的各种服务,它有3个实现类:ServiceImpl1、ServiceImpl2和ServiceImpl3。ServiceFactory静态工厂类负责构造ServiceIFC的实现类的实例,它的定义如下: public ServiceFactory{ private static final String serviceImpl; static{ //读取配置信息,根据配置信息设置服务实现类的类型,假定为ServiceImpl1 serviceImpl=\"ServiceImpl1\"; } public static ServiceIFC getInstance(){ Class.forName(serviceImpl).newInstance(); } } 327 Java Object-Oriented Programming Java面向对象编程 图11-5 静态工厂模型 当客户层需要获得业务逻辑层的服务时,先从静态工厂类ServiceFactory中获得ServiceIFC接口的实现类的实例,然后通过接口访问服务。 ServiceIFC service=ServiceFactory.getInstance(); service.service1(); 在客户层只会访问ServiceIFC接口,至于业务逻辑层到底采用哪个实现类的实例提供服务,这对客户层来说是透明的。 11.4 垃圾回收 当对象被创建时,就会在Java虚拟机的堆区中拥有一块内存,在Java虚拟机的生命周期中,Java程序会陆续地创建无数个对象,假如所有的对象都永久占有内存,那么内存有可能很快被消耗光,最后引发内存空间不足的错误。因此必须采取一种措施来及时回收那些无用对象的内存,以保证内存可以被重复利用。 在一些传统的编程语言(如C语言)中,回收内存的任务是由程序本身负责的。程序可以显式地为自己的变量分配一块内存空间,当这些变量不再有用时,程序必须显式地释放变量所占用的内存。把直接操纵内存的权利赋给程序,尽管给程序带来了很多灵活性,但是也会导致以下弊端: 󰁺 程序员有可能因为粗心大意,忘记及时释放无用变量的内存,从而影响程序的健壮性。 󰁺 程序员有可能错误地释放核心类库所占用的内存,导致系统崩溃。 在Java语言中,内存回收的任务由Java虚拟机来担当,而不是由Java程序来负责。在程序的运行时环境中,Java虚拟机提供了一个系统级的垃圾回收器线程,它负责自动回收那些无用对象所占用的内存,这种内存回收的过程被称为垃圾回收(Garbage Collection)。 垃圾回收具有以下优点: 󰁺 把程序员从复杂的内存追踪、监测和释放等工作中解放出来,减轻程序员进328 Java Object-Oriented Programming 第11章 对象的生命周期 行内存管理的负担。 󰁺 防止系统内存被非法释放,从而使系统更加健壮和稳定。 垃圾回收具有以下特点: 󰁺 只有当对象不再被程序中的任何引用变量引用时,它的内存才可能被回收。 󰁺 程序无法迫使垃圾回收器立即执行垃圾回收操作。 󰁺 当垃圾回收器将要回收无用对象的内存时,先调用该对象的finalize()方法,该方法有可能使对象复活,导致垃圾回收器取消回收该对象的内存。 11.4.1 对象的可触及性 在Java虚拟机的垃圾回收器看来,堆区中的每个对象都可能处于以下三个状态之一。 󰁺 可触及状态:当一个对象(假定为Sample对象)被创建后,只要程序中还有引用变量引用它,那么它就始终处于可触及状态。 󰁺 可复活状态:当程序不再有任何引用变量引用Sample对象时,它就进入可复活状态。在这个状态中,垃圾回收器会准备释放它占用的内存,在释放之前,会调用它及其他处于可复活状态的对象的finalize()方法,这些finalize()方法有可能使Sample对象重新转到可触及状态。 󰁺 不可触及状态:当Java虚拟机执行完所有可复活对象的finalize()方法后,假如这些方法都没有使Sample对象转到可触及状态,那么Sample对象就进入不可触及状态。只有当对象处于不可触及状态时,垃圾回收器才会真正回收它占用的内存。 图11-6显示了对象的状态转换过程。 图11-6 对象的状态转换图 Tips 在本书第1章的1.3.1节曾经提到,对象的状态是指某一瞬间其所有属性的取值。本节谈到的对象的状态具有不同的含义,是按照是否可以被垃圾回收来划分对象的状态的。 以下method()方法先后创建了两个Integer对象。 public static void method(){ Integer a1=new Integer(10); //① Java Object-Oriented Programming 329 Java面向对象编程 Integer a2=new Integer(20); //② a1=a2; //③ } public static void main(String args[]){ method(); System.out.println(\"End\"); } 当程序执行完第③行时,取值为10的Integer对象不再被任何变量引用,因此转到可复活状态,取值为20的Integer对象处于可触及状态,它被变量a1和a2引用,参见图11-7。 当程序退出method()方法并返回到main()方法时,在method()方法中定义的局部变量a1和a2都将结束生命周期,堆区中取值为20的Integer对象也将转到可复活状态。 图11-7 两个Integer对象的状态 如果从对象A到对象B存在关联关系,实际上就是指对象A的某个实例变量引用对象B。第6章的6.7.5节(区分对象的属性与继承)介绍了Book类与Category类的单向关联,以及Category类的自身双向关联,参见6.7.5节的图6-10。在6.7.5节的例程6-5中,CategoryTester类的create()方法创建了3个Category对象和1个Book对象,当create()方法执行完毕,退回到main()方法时,Book对象被main()方法中的mathBook局部变量引用,参见图11-8。 图11-8 Book对象、Category对象及mathBook变量之间的关系 从图11-8可以看出,尽管在程序中仅仅持有Book对象的引用,但是其他3个Category对象也都是可触及的。以下程序代码演示了如何从Book对象依次导航到其他3个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()方法,何时使它转到不可触及状态,何时回收它占用的内存,这对于程序来说都是透明的。程序只能决定一个对象何时不再被任何引用变量引用,使得它成为可以被回收的垃圾。这就像每330 Java Object-Oriented Programming 第11章 对象的生命周期 个居民只要把无用的物品(相当于无用的对象)放在指定的地方,清洁工人就会把它收拾走一样,但是,垃圾什么时候被收走,居民是不知道的,也无须对此了解。 站在程序的角度,如果一个对象不处于可触及状态,就可以称它为无用对象,程序不会持有无用对象的引用,不会再使用它,这样的对象可以被垃圾回收器回收。站在程序的角度,一个对象的生命周期从被创建开始,到不再被任何变量引用(即变为无用对象)结束。在本书其他章节提到对象的生命周期,如果未做特别说明,都沿用这个含义。 垃圾回收器作为低优先级线程独立运行。在任何时候,程序都无法迫使垃圾回收器立即执行垃圾回收操作。在程序中可以调用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()方法具有以下特点: 󰁺 垃圾回收器是否会执行该方法及何时执行该方法,都是不确定的。 󰁺 finalize()方法有可能使对象复活,使它恢复到可触及状态。 󰁺 垃圾回收器在执行finalize()方法时,如果出现异常,垃圾回收器不会报告异常,程序继续正常运行。 下面结合一个具体的例子来解释finalize()方法的特点。例程11-13的Ghost类是一个带实例缓存的不可变类,它的finalize()方法能够把当前实例重新加入到实例缓存Java Object-Oriented Programming 331 Java面向对象编程 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; } 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 以上程序创建了3个对象:1个Ghost对象、1个常量字符串“IAmBack”及1个HashMap对象。当程序执行完main()方法的第③行时,内存中引用变量与对象之间的关系如图11-9所示。 332 Java Object-Oriented Programming 第11章 对象的生命周期 图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()方法来复活对象。可以把处于可触及状态的对象比做活在阳间的人,把不处于这个状态的对象(无用对象)比做到了阴间的人。程序所能看见和使用的是阳间的人,假如阎王经常悄悄地让几个阴间的人复活,使他们在程序毫不知情的情况Java Object-Oriented Programming 333 Java面向对象编程 下溜回阳间,这只会扰乱程序的正常执行流程。 11.4.5 比较finalize()方法和finally代码块 在Object类中提供了finalize()方法,它的初衷是用于在对象被垃圾回收器回收之前,释放所占用的相关资源,这和try…catch…finally语句的finally代码块的用途比较相似。但由于垃圾回收器是否会执行finalize()方法及何时执行该方法,都是不确定的,因此在程序中不能用finalize()方法来完成同时具有以下两个特点的释放资源的操作。 󰁺 必须执行。 󰁺 必须在某个确定的时刻执行。 具有以上特点的操作更适合于放在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(\"女\"); 334 Java Object-Oriented Programming 第11章 对象的生命周期 以上Gender对象的生命周期取决于FEMALE变量的生命周期,而FEMALE静态变量的生命周期取决于代表Gender类的Class对象的生命周期。在Gender类不会被卸载的情况下,它的Class对象会常驻内存,直到程序运行结束,因此Gender对象一旦被创建,也会常驻内存,直到程序运行结束。 再例如以下局部变量sb和s分别引用一个StringBuffer对象和一个String对象: public void method(){ StringBuffer sb=new StringBuffer(\"Hello\"); sb.append(\" World\"); String s=sb.toString(); System.out.println(s); } 局部变量的生命周期很短暂,当method()方法执行完毕时,局部变量就结束生命周期。因此一旦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) Java Object-Oriented Programming 335 Java面向对象编程 throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity(){ //增加堆栈的容量 if(elements.length==size){ Object[] oldElements=elements; elements=new Object[elements.length+capacityIncrement]; System.arraycopy(oldElements,0,elements,0,size); } } } 以上程序看上去是可行的,可以正常地完成对象的入栈和出栈操作。下面的程序代码先向堆栈压入1 000个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 对象的强、软、弱和虚引用 在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它。这就像在日常生活中,从商店购买了某样物品后,如果有用,就一直保留它,否则就把它扔到垃圾箱,由清洁工人收走。一般说来,如果物品已经被扔到垃圾箱,想再把它捡回来使用就不可能了。 但有时候情况并不这么简单,你可能会遇到类似鸡肋一样的物品,食之无味,弃之可惜。这种物品现在已经无用了,保留它会占空间,但是立刻扔掉它也不划算,因为也许将来还会派上用场。对于这样的可有可无的物品,一种折衷的处理办法是:如果家里空间足够,就先把它保留在家里,如果家里空间不够,即使把家里所有的垃圾清除,也无法容纳那些必不可少的生活用品,那么再扔掉这些可有可无的物品。 从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控336 Java Object-Oriented Programming 第11章 对象的生命周期 制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。 1.强引用(StrongReference) 本章前文介绍的引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 2.软引用(SoftReference) 如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。 3.弱引用(WeakReference) 如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 4.虚引用(PhantomReference) “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 Tips 在本书中,“引用”既可以作为动词,也可以作为名词,读者应该根据上下文来区分“引用”的含义。 在java.lang.ref包中提供了三个类—SoftReference类、WeakReference类和PhantomReference类,它们分别代表软引用、弱引用和虚引用。ReferenceQueue类表Java Object-Oriented Programming 337 Java面向对象编程 示引用队列,它可以和这三种引用类联合使用,以便跟踪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所示。 图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。 338 Java Object-Oriented Programming 第11章 对象的生命周期 在以下程序代码中,执行完第④行后,“hello”对象仅仅具有弱引用。接下来两次调用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-11 “hello”对象只具有弱引用 图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对象占用较大内存空间,有80KB左右 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); } Java Object-Oriented Programming 339 Java面向对象编程 } public class References { 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对象的强引用。 340 Java Object-Oriented Programming 第11章 对象的生命周期 例程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]; //存放Key对象的强引用 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); } } 以上程序的部分打印结果如下: Finalizing Key 998 Finalizing Key 997 Finalizing Key 995 Finalizing Key 994 Java Object-Oriented Programming 341 Java面向对象编程 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虚拟机会从最上层的父类开始,依次执行各个父类及当前类的构造方法,从而保证来自于对象本身及从父类中继承的实例变量都被正确地初始化。 当一个对象不被程序的任何引用变量引用时,对象就变成无用对象,它占用的内存就可以被垃圾回收器回收。每个对象都会占用一定的内存空间,而内存是有限的资源,为了合理地利用内存,在决定对象的生命周期时,应该遵循以下原则: 󰁺 重用已经存在的对象,尤其是需要经常访问的不可变类的对象。在这种情况下,程序可通过类的静态工厂方法来获得已经存在的对象,而不是通过new语句来创建新的对象。 󰁺 当程序不需要再使用一个对象时,应该及时清除对这个对象的引用,使它的内存可以被回收。 在垃圾回收器的眼里,对象的生命周期开始于在内存中拥有立足之地,结束于它的内存被回收。由于无用对象何时被垃圾回收器回收对程序是透明的,因此在Java程序的眼里,对象的生命周期开始于在内存中拥有立足之地,并且能通过引用变量引用它,结束于没有任何引用变量引用它。 从JDK 1.2版本开始,对象的引用可分为4种级别:强引用、软引用、弱引用和虚引用。如果一个对象不允许被垃圾回收器回收,则应该持有强引用;如果一个对象可以被垃圾回收器回收,但是在没有被回收之前仍然可以使用,则应该持有软引用或弱引用。一个仅持有虚引用的对象在任何时候都可能被垃圾回收器回收。虚引用与引用队列联合使用,可用来跟踪垃圾回收的过程。 11.8 思考题 1.以下类是否具有默认构造方法? public class Counter { 342 Java Object-Oriented Programming 第11章 对象的生命周期 int current, step; public Counter(int startValue, int stepValue) { set(startValue); setStepValue(stepValue); } public int get() { return current; } public void set(int value) { current = value; } public void setStepValue(int stepValue) { step = stepValue; } } 2.构造方法可以被哪些修饰符修饰? (a)final (b)static (c)synchronized (d)native 3.以下代码能否编译通过?假如能编译通过,运行时将得到什么打印结果? public class Hope{ public static void main(String argv[]){ Hope h = new Hope(); } protected Hope(){ for(int i =0; i <10; i ++){ System.out.println(i); } } } 4.以下代码能否编译通过?假如能编译通过,运行“java B”时将得到什么打印结果? class A { int i; A(int i) { this.i = i * 2; } } class B extends A { public static void main(String[] args) { B b = new B(2); } B(int i) { System.out.println(i); } } 5.以下代码能否编译通过?假如能编译通过,运行时将得到什么打印结果? class Mystery { String s; public static void main(String[] args) { Mystery m = new Mystery(); m.go(); } void Mystery() { s = \"constructor\"; } Java Object-Oriented Programming 343 Java面向对象编程 void go() { System.out.println(s); } } 6.对于以下代码: class Base{ Base(int i){ System.out.println(\"base constructor\"); } Base(){} } public class Sup extends Base{ public static void main(String argv[]){ Sup s= new Sup(); //One } Sup(){ //Two } public void derived(){ //Three } } 以下哪些选项使得程序运行时打印“base constructor”? (a)在“//One”的地方加入一行“Base(10);” (b)在“//One”的地方加入一行“super(10);” (c)在“//Two”的地方加入一行“super(10);” (d)在“//Three”的地方加入一行“super(10);” 7.java.lang.Class类没有public类型的构造方法。这种说法是否正确? 8.以下代码能否编译通过?假如能编译通过,运行时将得到什么打印结果? class A { public A() {} public A(int i) { this(); } } class B extends A { public boolean B(String msg) { return false; } } class C extends B { private C() { super(); } public C(String msg) { this(); } public C(int i) {} } 344 Java Object-Oriented Programming

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- ovod.cn 版权所有

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务