五、初始化与清理
1.用构造器确保初始化
初始化和清理正是涉及安全的两个问题,C++引入了构造器(constructor)的概念,这是一个在创建对象时被自动调用的特殊方法。Java中也采用了构造器,并额外提供了“垃圾回收器”。对于不在使用的内部资源,垃圾回收器能自动将其释放。
在Java中,通过提供构造器,类的设计者可能确保每个对象都会得到初始化。创建对象时,如果其类有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。
那么如何命名这个构造器方法呢?有两个问题:第一,所取的任何名字都有可能与类的某个成员名称相冲突;第二,调用构造器是编译器的责任,所以必须让编译器知道调用哪个方法。Java中采用了与C++相同的解决方案:即构造器采用与类相同的名称。请注意:由于构造器的名称必须与类名完全相同,所以“每个方法首字母小写”的编码风格并不适用于构造器。
不接受任何参数的构造器叫做默认构造器,Java文档中通常适用术语无参构造器。和其他方法一样,构造器也能带有形式参数,以便制定如何创建对象。有了构造器形式参数,就可以在初始化对象时提供实际参数。
构造器有助于减少错误,并使代码更已阅读。从概念上将,“初始化”与“创建”是彼此独立的,然而在代码中,你却找不到initialize()方法的明确调用。在Java中“初始化”和“创建”捆绑在一起,两者不能分离。构造器是一种特殊类型的方法,因为它没有返回值。这与返回值为空(void)明显不同。
2.方法重载
2.1 需要方法重载的原因
将人类语言中存在细微差别的概念“映射”到程序设计语言中时,相同的词可以表达多种不同的含义——它们被“重载”了。在Java(和C++)里,构造函数是强制重载方法名的一个重要原因。为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载;同时,尽管方法重载是构造器所必需的,但它亦可应用于其他方法,其用法同样方面。要是对冥想相同的概念使用了不同的名字,那一定会让人很纳闷。好在有了方法重载,可以为两者使用相同的名字。
2.2 区分重载的方法
要是几个方法有相同的名字,Java如何才能知道你指的是哪一个呢?其实规则很简单:每个重载的方法必须有一个独一无二的参数类型类表,甚至参数顺序不同也足以区分两个方法。不过一般情况下别这么做,因为这会使代码难以维护。
2.3 涉及基本类型的重载
由第二章我们知道,基本类型能从一个“较小”的类型自动提升至一个“较大”的类型,此过程一旦涉及到重载,可能会造成一些混淆。下面是这种情况下的两个要点:
1)如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型,就会被提升。一般顺序为byte -> short -> int。char型略有不同,如果无法找到恰好接受char参数的方法,就会把char直接提升为int。
2)如果传入的实际参数较大,就得通过类型转换来执行窄化转换。如果不这样做,编译器就会报错。
2.3 以返回值区分重载方法
读者可能会想:“在区分重载方法的时候,为什么只能以类名和方法的形参列表作为标准呢?能否考虑用方法的返回值来区分呢?比如下面两个方法,虽然它们有相同的名字和形式参数,但却很容易区分它们:
void f(){};
int f(){ return 1}
只要编译器可以根据语境明确判断出语义,比如在int x = f()中,那么的确可以据此区分重载方法。不过有时你并不关心方法的返回值,你想要的是方法调用的其他效果(这常被称为”为了副作用而调用“),这时你可能会调用方法而忽略其返回值。所以,如果像下面这样调用方法:
f();
此时Java如何才能判断该调用哪一个f()呢?因此,根据方法的返回值来区分重载方法是行不通的。
3.默认构造器
如前所述,默认构造器(又名”无参“构造器)是没有形式参数的——它的作用是创建一个”默认对象“。如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。如果已经定义了一个构造器(无论是否有参数),编译器就会帮你自动创建默认构造器。这就好比,要是你没有提供任何构造器,编译器就会认为”你需要一个构造器,让我给你制造一个吧;但假如你已写了一个构造器,编译器则会认为“啊,你已写了一个构造器,所以你知道你在做什么;你是刻意省略了默认构造器。”
4. this关键字
4.1
请看如下代码:
class Banana {
void peel (int i) {/*...*/}
public class BananaPeel {
public static void main (String [] args) {
Banana a = new Banana(), b = new Banana();
a.peel(1);
b.peel(2);
}
}
}
这里只有一个peel()方法,它如;何知道是被a还是被b所调用呢?为了能用简便、面向对象的语法来编写代码——即“发送消息给对象”,编译器做了一些幕后工作。它暗自把“所操作对象的引用”作为第一个参数传递给peel()。所以上述两个方法的调用就变成了这样:Banana.peel(a,1),Banana.peel(b,2);这是内部的表示形式我们不能这样书写代码,并试图通过编译;但这种写法的确能帮你了解实际所发生的事情。
假设你希望在方法内部获得对当前对象的引用。由于这个引用是编译器“偷偷”传入的,所以没有标识符可用。但是,为此有个专门的关键字:this。this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同。但要注意,如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。当前方法中的this引用会自动应用于同一个类中的其他方法。
4.2 在构造器中调用构造器
可能为一个类写了多个构造器,有时可能想在一个构造器中调用另一个构造器,以避免重复代码。可用this关键字做到这一点。通常写this的时候,都是指“这个对象”或者“当前对象”,而且它本身表示对当前对象的引用。在构造器中,如果为this添加了参数列表,那么就有了不同的含义。这将残生对符合此参数列表的某个构造器的明确调用;这样,调用其他构造器就有了直接的途径。但请注意:机关可以用this调用一个构造器,但却不能调用两个,此外,必须将构造器调用置于最起始处,否则编译器会报错。
小结:以上内容其实就是Java中关于this的用法,为了彻底理解到本质,这里摘录了书中的较多解释性文字。现将Java中this关键字的用法总结如下:
1)this关键字只能在方法内部使用,表示“调用方法的那个对象”的引用。(此处可以解决方法形式参数名与某些属性名重名的情况,构造方法经常这样使用,相信大家都清楚)
2)在构造方法内部使用this+参数列表,用来调用类中的其他构造方法。
4.3 static的含义
了解this关键字之后,就能更全面地理解理解static(静态)方法的含义。static方法就是没有this的方法。在static方法的内部不能调用非静态方法,反过来倒是可以的。而且在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。它很想全局方法。Java中禁止使用全局方法,但你在类中置入static方法就可以访问其他static方法和static域。
有些人认为static方法不是“面向对象”的,因为他们的确具有全局函数的语义;使用static方法时,由于不存在this,所以不是通过“向对象发送消息”的方式来完成的。的确,要是在代码中出现了大量的static方法,就该重新考虑自己的设计了。
5.清理:终结处理和垃圾回收
Java有垃圾回收器负责回收无用对象占据内存资源。但也有特殊情况:嘉定你的对象(并非食用new)获得了一块“特殊”的内存区域,由于来及回收期只知道释放那些经由new分配的内存,所以它不知道如何释放该对象的这块“特殊”内存。为了应对这种情况,Java允许在类中定义一个名为finalize()的方法。它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以要是你打算用finalize(),就能在垃圾回收时刻做一些重要的清理工作。
这里有一个潜在的编程陷阱,因为有些程序员(特别是C++程序员)刚开始可能会误把finalize()方法当做C++中的析构函数。所以有必要明确区分一下:在C++中,对象一定会被销毁(如果程序中没有缺陷的话);而Java里的对象却并非总是被垃圾回收。或者换句话说:
1)对象可能不被垃圾回收
2)垃圾回收并不等于“析构”
3)垃圾回收只与内存有关。
这意味着在你不再需要某个对象之前,如果必须执行某些动作,那么你得自己去做。也许你会发现,只要程序没有濒临存储空间用完的那一刻,对象占用的控件就总也得不到释放。如果程序执行结束,并且垃圾回收器一直都没有释放你创建的任何对象的存储空间,则随着程序的退出,那些资源也会全部交换个操作系统。这个策略是恰当的,因为垃圾回收本身也有开销,要是不是用它,他就不用支付这部分开销了。
5.1 finalize()的用途何在
此时读者已经明白了不该将finalize()作为通用的清理方法。那么,finalize()的真正用途是什么呢?由于垃圾回收只与内存有关,也就是说,使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是finalize()方法),他们也必须通内存及其回收有关。但这是否一位置要是对象中含有其他对象,finalize()就应该明确释放那些对象呢?不,无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。这就将对finalize()的需求先知道一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间。不过,读者也看到了,Java中一切皆为对象,那这种特殊情况是怎么回事呢?
看来之所以要有finalize(),是由于在分配内存时可能采用了类似C语言中的做法,而非Java中的通常做法。这种情况主要发生在使用“本地方法”的情况下,本地方法是一种在Java中调用非Java代码的方式。本地方法目前只支持C和C++,但他们可以调用其他语言的代码,所以实际上可以调用任何代码。在非Java代码中,也许会调用C的malloc()函数系列来分配存储空间,而且除非调用了free()函数,否则存储空间将得不到释放,从而造成了内存泄露。当然,free()是C和C++中的函数,所以需要在finalize()中用本地方法调用它。至此,读者或许已经明白了不要过多地使用finalize()的道理了。对,它确实不是进行普通的清理工作的合适场所。那么,普通的清理工作应该在哪里执行呢?
5.2 你必须试试清理
Java不允许创建局部对象,必须使用new创建对象。如果希望进行除释放存储空间之外的清理工作,哎是得明确调用某个恰当的Java方法。这就等同于使用析构函数了,只是没有它方便。
记住:无论是“垃圾回收”还是“终结”,都不保证一定会发生。如果Java虚拟机并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以回复内存的。
5.3 终结条件
通常,不能指望finalize(),必须创建其他“清理”方法,并且明确地调用它们。看来,finalize()只能存在于程序员很难用大的一些晦涩用法里了。不过,finalize()还有一个有趣的用法,它并不依赖于每次都要对finalize()进行调用,这就是对象终结条件的验证。
当对某个对象不再感兴趣——也就是说它可以被清理了,这个对象应该处于某种状态,使它占用的内存可以被安全地释放。例如,要是对象代表了一个打开的文件,在对象被回收之前,程序员应该关闭这个文件。只要对象中存在没有被适当清理的部分,程序就存在很隐晦的缺陷,finalize()可以用来最终发现这种情况——尽管他并不总是会被调用。如果某次finalize()的动作使得缺陷发现,那么就可据此找出问题所在——这才是人们真正关心的。
5.4 垃圾回收器如何工作
关于这个问题,文章中以比喻的形式,比较了C++和Java的内存分配情况,然后介绍了几种理论上的垃圾清理方式,进而阐述了Java虚拟机垃圾回收器是如何工作的。这似乎还会因虚拟机的不同而有不同的工作方式。篇幅较长而内容经典较多,因此,在此不作摘录。
6.成员初始化
Java尽力保证:所有变量在使用之前都能得到恰当的初始化。对于方法的局部变量,Java以编译时错误的形式来贯彻着这种保证。要是类的数据成员(即字段)是基本类型,类的每个基本类型数据成员都会有一个初始值。在类里定义一个对象引用时,如果不将其初始化,此引用就会获得一个特殊值null。
如果想为某个变量赋初值,有两种方法:
1)在定义类成员变量的地方为其赋初值(注意在C++里不能这样做,尽管C++的新手们总想这样做)。
2)利用构造器进行初始化(接下来的内容)
7.构造器初始化
可以用构造器来进行初始化。在运行时刻,可以调用方法或执行某些动作来确定初值,这为编程带来了更大的灵活性。但要牢记:无法阻止自动初始化的进行,它将在构造器被调用之前发生。对于所有基本类型和对象引用,包括在定义时已经指定初值的变量,这种情况都是成立的。
7.1 初始化顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,他们仍旧会在任何方法(包括构造器)被调用之前得到初始化。
7.2 静态数据的初始化
无论创建多少个对象,静态数据都只占用一份存储区域。static关机子不能应用于局部变量,因此它只能作用于域。如果一个域是静态的基本类型域,且也没有对他进行初始化,那么它就会获得基本类型的标准初值;如果它是一个对象引用,那么他的默认初始化值就是null。但请注意:静态初始化只有在必要时刻才会进行。只有在第一个对象被创建(或者第一次访问静态数据)的时候,它们才会被初始化。
7.3 显示的静态初始化
Java允许将多个静态初始化动作组织成一个特殊的“静态子句”(有时也叫做“静态块”)。例如:
public class Spoon {
static int i;
static {
i = 47;
}
}
尽管上面的代码看起来像个方法,但它实际上只是一段跟在static关键字后面的代码。与其他静态初始化董总一样,这段代码仅执行一次:当首次生成这个类的一个对象时,或者首次访问属于哪个类的静态数据成员时(即从未生成过哪个类的对象)。
7.4 非静态实例初始化
Java中也有被称为实例初始化的类似语法,用来初始化每一个对象的非静态变量。它看起来与静态初始化子句一模一样,只不过少了static关键字。这种语法对于支持“匿名内部类”的初始化是必须的,但是它也使得你可以保证
无论调用了那个现实的构造器,某些操作都会发生。实例初始化子句是在两个构造器之前执行的。
8.数组初始化
8.1 数组简介
数组只是相同类型的、用一个标识符封装到一起的一个对象序列或基本类型数据序列。编译器不允许指定数组的大小。这又把我们带回到有关“引用”的问题上。int []a,现在拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),而且也没给数组对象本身分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一堆花括号括起来的值组成的。在这种情况下,存储空间的分配(等价于使用new)将由编译器负责。
所有数组(无论他们的元素是对象还是基本类型)都有一个固有成员,可以通过它获知数组内包含了多少个元素,但不能对其修改。这个成员就是length。与C和C++类似,Java数组计数也是从第0个元素开始的,所以能使用的最大下标数是length-1.要是超出这个边界,C和C++会“默默”地接收,并允许你访问所有内存。Java则能保护你免受这一问题的困扰,一旦访问下标过界,就会出现运行时错误(即异常)。
8.2 可变参数列表
在Java SE5中,添加了可变参数列表的支持。有了可变参数,就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你去填充数组。你获取的仍旧是一个数组。但是,这不仅仅只是从元素列表到数组的自动转换,如果你有一组事务,可以把它们当做列表传递,而如果你已经有了一个数组,该方法就可以吧他们当做可变参数列表来接受。将0个参数传递给可变参数列表是可行的,当具有可选的尾随参数时,这一特性就会很有用。
小结:以上内容主要讲述了Java中,对象的初始化顺序以及一些特殊的初始化方式。下面总结一下对象的创建过程,假设有个名为Dog的类:
1)即使没有显式地实用static关键字,构造器实际上也是静态的方法。因此,当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,一定为Dog.class文件
2)然后载入Dog.class文件(后面会学到,这将创建一个Class对象),有缘静态初始化的所有动作都会执行,因此,静态初始化只在Class对象首次加载的时候进行一次
3)当用new Dog()创建对象的时候,首先将在堆上位Dog对象分配足够的存储空间。
4)这块存储空间会被清零,这就自动地将Dog对象中的所有基本类型数据都设置成了默认值(对数字来说就是0,对布尔型和字符型也相同),而引用则被设置成了null
5)执行所有出现于字段定义出的初始化动作
6)执行构造器。正如第7章所看到的,这可能会牵涉到很多动作,尤其是设计集成的时候。
9.枚举类型
在Java SE5中添加了一个看似很小的特性,即enum关键字,它使得我们在需要群组并使用枚举型集时,可以很方便地处理。在此之前,你需要创建一个整型常量集,但是这些枚举值并不会必然地江其自身的取值限制在这个常量集的范围之内,因此它们显得更有风险,且更难使用。
当你创建enum时,编译器会自动添加一些有用的特性。例如,它会创建toString()方法,以便你可以很方便地显示某个enum实例的名字;编译器还会创建ordinal()方法,用来表示某个特定enum常量的声明顺序,以及static values()方法,用来按照enum敞亮的声明顺序,产生由这些常量构成的数组。
尽管enum看起来像是一种新的数据类型,但是这个关键字只是为enum生成对应的类时,产生了某些编译行为,因此在很大程度上,你可以将enum当做其他任何类来处理。事实上,enum确实是类,并且具有自己的方法。
enum有一个特别实用的特性,即它可以在switch语句内实用。
Java程序初始化的顺序
java程序初始化工作可以在许多不同的代码块中来完成(例如:静态代码块、构造函数等),他们执行的顺序如下:
- 父类静态变量
- 父类静态代码块
- 子类静态变量
- 子类静态代码块
- 父类非静态变量
- 父类非静态代码块
- 父类构造器
- 子类非静态变量
- 子类非静态代码块
- 子类构造器
public class Base {
static{
System.out.println("Base static block");
}
{
System.out.println("Base Block");
}
public Base(){
System.out.println("Base Constructor");
}
}
public class Derived extends Base {
static {
System.out.println("Derived static block");
}
{
System.out.println("Derived block");
}
public Derived(){
System.out.println("Derived constructor");
}
public static void main(String[] args) {
new Derived();
}
}
执行Derived类,可以看到运行结果:
Base static block
Derived static block
Base Block
Base Constructor
Derived block
Derived constructor
Java程序初始化一般遵循3个原则
- 静态对象(变量)由于非静态对象(变量)初始化。其中静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化很多次
- 父类优先于子类进行初始化
- 按照成员变量的定义顺序进行初始化。即使变量定义散布于方法之中,他们依然在任何方法(包括构造函数)被调用前先初始化
所以,知道怎么优先于main方法,执行" Hello World"么
public class Test {
static{
System.out.println("Hello World");
}
public static void main(String[] args) {
System.out.println("Hello Main Fun");
}
}
程序输出如下:
Hello World
Hello Main Fun