3.2 创建对象
对象即类的实例化。在Java虚拟机的生命周期中,一个对象被创建,又一个被销毁。在对象的生命周期的开始阶段,需要为对象分配内存,并且初始化它的实例变量。当程序不再使用某个对象时,它就会结束生命周期。它的内存可以被Java虚拟机的垃圾回收器回收。在Java程序中,对象可以被显式地或隐含地创建。创建一个对象就是指构造一个类的实例。用new语句创建对象是Java语言创建对象的最常见的方式。
3.2.1 构造方法
在多数情况下,初始化一个对象的最终步骤是去调用这个对象的构造方法。构造方法的功能是:当创建一个类的对象时,首先用new语句从堆区中分配该对象的内存空间,然后自动调用类的某一个构造方法,对该对象的内存空间进行初始化,为实例变量赋予合适的初始值。构造方法的语法规则:
● 方法名必须与类名相同。
● 不要声明返回类型。
● 不能被static、final、synchronized、abstract和native修饰。
【例3.3】 不带形参构造方法,在构造方法中对其成员变量初始化。
ConstructorInitiate.java
public class ConstructorInitiate { boolean t; byte b; short s; int i; long l; float f; double d; char c; ConstructorInitiate() { t=true; // 成员变量的初始化 b = 12; s = 99; i = 49; l = 120; f = 3.14f; d = 3.1415; c = 'h'; } public static void main(String[] args) { ConstructorInitiate initiate; // 定义引用变量 initiate = new ConstructorInitiate(); System.out.println(initiate.t); System.out.println(initiate.b); System.out.println(initiate.s); System.out.println(initiate.i); System.out.println(initiate.l); System.out.println(initiate.f); System.out.println(initiate.d); System.out.println(initiate.c); } }
Java虚拟机在执行下面语句时,将在栈内存中生成一个引用变量initiate,由于initiate是一个局部变量,且这时还没有创建任何对象,所以initiate还没有被赋值。
ConstructorInitiate initiate;
执行下面的语句后,将在堆内存中创建一个ConstructorInitiate对象,并对其成员变量进行初始化,并把对象的引用赋给initiate。这时,initiate指向ConstructorInitiate对象,其内存布局如图3-1所示。
图3-1 对象的内存布局
initiate = new ConstructorInitiate();
程序运行结果:
true 12 99 49 120 3.14 3.1415 h
既然在创建对象时,会对类的成员变量进行初始化,那么初始化的时机和顺序又是如何?下面的程序解释了初始化时机和顺序。
【例3.4】 在Teacher类中创建3个Pupil对象来测试类的初始化时机和顺序。
Sequence.java
class Pupil{ Pupil(int age){ System.out.println("pupil:"+age); } } class Teacher{ Pupil p1=new Pupil(9); Teacher(){ System.out.println("Teacher()"); p3=new Pupil(10); } Pupil p2=new Pupil(11); void teach(){ System.out.println("teach()"); } Pupil p3=new Pupil(12); } public class Sequence{ public static void main(String[]args){ Teacher t=new Teacher(); t.teach(); } }
在类Teacher中定义的3个成员变量p1、p2、p3均为引用类型。
程序运行结果:
pupil: 9 pupil: 11 pupil: 12 Teacher() pupil: 10 teach()
可以发现,成员变量p1、p2、p3定义在类体中的各个地方,初始化的顺序由变量定义的顺序决定,尽管变量定义位于方法(包括构造方法)之后,但仍然在方法(包括构造方法)被调用之前得到初始化。
3.2.2 默认构造方法
Java语言规定,每个类都必须至少定义一个构造方法。若一个类没有定义构造方法,则编译程序提供一个构造方法。无参数的构造方法称为默认构造方法。编译程序自动提供的构造方法就是一个默认构造方法。格式如下:
<与类相同的访问控制符><方法名>(){ super(); // 自动调用父类的默认构造方法 }
说明:若类是public,自动生成的默认构造方法也是public。若是private(如内部类),则该默认构造方法也是private。由于方法体中有super();语句,因而要求该类的父类必须要定义一个默认构造方法,若没有定义,则编译报错。这种错误在Java初学者中是经常发生的。
若一个类中已定义构造方法(不论有无参数),编译程序将不再自动提供上述格式的默认构造方法。因此,当给类定义构造方法时,建议同时定义一个无参构造方法,即默认构造方法,以避免前面所述的编译错误。例如下面的代码片段:
public class Dog1{ // 编译程序自动提供默认方法 System.out.println("run fast"); } public class Dog2{ // 该类缺少默认构造方法。建议程序员自定义一个 public Dog2(String s) { System.out.println("run fast"); } } public class Dog3{ // 自己定义了默认构造方法 public Dog3(){ System.out.println(" run fast"); } }
可以调用Dog1类的默认构造方法来创建Dog1对象。
Dog1 d1=new Dog1(); // 合法
Dog2类没有默认的构造方法,因此以下语句会导致编译错误。
Dog2 d2=new Dog2(); // 编译出错
Dog3类显式定义了默认的构造方法,因此以下语句是合法的。
Dog1 d3 = new Dog3(); // 合法
3.2.3 构造方法重载
当通过new语句创建一个对象时,在不同条件下,对象可能有不同的初始化行为。既然构造器的名字已经由类名决定,就只能有一个构造器名。那么要想用多种方式创建一个对象该怎么办呢?假设要创建一个类,既可以用标准方式进行初始化,也可以从文件里读取信息来初始化。这就需要两个构造器:一个默认构造器;另一个取字符串作为形式参数。由于都是构造器,所以它们必须用到相同的名字,即类名。为了让方法名相同而形参不同的构造器同时存在,必须用到方法重载。方法的重载是指一个类中可以定义有相同的名字,但参数不同的多个方法。各方法之间的参数个数、参数类型、排列顺序不同即可构成重载,有普通方法重载和构造方法重载。
【例3.5】 重载构造方法Student(),并根据实在参数调用相应的构造方法。
ConstructorOverLoad.java
class Student { private String name = "Lucy"; private int age = 18; Student() { System.out.println("invoke no parameter construcor method"); System.out.println("name is " + name + ",age is " + age); } Student(String n) { name = n; System.out.println("invoke construcor method with one parameter"); System.out.println("name is " + name + ",age is " + age); } Student(String n, int i) { name = n; age = i; System.out.println("invoke construcor method with two parameters"); System.out.println("name is " + name + ",age is " + age); } } public class ConstructorOverLoad { public static void main(String[] args) { new ConstructorOverLoad(); new Student(); new Student("Tom"); new Student("Jack", 25); } }
程序运行结果:
invoke no parameter construcor method name is Lucy, age is 18 invoke construcor method with one parameter name is Tom, age is 18 invoke construcor method with two parameters name is Jack, age is 25
3.2.4 普通方法重载
同构造方法一样,普通方法也可重载。下面的代码片段重载了普通方法f(),如下所示。
void f(int x) { … } void f(int x, int y) { … } void f(int x, String s) { … }
注意:只有返回值类型不同是不行的,例如下面的语句:
int f(int x,int y){…} double f(int x,int y){…}
方法重载是编译时多态性的表现,Java编译程序会根据方法调用的实在参数来决定使用哪一个方法。
【例3.6】 测试方法重载,程序重载了sort()方法,根据所传递的参数个数来调用相应的sort()方法,其sort()方法用来对2个整数或3个整数按从小到大排序。
TestSort.java
class SortDemo { int max, midst, mix; SortDemo() { max = -1; midst = -1; mix = -1; } void sort(int i,int j){ //两个数排序 int s; max = i; mix = j; if (max < mix) { s = max; max = mix; mix = s; } } void sort(int i,int j,int k){ //三个数排序 int s; max = i; midst = j; mix = k; if(max<midst){ //第一个数和第二个数比较 s = max; max = midst; midst = s; } if(max<mix){ //第一个数和第三个数比较 s = max; max = mix; mix = s; } if(midst<mix){ //第二个数和第三个数比较 s = midst; midst = mix; mix = s; } } } public class TestSort { public static void main(String args[]) { SortDemo sd = new SortDemo(); sd.sort(30, 60); System.out.println("两个数从大到小为:" + sd.max + "," + sd.mix); sd.sort(20, 80, 50); System.out.println("三个数从大到小为:" + sd.max + "," + sd.midst + ","+ sd.mix); } }
程序运行结果:
两个数从大到小为:60,30 三个数从大到小为:80,50,20
调用一个重载过的方法时,Java编译程序是如何确定究竟应该调用哪一个方法?以下代码定义了三个重载方法:
public void f(char ch){ System.out.println(“char!”); } public void f(short sh){ System.out.println(“short!”); } public void f(float f){ System.out.println(“float!”); }
当调用语句f((byte)65);时,到底调用的是上述哪一个方法?
这个问题其实就是2.1.5 节中所述的类型转换,是在“方法调用时发生的类型转换”,即在方法调用上下文中产生的类型转换。其类型转换是按2.1.5 节中“基本数据类型宽转换”进行。若“宽转换”不成功,再进行“装箱”和“拆箱”类型转换。
按“宽转换”,byte可自动转换成short或float,但short比float更特殊。特殊的含义是:short能够宽转换到float,反之却不行。故最终调用的是方法:public void f(short sh){…};
再如
public void f(Object o){…} public void f(int[] ia){…}
当调用语句f(null);时,应调用哪一个方法?由于空引用null可自动转换到int[]类型,也可自动转换到Objec类型。但int[]数组类型更特殊(即:int[]数组类型是一个Object类型,但Object不是int[]数组),故最终调用的是f(int[] ia)方法。