第五章 初始化与清理
5.6 成员初始化
Java尽力保证:所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,Java以编译错误的形式来保证。如下:
void f() {
int i;
i++; //Error:i not initialized
}
会得到一条错误的消息,提示i可能没有初始化。编译器可以给i赋初值,但是并没有这么做,因为没有初始化是程序员的疏忽,为了保证程序的稳定性,能帮助程序员找出程序的缺陷所在。
但如果是类的数据成员是基本类型,类的每个基本类型成员变量都会保证有一个初值。如下:
public class Test {
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
InitialValues reference;
}
上面的一系列数值分别为:false, (空白), 0, 0, 0, 0, 0.0, 0.0, null
。char值为0,所以显示为空白。
5.6.1 指定初始化
有一种很直接的方法给某个变量赋初值,就是在定义类成员变量的地方直接为其赋值,C++中不允许。对初始化非基本类型的对象也可以,例如下面的A类的对象,这样类Test的每个对象都会具有相同的初始值。如下:
class A{
}
public class Test {
boolean bool = true;
char ch = ‘x‘;
int i = 99;
A a = new A();
}
5.7 构造器初始化
还可以用构造器来进行初始化。在运行时刻,可以调用方法或执行某些动作来确定初值,这给编程带来了更大的灵活性。但是要记住,无法组织自动初始化的进行,也就是前面提到的编译器自动赋值,这个工作在构造器被调用之前就发生。例如:
public class Test {
int i;
Test() {i = 7;}
}
i的值首先被置为0,再被赋值为7。
5.7.1 初始化顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布与方法定义之间,它们人就会在任何方法(包括构造器)被调用之前得到初始化。例如:
class Window {
Window(int maker) {
System.out.println("Window(" + maker + ")");
}
}
class House {
Window w1 = new Window(1);
House() {
System.out.println("House");
w3 = new Window(33);
}
Window w2 = new WIndow(2);
void f() {
System.out.println("f()");
Window w3 = new Window(3);
}
}
public class Test {
public static void main(String[] args) {
House h = new House();
h.f();
}
}
结果为Window(1) Window(2) Window(3) House() Window(33) f()
。
5.7.2 静态数据的初始化
无论创建多少个对象,静态数据都至占用一份存储区域。static关键字不能应用于局部变量。看下面的例子:
class Bowl {
Bowl(int marker) {
System.out.println("Bowl(" + marker + ")");
}
void f1(int marker) {
System.out.println("f1(" + marker + ")");
}
}
class Table {
static Bowl bowl1 = new Bowl(1);
Table() {
System.out.println("Table()");
bowl2.f1(1);
void f2(int marker) {
System.out.println("f2(" + marker + ")");
}
static Bowl bowl2 = new Bowl(2);
}
class Cupboard {
Bowl bowl3 = new Bowl(3);
static Bowl bowl4 = new Bowl(4);
Cupboard() {
System.out.println("Cupboard()");
bowl4.f1(2);
}
void f3(int marker) {
System.out.println("f3(" + marker + ")");
}
static Bowl bowl5 = new Bowl(5);
}
}
public class Test {
public static void main(String[] args){
System.out.println("Creating new Cupboard() in main");
new Cupboard();
Syste.out.println("Creating new Cupboard() in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
static Table table = new Table();
static Cupboard cupboard = new Cupbpard();
}
输出的结果依次为:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
静态初始化只有在必要的时候才会进行,只有在第一个类对象被创建(或第一次访问静态数据)的时候,静态数据才会被初始化。之后,无论怎么创建对象,都不会再次被初始化。
初始化的顺序是先静态对象(如果它们尚未被初始化),而后是”非静态对象”。
总结一下对象的创建过程,假设有个名为Dog的类:
- 即使没有显式的使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog对象时(构造器可以看成静态方法),或者Dog类的静态方法、静态域首次被访问的时候,Java解释器必须查找类的路径,以定位Dog.class。
- 然后载入Dog.class,有关静态初始化的所有动作都会被执行,因此,静态初始化只在Class对象首次加载的时候进行一次。
- 当用new创建Dog的对象时候,首先在堆上给Dog对象分配足够的存储空间。
- 清零所分配的存储空间,这就自动的将Dog对象中所有的基本数据类型设置成了默认值(对数字来说就是0,对boolean和char类型的来说也类似),而引用则都被设置成了null。
- 执行所有出现于字段定时的初始化动作。
- 执行构造器方法。
5.7.3 显示的静态初始化
Java允许多个静态初始化动作组织成一个特殊的”静态子句”(有时也叫做”静态块”)。就像下面这样:
public class Spoon {
static int i;
static {
i = 47;
}
}
和其他静态初始化动作一样,这段代码只执行一次:当首次生成这个类的对象时,或首次访问属于这个类的静态成员数据的时候执行。例如:
class Cup {
Cup(int marker) {
System.out.println("Cup(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Cups {
static Cup cup1;
static Cup cup2;
static {
cup1 = new Cup(1);
cup2 = new Cup(2);
}
Cups() {
System.out.println("Cups()");
}
}
public class Test {
public static void main(String[] args) {
System.out.println("Inside main()");
Cups.cup1.f(99); // (1)
}
//static Cups cups1 = new Cups(); // (2)
//static Cups cup2 = new Cups(); // (2)
}
上面的结果为:Inside main() Cup(1) Cup(2) f(99)
5.7.4 非静态实例初始化
Java中也有被成为实例初始化的类似语法,用来初始化每一个对象的非静态变量。和静态语句块一样的,只不过少了static。如果不创建类的实例,非静态语句块是不会被执行,只会触碰static变量和语句块。
下面用一个例子来总结下上述的顺序:
class Cup {
{
System.out.println("Block - Cup");
}
static int c;
static {
c = 1;
System.out.println("Static Bolck - Cup");
}
Cup(int marker) {
System.out.println("Construct - Cup(" + marker + ")");
}
void f(int marker) {
System.out.println("Function - f(" + marker + ")");
}
}
class Cups {
static {
cup1 = new Cup(1);
cup2 = new Cup(2);
}
static Cup cup1;
static Cup cup2;
{
System.out.println("Block - Cups");
}
Cups() {
System.out.println("Construct - Cups()");
}
}
public class JavaTest {
public static void main(String[] args) {
System.out.println("Inside main()");
Cups.cup1.f(99); // (1)
}
}
输出的结果为:
Inside main()
Static Bolck - Cup
Block - Cup
Construct - Cup(1)
Block - Cup
Construct - Cup(2)
Function - f(99)
从上面的结果可以看出,没有新建Cups类的对象时,不会执行非静态语句块,也就是被{}包括起来的语句块。在第一次创建类对象或者使用到类的静态变量的时候,就会将.class文件加载进来,初始化static变量,执行static{}语句块。
5.8 数组初始化
数组只是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。素组是通过方括号下标操作符【】来定义和使用的。定义数组只需要在类型名后面加上一对方括号:int[] a1;
。方括号也可以放在后面:int a1[];
两种格式的含义是一样的,后面一种格式符合C和C++程序员的习惯。前面一种格式能更直观的看出,其表明的类型是”一个int型数组”。
编译器不允许指定数组的大小,现在拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),而且也没给数组对象本身分配任何空间。为了给数组创建相应的存储空间,需要对数组进行初始化。数组的初始化代码可以出现在代码的任何地方,但也可以使用一种特殊的初始化表达式,必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的,存储空间的分配(等价于使用new)将由编译器负责。例如:
int[] a1 = {1,2,3,4,5};
那么为什么还要在没有数组的时候定义一个数组的引用呢?Java中可以将数组赋值给另一个数组,int[] a2;
,在Java中可以将一个数组赋值给另一个数组,所以可以这样:a2 = a1;直接复制一个引用。下面的例子:
public class ArrayTest {
public static void main(String[] args) {
int[] a1 = {1,2,3,4,5};
int[] a2;
a2 = a1;
for(int i = 0;i < a2.length; i++)
a2[i] = a2[i] + 1;
for(int i = 0;i < a1.length; i++)
System.out.println("a1[" + i + "]" + a[i]);
}
}
输出为a1[0]=1 a1[1]=2 a1[2]=3 a1[3]=4 a1[4]=5
所有数组(无论元素始对象还是基本类型)都有一个固有成员length,可以通过它获得数组长度,但其不能直接被更改。和C与C++类似,Java数组计数从0开始,数组越界,C和C++默默接受,但Java直接出现运行时错误。
可以在编程时,通过new再数组里面创建元素。尽管创建的是基本类型数组,new仍然可以工作(不能用new创建单个的基本类型数据)。
public class ArrayNew {
public static void main(String[] args) {
int[] a;
Random rand = new Random(47);
a = new int[rand.nextInt(20)];
}
}
如果创建了一个非基本类型的数组,那么就是一个引用数组。以整型的包装器类Integer为例:
public class Test {
public static void main(String[] args) {
Random rand = new Random(47);
Integer[] a = new Integer[rand.nextInt(20)];
}
}
这里即便用new创建了数组之后,也只是一个引用数组,并且直到通过创建新的Integer对象,并把对象和引用连接起来,初始化才算结束。如果忘记了创建对象,并链接对象和引用,那数组中就全是空引用,运行时会产生异常。
5.8.1 可变参数列表
可变参数列表可用于参数个数或者类型未知的场合。例如void f(Object[] args)
函数里的参数。这种在Java SE5之前出现,然而再Java SE5中,添加入了新特性,可以使用新特性来定义可变参数列表了,下面的例子:
public class NewVarArgs {
static void printArray(Object... args) {
for(Object obj : args) {
System.out.println(obj + " ");
}
}
public static void main(String[] args) {
printArray(new Integer(47), new Float(3.14), new Double(11.11));
printArray(47, 3.14F, 11.11);
printArray("one", "two", "three");
}
}
有了可变参数,就不用显示的编写数组语法了,当指定参数的时候,编译器实际上会去填充数组,最后得到的仍然是一个数组。
5.9 枚举类型
Java SE5中添加了一个看似很小的特性,即enum关键字,它使得我们在需要群组并使用枚举集时可以很方便的处理。C和C++以及以其他很多语言已经拥有枚举类型了,Java中枚举类型功能比C/C++的功能更加完备。下面是简单的示例:
public enum A {
NOT, MILD, MEDIUM, HOT, FLAMING
}
这里创建了一个名为A的枚举类型,它具有5种值,由于枚举类型的实例是常量,因此按照命名习惯通常用大写字母表示(有多个字母用下划线隔开)。
为了使用enum,需要创建一个该类型的引用,并和某个实例连接起来。
public class Test {
public static void main(String[] args) {
A a = A.MEDUIM;
System.out.println(a);
}
}
在创建枚举类型的时候,编译器会自动添加一些特性。例如:
- 会创建toString()方法,一边可以很方便的显示某个enum实例的名字。
- 创建ordinal()方法,用来表示某个特定enum常量的声明顺序。
- static values()方法,用来按照enum常量的声明顺序,产生由这些常量值构成的数组。
例子如下:
public class EnumOrder {
public static void main(String[] args){
for(A a : A.values) {
System.out.println(s + ", oridinal " + a.ordinal());
}
}
}
输出结果为:
NOT, oridinal 0
MILD, oridinal 1
MEDIUM, oridinal 2
HOT, oridinal 3
FLAMING, oridinal 4
enum的另一个特别实用的特性是能和switch语句一起使用。看下面的例子:
enum Pet {
Cat,
Dog,
Bird
}
public class JavaTest {
Pet pet;
public JavaTest(Pet p) {
pet = p;
}
public void describe() {
switch(pet) {
case Cat :
System.out.println("The pet is Cat");
break;
case Dog :
System.out.println("The pet is Dog");
break;
case Bird :
System.out.println("The pet is Bird");
break;
}
}
public static void main(String[] args) {
Pet p1 = Pet.Bird;
JavaTest test = new JavaTest(p1);
test.describe();
}
}
结果为:The pet is Bird