# 4.4 成员初始化
Java尽自己的全力保证所有变量都能在使用前得到正确的初始化。若被定义成相对于一个方法的“局部”变量,这一保证就通过编译期的出错提示表现出来。因此,如果使用下述代码:
```
void f() {
int i;
i++;
}
```
就会收到一条出错提示消息,告诉你`i`可能尚未初始化。当然,编译器也可为`i`赋予一个默认值,但它看起来更象一个程序员的失误,此时默认值反而会“帮倒忙”。若强迫程序员提供一个初始值,就往往能够帮他/她纠出程序里的“Bug”。
然而,若将基本类型设为一个类的数据成员,情况就会变得稍微有些不同。由于任何方法都可以初始化或使用那个数据,所以在正式使用数据前,若还是强迫程序员将其初始化成一个适当的值,就可能不是一种实际的做法。然而,若为其赋予一个垃圾值,同样是非常不安全的。因此,一个类的所有基本类型数据成员都会保证获得一个初始值。可用下面这段小程序看到这些值:
```
//: InitialValues.java
// Shows default initial values
class Measurement {
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
void print() {
System.out.println(
"Data type Inital value\n" +
"boolean " + t + "\n" +
"char " + c + "\n" +
"byte " + b + "\n" +
"short " + s + "\n" +
"int " + i + "\n" +
"long " + l + "\n" +
"float " + f + "\n" +
"double " + d);
}
}
public class InitialValues {
public static void main(String[] args) {
Measurement d = new Measurement();
d.print();
/* In this case you could also say:
new Measurement().print();
*/
}
} ///:~
```
输入结果如下:
```
Data type Inital value
boolean false
char
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
```
其中,`Char`值为空(`NULL`),没有数据打印出来。
稍后大家就会看到:在一个类的内部定义一个对象引用时,如果不将其初始化成新对象,那个引用就会获得一个空值。
## 4.4.1 规定初始化
如果想自己为变量赋予一个初始值,又会发生什么情况呢?为达到这个目的,一个最直接的做法是在类内部定义变量的同时也为其赋值(注意在C++里不能这样做,尽管C++的新手们总“想”这样做)。在下面,`Measurement`类内部的字段定义已发生了变化,提供了初始值:
```
class Measurement {
boolean b = true;
char c = 'x';
byte B = 47;
short s = 0xff;
int i = 999;
long l = 1;
float f = 3.14f;
double d = 3.14159;
//. . .
```
亦可用相同的方法初始化非基本(主)类型的对象。若`Depth`是一个类,那么可象下面这样插入一个变量并进行初始化:
```
class Measurement {
Depth o = new Depth();
boolean b = true;
// . . .
```
若尚未为`o`指定一个初始值,同时不顾一切地提前试用它,就会得到一条运行期错误提示,告诉你产生了名为“异常”(`Exception`)的一个错误(在第9章详述)。
甚至可通过调用一个方法来提供初始值:
```
class CInit {
int i = f();
//...
}
```
当然,这个方法亦可使用参数,但那些参数不可是尚未初始化的其他类成员。因此,下面这样做是合法的:
```
class CInit {
int i = f();
int j = g(i);
//...
}
```
但下面这样做是非法的:
```
class CInit {
int j = g(i);
int i = f();
//...
}
```
这正是编译器对“向前引用”感到不适应的一个地方,因为它与初始化的顺序有关,而不是与程序的编译方式有关。
这种初始化方法非常简单和直观。它的一个限制是类型`Measurement`的每个对象都会获得相同的初始化值。有时,这正是我们希望的结果,但有时却需要盼望更大的灵活性。
## 4.4.2 构造器初始化
可考虑用构造器执行初始化进程。这样便可在编程时获得更大的灵活程度,因为我们可以在运行期调用方法和采取行动,从而“现场”决定初始化值。但要注意这样一件事情:不可妨碍自动初始化的进行,它在构造器进入之前就会发生。因此,假如使用下述代码:
```
class Counter {
int i;
Counter() { i = 7; }
// . . .
```
那么i首先会初始化成零,然后变成7。对于所有基本类型以及对象引用,这种情况都是成立的,其中包括在定义时已进行了明确初始化的那些一些。考虑到这个原因,编译器不会试着强迫我们在构造器任何特定的场所对元素进行初始化,或者在它们使用之前——初始化早已得到了保证(注释⑤)。
⑤:相反,C++有自己的“构造器初始模块列表”,能在进入构造器主体之前进行初始化,而且它对于对象来说是强制进行的。参见《Thinking in C++》。
(1) 初始化顺序
在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间,那些变量仍会在调用任何方法之前得到初始化——甚至在构造器调用之前。例如:
```
//: OrderOfInitialization.java
// Demonstrates initialization order.
// When the constructor is called, to create a
// Tag object, you'll see a message:
class Tag {
Tag(int marker) {
System.out.println("Tag(" + marker + ")");
}
}
class Card {
Tag t1 = new Tag(1); // Before constructor
Card() {
// Indicate we're in the constructor:
System.out.println("Card()");
t3 = new Tag(33); // Re-initialize t3
}
Tag t2 = new Tag(2); // After constructor
void f() {
System.out.println("f()");
}
Tag t3 = new Tag(3); // At end
}
public class OrderOfInitialization {
public static void main(String[] args) {
Card t = new Card();
t.f(); // Shows that construction is done
}
} ///:~
```
在`Card`中,`Tag`对象的定义故意到处散布,以证明它们全都会在构造器进入或者发生其他任何事情之前得到初始化。除此之外,`t3`在构造器内部得到了重新初始化。它的输入结果如下:
```
Tag(1)
Tag(2)
Tag(3)
Card()
Tag(33)
f()
```
因此,`t3`引用会被初始化两次,一次在构造器调用前,一次在调用期间(第一个对象会被丢弃,所以它后来可被当作垃圾收掉)。从表面看,这样做似乎效率低下,但它能保证正确的初始化——若定义了一个重载的构造器,它没有初始化`t3`;同时在`t3`的定义里并没有规定“默认”的初始化方式,那么会产生什么后果呢?
(2) 静态数据的初始化
若数据是静态的(`static`),那么同样的事情就会发生;如果它属于一个基本类型,而且未对其初始化,就会自动获得自己的标准基本类型初始值;如果它是指向一个对象的引用,那么除非新建一个对象,并将引用同它连接起来,否则就会得到一个空值(`NULL`)。
如果想在定义的同时进行初始化,采取的方法与非静态值表面看起来是相同的。但由于`static`值只有一个存储区域,所以无论创建多少个对象,都必然会遇到何时对那个存储区域进行初始化的问题。下面这个例子可将这个问题说更清楚一些:
```
//: StaticInitialization.java
// Specifying initial values in a
// class definition.
class Bowl {
Bowl(int marker) {
System.out.println("Bowl(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Table {
static Bowl b1 = new Bowl(1);
Table() {
System.out.println("Table()");
b2.f(1);
}
void f2(int marker) {
System.out.println("f2(" + marker + ")");
}
static Bowl b2 = new Bowl(2);
}
class Cupboard {
Bowl b3 = new Bowl(3);
static Bowl b4 = new Bowl(4);
Cupboard() {
System.out.println("Cupboard()");
b4.f(2);
}
void f3(int marker) {
System.out.println("f3(" + marker + ")");
}
static Bowl b5 = new Bowl(5);
}
public class StaticInitialization {
public static void main(String[] args) {
System.out.println(
"Creating new Cupboard() in main");
new Cupboard();
System.out.println(
"Creating new Cupboard() in main");
new Cupboard();
t2.f2(1);
t3.f3(1);
}
static Table t2 = new Table();
static Cupboard t3 = new Cupboard();
} ///:~
```
`Bowl`允许我们检查一个类的创建过程,而`Table`和`Cupboard`能创建散布于类定义中的`Bowl`的`static`成员。注意在`static`定义之前,`Cupboard`先创建了一个非`static`的`Bowl b3`。它的输出结果如下:
```
Bowl(1)
Bowl(2)
Table()
f(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f(2)
f2(1)
f3(1)
```
`static`初始化只有在必要的时候才会进行。如果不创建一个`Table`对象,而且永远都不引用`Table.b1`或`Table.b2`,那么`static Bowl b1`和`b2`永远都不会创建。然而,只有在创建了第一个`Table`对象之后(或者发生了第一次`static`访问),它们才会创建。在那以后,`static`对象不会重新初始化。
初始化的顺序是首先`static`(如果它们尚未由前一次对象创建过程初始化),接着是非`static`对象。大家可从输出结果中找到相应的证据。
在这里有必要总结一下对象的创建过程。请考虑一个名为`Dog`的类:
(1) 类型为`Dog`的一个对象首次创建时,或者`Dog`类的`static`方法/`static`字段首次访问时,Java解释器必须找到`Dog.class`(在事先设好的类路径里搜索)。
(2) 找到`Dog.class`后(它会创建一个`Class`对象,这将在后面学到),它的所有`static`初始化模块都会运行。因此,`static`初始化仅发生一次——在`Class`对象首次载入的时候。
(3) 创建一个`new Dog()`时,`Dog`对象的构建进程首先会在内存堆(Heap)里为一个`Dog`对象分配足够多的存储空间。
(4) 这种存储空间会清为零,将`Dog`中的所有基本类型设为它们的默认值(零用于数字,以及`boolean`和`char`的等价设定)。
(5) 进行字段定义时发生的所有初始化都会执行。
(6) 执行构造器。正如第6章将要讲到的那样,这实际可能要求进行相当多的操作,特别是在涉及继承的时候。
(3) 明确进行的静态初始化
Java允许我们将其他`static`初始化工作划分到类内一个特殊的“`static`构建从句”(有时也叫作“静态块”)里。它看起来象下面这个样子:
```
class Spoon {
static int i;
static {
i = 47;
}
// . . .
```
尽管看起来象个方法,但它实际只是一个`static`关键字,后面跟随一个方法主体。与其他`static`初始化一样,这段代码仅执行一次——首次生成那个类的一个对象时,或者首次访问属于那个类的一个`static`成员时(即便从未生成过那个类的对象)。例如:
```
//: ExplicitStatic.java
// Explicit static initialization
// with the "static" clause.
class Cup {
Cup(int marker) {
System.out.println("Cup(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Cups {
static Cup c1;
static Cup c2;
static {
c1 = new Cup(1);
c2 = new Cup(2);
}
Cups() {
System.out.println("Cups()");
}
}
public class ExplicitStatic {
public static void main(String[] args) {
System.out.println("Inside main()");
Cups.c1.f(99); // (1)
}
static Cups x = new Cups(); // (2)
static Cups y = new Cups(); // (2)
} ///:~
```
在标记为(1)的行内访问`static`对象`c1`的时候,或在行(1)标记为注释,同时(2)行不标记成注释的时候,用于`Cups`的`static`初始化模块就会运行。若(1)和(2)都被标记成注释,则用于`Cups`的`static`初始化进程永远不会发生。
(4) 非静态实例的初始化
针对每个对象的非静态变量的初始化,Java 1.1提供了一种类似的语法格式。下面是一个例子:
```
//: Mugs.java
// Java 1.1 "Instance Initialization"
class Mug {
Mug(int marker) {
System.out.println("Mug(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
public class Mugs {
Mug c1;
Mug c2;
{
c1 = new Mug(1);
c2 = new Mug(2);
System.out.println("c1 & c2 initialized");
}
Mugs() {
System.out.println("Mugs()");
}
public static void main(String[] args) {
System.out.println("Inside main()");
Mugs x = new Mugs();
}
} ///:~
```
大家可看到实例初始化从句:
```
{
c1 = new Mug(1);
c2 = new Mug(2);
System.out.println("c1 & c2 initialized");
}
```
它看起来与静态初始化从句极其相似,只是`static`关键字从里面消失了。为支持对“匿名内部类”的初始化(参见第7章),必须采用这一语法格式。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物