“初始化”和“清除”,许多 C程序的错误都是由于程序员忘记初始化一个变量造成的。清除是另一个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资源会一直保留下去,极易产生资源(主要是内存)用尽的后果。C++引入了“构建器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java 也沿用了这个概念,还新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。
这里笔者将记录初始化和清除的问题,主要是初始化内容,以下内容由参考原文和自己理解组成。
初始化:
一、
对于类的使用,可将其想象为自己写的每个类都要调用一次 initialize()。这个名字提醒我们在使用对象之前,应首先进行这样的调用。在 Java 中,提供了名为“构建器”特殊方法,可担保每个对象都会得到正确的初始化。
然后考虑如何命名这个“构建器”方法。C++采取的方案是最简单的,且更有逻辑性,所以也在Java 里得到了应用:构建器的名字与类名相同。这样一来,可保证这样的一个方法会在初始化期间自动调用。
例如:
class Rock {
Rock() { // This is the constructor
System.out.println("Creating Rock");
}
}
现在,一旦创建一个对象:
new Rock();
就会分配相应的存储空间,并调用构建器。这样可保证在我们经手之前,对象得到正确的初始化。请注意所有方法首字母小写的编码规则并不适用于构建器。这是由于构建器的名字必须与类名完全相同!和其他任何方法一样,构建器也能使用自变量,以便我们指定对象的具体创建方式。可非常方便地改动上述例子,以便构建器使用自己的自变量,如下:
class Rock {
Rock(int i) {
System.out.println("Creating Rock number " + i);
}
}
构建器属于一种较特殊的方法类型,因为它没有返回值。这与 void 返回值存在着明显的区别。对于void 返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。
注意,不能根据返回值重载(过载):
void f() {}
int f() {}
若编译器可根据上下文(语境)明确判断出含义,比如在 int x=f()中,那么这样做完全没有问题。然而,我们也可能调用一个方法,同时忽略返回值;因为我们关心的不是返回值,而是方法调用的其他效果。假如我们像下面这样调用方法:
f();
Java 怎样判断f()的具体调用方式呢?而且别人如何识别并理解代码呢?由于存在这一类的问题,所以不能根据返回值类型来区分过载的方法。
默认构建器是没有自变量的,它们的作用是创建一个“空对象”。若创建一个没有构建器的类,则编译程序会帮我们自动创建一个默认构建器。如这一行:
new Bird();
它的作用是新建一个对象,并调用默认构建器。然而,如果已经定义了一个构建器(无论是否有自变量),编译程序都不会帮我们自动生成一个。
二、
this关键字
如果有两个同类型的对象,分别叫作a 和b,那么您也许不知道如何为这两个对象同时调用一个 f()方法:
class Banana { void f(int i) { /* ... */ } }
Banana a = new Banana(), b = new Banana();
a.f(1);
b.f(1);
若只有一个名叫f()的方法,它怎样才能知道自己是被 a 还是被b 调用的呢?
编译器为我们完成了一些幕后工作。其中的秘密就是将准备操作的那个对象的句柄(引用)传递过去。所以前述的两个方法调用就变成了下面这样的形式:
Banana.f(a,1);
Banana.f(b,1);
这是内部的表达形式,通过它可理解幕后到底发生了什么事情。
假定我们在一个方法的内部,并希望获得当前对象的句柄(引用)。可使用关键字:this。this 关键字(注意只能在方法内部使用)可像其它对象句柄一样使用。假若准备从某个类的一个方法内,调用此类的另一个方法,就不必使用this:
class Apricot {
void pick() { /* ... */ }
void pit() { pick(); /* ... */ }
}
在pit()内部,我们可以说 this.pick(),但事实上无此必要。编译器能帮我们自动完成。
三、
这里再说说变量的初始化:
若将基本类型设为一个类的成员变量。由于任何方法都可以初始化或使用那个数据,所以在正式使用数据前,若强制让程序员将其初始化成一个适当的值,就可能不是一种有效的做法。因此,一个类的所有基本类型变量都应该获得一个初始值。
使用下面这段程序看到这些值:
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();
}
}
输入结果如下:
Data type Inital value
boolean false
char
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
其中,Char 值为空(NULL),没有数据打印出来。
在一个类的内部定义一个对象句柄时,如果不将其初始化成新对象,那个句柄就会获得一个空值。
四、
有继承关系的类的初始化:
class Art {
Art() {
System.out.println("Art constructor");
}
}
class Drawing extends Art {
Drawing() {
System.out.println("Drawing constructor");
}
}
public class Cartoon extends Drawing {
Cartoon() {
System.out.println("Cartoon constructor");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
}
该程序的输出显示了自动调用:Art constructor
Drawing constructor
Cartoon constructor
结论:基础类会在衍生类访问它之前得到正确的初始化。
五、
这里再记录一下类的相对复杂的初始化顺序:
在一个类里,初始化的顺序是由变量(成员变量)在类内的定义顺序决定的。即使变量定义遍布于方法定义的中间,那些变量仍会在调用任何方法之前得到初始化,包括在构建器调用之前。
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(); }
}
结果为:
Tag(1)
Tag(2)
Tag(3)
Card()
Tag(33)
f()
在Card 中,Tag 对象的定义故意放置在构造器前后,以证明它们都会在构建器进入或者发生其他任何事情之前得到初始化。除此之外,t3 在构建器内部得到了重新初始化。
然后加入静态变量的执行demo如下:
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(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(如果它们尚未由前一次对象创建过程初始化),接着是非static 对象。最后再总结一下对象的创建过程。请考虑一个名为 Dog的类:
(1) 类型为 Dog的一个对象首次创建(new)时,或者Dog 类的static方法/static 字段首次访问时,Java 解释器必须找到Dog.class。
(2) 找到Dog.class 后(它会创建一个 Class对象),它的所有 static初始化模块都会运行。因此,static初始化仅发生一次——在 Class 对象首次载入的时候。
(3) 创建一个new Dog()时,Dog 对象的构建进程首先会在内存堆(Heap)里为一个 Dog对象分配足够多的存储空间。
(4) 将Dog中的所有基本类型设为它们的默认值。
(5) 进行字段定义时发生的所有初始化都会执行。
(6) 执行构建器。
最后一个例子如下:
class Insect {
int i = 9;
int j;
Insect() {
prt("i = " + i + ", j = " + j);
j = 39;
}
static int x1 =
prt("static Insect.x1 initialized");
static int prt(String s) {
System.out.println(s);
return 47;
}
}
public class Beetle extends Insect {
int k = prt("Beetle.k initialized");
Beetle() {
prt("k = " + k);
prt("j = " + j);
}
static int x2 =
prt("static Beetle.x2 initialized");
static int prt(String s) {
System.out.println(s);
return 63;
}
public static void main(String[] args) {
prt("Beetle constructor");
Beetle b = new Beetle();
}
}
该程序的输出如下:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 63
j = 39
1、Beetle 运行时,第一件事是装载,在装载过程中,发现它有一个基础类(即extends 关键字要表达的意思),然后随之将其载入。无论是否准备生成那个基础类的一个对象,这个过程都会发生。
所以先执行Insect类。
2、接下来,会在基础类(此时是Insect)执行 static 初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类的初始化可能要依赖于对基础类成员的正确初始化。(《JAVA解惑》中有不少因初始化问题引发的错误案例)
所里这里先初始化Insect内的x1,x1调用prt()打印得到static Insect.x1 initialized,并且x1值为47
然后初始化Beetle,执行静态变量,执行Beetle的prt()打印得到static Beetle.x initialized
接着main调用Beetle的prt()打印得到Beetle constructor
接着new对象,同理必须先初始化父类(例如成员变量,构造方法),于是得到 i = 9, j = 0,之后j在构造方法内被赋值为39
接着初始化子类,初始化Beetle的k成员变量时调用prt(),打印Beetle.k initialized,k赋值为63
最后Beetle的构造方法打印子类的k=63,打印父类的j=39
补充:
Java 允许我们将static初始化工作划分到类内一个特殊的“static 构建从句”(也叫作“静态
块”)里。如下:
class Spoon {
static int i;
static {
i = 47;
}
这不是方法,与其他 static初始化一样,这段代码仅执行一次——首次生成那个类的一个对象时,或者首次访问属于那个类的一个 static 成员时(即便从未生成过那个类的对象)。清除:
为清除一个对象,使用对象的用户必须在希望进行清除的地点调用一个清除方法。若程序员忘记了,那么永远不会调用破坏器,我们最终得到的将是一个内存“漏洞”。
而java没有类似于c++的delete方法,也没有c++的破坏器,而做到简单代替的产物是垃圾收集机制,它能自动的销毁垃圾,但深入了解后它也不能完全代替破坏器机制提供的所有需求。
对于垃圾收集器的具体工作流程这里不做描述。