第4章初始化和清除“随着计算机的进步,‘不安全’的程序设计已成为造成编程代价高昂的罪魁祸首之一。”“初始化”和“清除”是这些安全问题的其中两个。许多C程序的错误都是由于程序员忘记初始化一个变量造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资源会一直保留下去,极易产生资源(主要是内存)用尽的后果。C++为我们引入了“构建器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java也沿用了这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。本章将讨论初始化和清除的问题,以及Java如何提供它们的支持。4.1用构建器自动初始化对于方法的创建,可将其想象成为自己写的每个类都调用一次initialize()。这个名字提醒我们在使用对象之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在Java中,由于提供了名为“构建器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构建器,那么在创建对象时,Java会自动调用那个构建器——甚至在用户毫不知觉的情况下。所以说这是可以担保的!接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某个类成员使用的名字冲突。第二是由于编译器的责任是调用构建器,所以它必须知道要调用是哪个方法。C++采取的方案看来是最简单的,且更有逻辑性,所以也在Java里得到了应用:构建器的名字与类名相同。这样一来,可保证象这样的一个方法会在初始化期间自动调用。下面是带有构建器的一个简单的类(若执行这个程序有问题,请参考第3章的“赋值”小节)。148-149页程序//:c04:SimpleConstructor.java//Demonstrationofasimpleconstructor.classRock{Rock(){//ThisistheconstructorSystem.out.println(CreatingRock);}}publicclassSimpleConstructor{publicstaticvoidmain(String[]args){for(inti=0;i10;i++)newRock();}}///:~现在,一旦创建一个对象:newRock();就会分配相应的存储空间,并调用构建器。这样可保证在我们经手之前,对象得到正确的初始化。请注意所有方法首字母小写的编码规则并不适用于构建器。这是由于构建器的名字必须与类名完全相同!和其他任何方法一样,构建器也能使用自变量,以便我们指定对象的具体创建方式。可非常方便地改动上述例子,以便构建器使用自己的自变量。如下所示:149页中程序//:c04:SimpleConstructor2.java//Constructorscanhavearguments.classRock2{Rock2(inti){System.out.println(CreatingRocknumber+i);}}publicclassSimpleConstructor2{publicstaticvoidmain(String[]args){for(inti=0;i10;i++)newRock2(i);}}///:~利用构建器的自变量,我们可为一个对象的初始化设定相应的参数。举个例子来说,假设类Tree有一个构建器,它用一个整数自变量标记树的高度,那么就可以象下面这样创建一个Tree对象:treet=newTree(12);//12英尺高的树若Tree(int)是我们唯一的构建器,那么编译器不会允许我们以其他任何方式创建一个Tree对象。构建器有助于消除大量涉及类的问题,并使代码更易阅读。例如在前述的代码段中,我们并未看到对initialize()方法的明确调用——那些方法在概念上独立于定义内容。在Java中,定义和初始化属于统一的概念——两者缺一不可。构建器属于一种较特殊的方法类型,因为它没有返回值。这与void返回值存在着明显的区别。对于void返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构建器则不同,它不仅什么也不会自动返回,而且根本不能有任何选择。若存在一个返回值,而且假设我们可以自行选择返回内容,那么编译器多少要知道如何对那个返回值作什么样的处理。4.2方法过载在任何程序设计语言中,一项重要的特性就是名字的运用。我们创建一个对象时,会分配到一个保存区域的名字。方法名代表的是一种具体的行动。通过用名字描述自己的系统,可使自己的程序更易人们理解和修改。它非常象写散文——目的是与读者沟通。我们用名字引用或描述所有对象与方法。若名字选得好,可使自己及其他人更易理解自己的代码。将人类语言中存在细致差别的概念“映射”到一种程序设计语言中时,会出现一些特殊的问题。在日常生活中,我们用相同的词表达多种不同的含义——即词的“过载”。我们说“洗衬衫”、“洗车”以及“洗狗”。但若强制象下面这样说,就显得很愚蠢:“衬衫洗衬衫”、“车洗车”以及“狗洗狗”。这是由于听众根本不需要对执行的行动作任何明确的区分。人类的大多数语言都具有很强的“冗余”性,所以即使漏掉了几个词,仍然可以推断出含义。我们不需要独一无二的标识符——可从具体的语境中推论出含义。大多数程序设计语言(特别是C)要求我们为每个函数都设定一个独一无二的标识符。所以绝对不能用一个名为print()的函数来显示整数,再用另一个print()显示浮点数——每个函数都要求具备唯一的名字。在Java里,另一项因素强迫方法名出现过载情况:构建器。由于构建器的名字由类名决定,所以只能有一个构建器名称。但假若我们想用多种方式创建一个对象呢?例如,假设我们想创建一个类,令其用标准方式进行初始化,另外从文件里读取信息来初始化。此时,我们需要两个构建器,一个没有自变量(默认构建器),另一个将字串作为自变量——用于初始化对象的那个文件的名字。由于都是构建器,所以它们必须有相同的名字,亦即类名。所以为了让相同的方法名伴随不同的自变量类型使用,“方法过载”是非常关键的一项措施。同时,尽管方法过载是构建器必需的,但它亦可应用于其他任何方法,且用法非常方便。在下面这个例子里,我们向大家同时展示了过载构建器和过载的原始方法:151-152页程序//:c04:Overloading.java//Demonstrationofbothconstructor//andordinarymethodoverloading.importjava.util.*;classTree{intheight;Tree(){prt(Plantingaseedling);height=0;}Tree(inti){prt(CreatingnewTreethatis+i+feettall);height=i;}voidinfo(){prt(Treeis+height+feettall);}voidinfo(Strings){prt(s+:Treeis+height+feettall);}staticvoidprt(Strings){System.out.println(s);}}publicclassOverloading{publicstaticvoidmain(String[]args){for(inti=0;i5;i++){Treet=newTree(i);t.info();t.info(overloadedmethod);}//Overloadedconstructor:newTree();}}///:~Tree既可创建成一颗种子,不含任何自变量;亦可创建成生长在苗圃中的植物。为支持这种创建,共使用了两个构建器,一个没有自变量(我们把没有自变量的构建器称作“默认构建器”,注释①),另一个采用现成的高度。①:在Sun公司出版的一些Java资料中,用简陋但很说明问题的词语称呼这类构建器——“无参数构建器”(no-argconstructors)。但“默认构建器”这个称呼已使用了许多年,所以我选择了它。我们也有可能希望通过多种途径调用info()方法。例如,假设我们有一条额外的消息想显示出来,就使用String自变量;而假设没有其他话可说,就不使用。由于为显然相同的概念赋予了两个独立的名字,所以看起来可能有些古怪。幸运的是,方法过载允许我们为两者使用相同的名字。4.2.1区分过载方法若方法有同样的名字,Java怎样知道我们指的哪一个方法呢?这里有一个简单的规则:每个过载的方法都必须采取独一无二的自变量类型列表。若稍微思考几秒钟,就会想到这样一个问题:除根据自变量的类型,程序员如何区分两个同名方法的差异呢?即使自变量的顺序也足够我们区分两个方法(尽管我们通常不愿意采用这种方法,因为它会产生难以维护的代码):152-153页程序//:c04:OverloadingOrder.java//Overloadingbasedontheorderof//thearguments.publicclassOverloadingOrder{staticvoidprint(Strings,inti){System.out.println(String:+s+,int:+i);}staticvoidprint(inti,Strings){System.out.println(int:+i+,String:+s);}publicstaticvoidmain(String[]args){print(Stringfirst,11);print(99,Intfirst);}}///:~两个print()方法有完全一致的自变量,但顺序不同,可据此区分它们。4.2.2主类型的过载主(数据)类型能从一个“较小”的类型自动转变成一个“较大”的类型。涉及过载问题时,这会稍微造成一些混乱。下面这个例子揭示了将主类型传递给过载的方法时发生的情况:153-155页程序//:c04:PrimitiveOverloading.java//Promotionofprimitivesandoverloading.publicclassPrimitiveOverloading{//booleancan'tbeautomaticallyconvertedstaticvoidprt(Strings){System.out.println(s);}voidf1(charx){prt(f1(char));}voidf1(bytex){prt(f1(byte));}voidf1(shortx){prt(f1(short));}voidf1(intx){prt(f1(int));}voidf1(longx){prt(f1(long));}voidf1(floatx){prt(f1(float));}voidf1(doublex){prt(f1(double));}voidf2(bytex){prt(f2(byte));}voidf2(shortx){prt(f2(short));}voidf2(intx){prt(f2(int));}voidf2(longx){prt(f2(long));}voidf2(floatx){prt(f2(float));}voidf2(doublex){prt(f2(double));}voidf3(shortx){prt(f3(short));}voidf3(intx){prt(f3(int));}voidf3(longx){prt(f3(long));}voidf3(floatx){prt(f3(float));}voidf3(doublex){prt(f3(double));}voidf4(intx){prt(f4(int));}voidf4(longx){prt(f4(long));}voidf4(floatx){prt(f4(flo