3.3 创建和初始化对象
介绍字段和方法之后,接下来要介绍类的其他重要成员。具体而言,我们要介绍构造方法。构造方法是类成员,作用是初始化新建实例中的字段。
再看一下创建 Circle 对象的方式:
Circle c = new Circle();
这行代码的意思是,调用看起来有点儿像方法的东西创建一个新 Circle 实例。其实,Circle() 是一种构造方法,是类中的成员,和类同名,而且像方法一样,有主体。
构造方法的工作方式是这样的:new 运算符表明我们想创建类的一个新实例。首先,分配内存存储新建的对象实例;然后,调用构造方法的主体,并传入指定的参数;最后,构造方法使用这些参数执行初始化新对象所需的一切操作。
Java 中的每个类都至少有一个构造方法,其作用是执行初始化新对象所需的操作。示例 3-1 定义的 Circle 类没有显式定义构造方法,因此 javac 编译器自动为我们提供了一个构造方法(叫作默认构造方法)。这个构造方法没有参数,而且不执行任何特殊的初始化操作。
3.3.1 定义构造方法
可是 Circle 对象显然要做些初始化操作,下面就来定义一个构造方法。示例 3-2 重新定义了 Circle 类,包含一个构造方法,指定新建 Circle 对象的半径。借此机会,我们还把 r 字段改成了受保护的(禁止对象随意访问)。
示例 3-2:为 Circle 类定义一个构造方法
public class Circle {public static final double PI = 3.14159; // 常量// 实例字段,保存圆的半径protected double r;// 构造方法:初始化r字段public Circle(double r) { this.r = r; }// 实例方法:基于半径计算得到值public double circumference() { return 2 * PI * r; }public double area() { return PI * r*r; }public double radius() { return r; }}
如果依赖编译器提供的默认构造方法,就要编写如下的代码显式初始化半径:
Circle c = new Circle();c.r = 0.25;
添加上述构造方法后,初始化变成创建对象过程的一部分:
Circle c = new Circle(0.25);
下面是一些关于命名、声明和编写构造方法的基本注意事项。
构造方法的名称始终和类名一样。
声明构造方法时不指定返回值类型,连
void都不用。构造方法的主体初始化对象。可以把主体的作用想象为设定
this引用的内容。构造方法不能返回
this或任何其他值。
3.3.2 定义多个构造方法
有时,根据遇到的情况,可能想在多个不同的方式中选择一个最便利的方式初始化对象。例如,我们可能想使用指定的值初始化圆的半径,或者使用一个合理的默认值初始化。为 Circle 类定义两个构造方法的方式如下:
public Circle() { r = 1.0; }public Circle(double r) { this.r = r; }
Circle 类只有一个实例字段,由此并没有太多的初始化方式。不过在复杂的类中,经常会定义不同的构造方法。
只要构造方法的参数列表不同,为一个类定义多个构造方法完全是合法的。编译器会根据提供的参数数量和类型判断你想使用的是哪个构造方法。定义多个构造方法和方法重载的原理类似。
3.3.3 在一个构造方法中调用另一个构造方法
如果类有多个构造方法,会用到 this 关键字的一种特殊用法。在一个构造方法中可以使用 this 关键字调用同一个类中的另一个构造方法。因此,前面 Circle 类的两个构造方法可以改写成:
// 这是基本构造方法:初始化半径public Circle(double r) { this.r = r; }// 这个构造方法使用this()调用前一个构造方法public Circle() { this(1.0); }
如果一些构造方法共用大量的初始化代码,这种技术是有用的,因为能避免代码重复。如果构造方法执行很多初始化操作,在这种复杂的情况下,这种技术十分有用。
使用 this() 时有个重大的限制:只能出现在构造方法的第一个语句中。但是,调用这个方法后,可以执行构造方法所需的任何其他初始化操作。这个限制的原因涉及自动调用超类的构造方法,本章后面会说明。
3.3.4 字段的默认值和初始化程序
类中的字段不一定要初始化。如果没有指定初始值,字段自动使用默认值初始化:false、\u0000、0、0.0 或 null。具体使用哪个值,根据字段的类型而定(详情参见表 2-1)。这些默认值由 Java 语言规范规定,实例字段和类字段都适用。
如果字段的默认值不适合字段,可以显式提供其他的初始值。例如:
public static final double PI = 3.14159;public double r = 1.0;
字段声明不是任何方法的一部分。Java 编译器会自动为字段生成初始化代码,然后把这些代码放在类的所有构造方法中。这些初始化代码按照字段在源码中出现的顺序插入构造方法,因此,字段的初始化程序可以使用在其之前声明的任何字段的初始值。
例如下述代码片段是一个假设类,定义了一个构造方法和两个实例字段:
public class SampleClass {public int len = 10;public int[] table = new int[len];public SampleClass() {for(int i = 0; i < len; i++) table[i] = i;}// 类余下的内容省略了……}
对这个例子来说,javac 生成的构造方法其实和下述代码等效:
public SampleClass() {len = 10;table = new int[len];for(int i = 0; i < len; i++) table[i] = i;}
如果某个构造方法的开头使用 this() 调用其他构造方法,那么字段的初始化代码不会出现在这个构造方法中。此时,初始化由 this() 调用的构造方法处理。
既然实例字段在构造方法中初始化,那么类字段在哪初始化呢?就算从不创建类的实例,类字段也关联在类身上。这意味着,类字段要在调用构造方法之前初始化。
为此,javac 会为每个类自动生成一个类初始化方法。类字段在这个方法的主体中初始化。这个方法只在首次使用类之前调用一次(经常是在 Java 虚拟机首次加载类时)。
和实例字段的初始化一样,类字段的初始化表达式按照类字段在源码中的顺序插入类初始化方法。因此,类字段的初始化表达式可以使用在其之前声明的类字段。类初始化方法是内部方法,对 Java 程序员不可见。在类文件中,它的名称是 (例如,使用 javap 检查类文件时可以看到这个方法。第 13 章会详细介绍如何使用 javap 执行这项操作)。
初始化程序块
至此,我们知道对象可以通过字段的初始化表达式和构造方法中的任何代码初始化。类有一个类初始化方法,这个方法和构造方法不一样,不能像构造方法那样显式定义主体。不过,Java 允许编写用于初始化类字段的代码,所用的结构叫静态初始化程序。静态初始化程序由 static 关键字及随后的花括号中的代码块组成。在类定义中,静态初始化程序可以放在字段和方法定义能出现的任何位置。例如,下述代码为两个类字段执行一些重要的初始化操作:
// 我们可以使用三角函数画出圆的轮廓// 不过,三角函数很慢,所以预先算出一些值public class TrigCircle {// 这是静态查找表和各自的初始化程序private static final int NUMPTS = 500;private static double sines[] = new double[NUMPTS];private static double cosines[] = new double[NUMPTS];// 这是一个静态初始化程序,填充上述数组static {double x = 0.0;double delta_x = (Circle.PI/2)/(NUMPTS-1);for(int i = 0, x = 0.0; i < NUMPTS; i++, x += delta_x) {sines[i] = Math.sin(x);cosines[i] = Math.cos(x);}}// 类余下的内容省略了……}
一个类可以有任意多个静态初始化程序。各个初始化程序块的主体会和所有静态字段的初始化表达式一起合并到类初始化方法中。静态初始化程序和类方法的相同点是,不能使用 this 关键字,也不能使用类中的任何实例字段或实例方法。
类还可以有实例初始化程序。实例初始化程序和静态初始化程序类似,不过初始化的是对象而不是类。一个类可以有任意多个实例初始化程序,而且实例初始化程序可以放在字段和方法定义能出现的任何位置。各个实例初始化程序的主体和所有实例字段初始化表达式一起,放在类中每个构造方法的开头。实例初始化程序的外观和静态初始化程序类似,不过不使用 static 关键字。也就是说,实例初始化程序只是放在花括号里的任意 Java 代码。
实例初始化程序可以初始化数组或其他需要复杂初始化操作的字段。实例初始化程序有时很有用,因为它们把初始化代码放在字段后面,而不是单独放在构造方法中。例如:
private static final int NUMPTS = 100;private int[] data = new int[NUMPTS];{ for(int i = 0; i < NUMPTS; i++) data[i] = i; }
不过,现实中很少使用实例初始化程序。
字段声明不是任何方法的一部分。Java 编译器会自动为字段生成初始化代码,然后把这些代码放在类的所有构造方法中。这些初始化代码按照字段在源码中出现的顺序插入构造方法,因此,字段的初始化程序可以使用在其之前声明的任何字段的初始值。