5 初始化与清理 5.2 方法重载

  5.2 方法重载

     任何程序设计语言都具备的一项重要特性就是对名字的运用。当创建一个对象时,也就给此对象分配到的存储空间去了一个名字。所谓方法则是给某个动作取的名字。通过适用名字,你可以引用所有的对象和方法。名字起得好可以使系统更易于理解和修改。就好比写散文——目的是让读者易于理解。

     将人类语言中存在细微差别的概念“ 映射 ”到程序设计语言中时,问题随之产生。在日常生活中,相同的词可以表达多种不同的含义——它们被“ 重载 ”了。特别是含义之间的差别很小时,这种方式十分有用。你可以说“ 清洗衬衫 ”“ 清洗狗 ”。但如果硬要这样说就显得很愚蠢:“ 以洗衬衫的方式系衬衫 ”“ 以洗车的方式洗车 ”、“ 以洗狗的方式洗狗 ”。这是因为听众根本不需要对所执行的动作做出明确的区分。大多数人类语言具有很强的“ 冗余 ”性,所以即使漏掉了几个词,仍然可以推断出含义。不需要对每个概念都使用不同的词汇——从具体的语境中就可以推断出含义。

      大多数程序设计语言(尤其是C)要求每个方法(在这些语言中经常成为函数)都提供一个独一无二的标识符。所以决不能用名为print()的函数显示了整数之后,又用一个名为print()的函数显示浮点数——每一个函数都要有唯一的名称。

      在Java(和C++)里,构造器是强制重载方法名的另一个原因。既然构造器的名字已经由类名所决定,就只能有一个构造器名。那么要想用多种方式创建一个对象该怎么办呢?假设你要创建一个类,既可以用标准方式进行初始化,也可以从文件里读取信息来初始化。这就需要两个构造器:一个默认构造器,另一个取字符串作为形式参数——该字符串表示初始化对象所需的文件名称。由于都是构造器,所以它们必须有相同的名字,即类名。为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载。同时,尽管方法重载是构造器所必需的,但它亦可应用于其他方法,且用法同样方便。

   5.2.1 区分重载方法

       要是几个方法有相同的名字,Java如何才能知道你指的是哪一个呢?其实规则很简单:给个重载的方法都必须有一个独一无二的参数类型列表。

      稍加思考,就会觉得这是合理的。毕竟,对于名字相同的方法,除了参数类型的差异以外,还有什么方法能把它们区分开呢?

       甚至参数顺序的不同也足以区分两个方法。不过,一般情况下别这么做,因为这回事代码难以维护。

    5.2.2 涉及基本联类型的重载

    基本类型能从一个“ 较小 ”的类型自动提升至一个 “ 较大 ”的类型,此过程一旦涉及到重载,可能会造成一些混乱。

    在这里,方法接受较小的基本类型作为桉树。如果传入的实际参数较大,就得通过类型转换来窄化转换。如果不这样做,编译器就会报错。

     5.2.3 以返回值区分重载方法

     读者可能会想:“ 在区分重载方法的时候,为什么只能以类名和方法的形参列表作为标准呢?能够考虑用方法的返回值来区分呢? ” 比如下面两个方法,虽然它们有同样的名字和形式参数,但却很容易区分它们:

     void f();

     int f() {return 1;}

      只要编译器可以根据语境明确判断出语义,比如在int x=f()中,那么的确可以据此区分重载方法。不过,有时,你并不关心方法的返回值,你想要的是方法调用的其他效果(这常被称为“ 为了副作用而调用 ”),这时你可能会调用方法而忽略其返回值。所以,如果下面这样调用方法:

    f();

     此时Java如何才能判断该调用哪一个f()呢?别人如何理解这种代码呢?因此根据返回值来区分重载方法是行不通的。

    5.3 默认构造器

     如前所述,默认构造器(又名“ 无参 ”构造器)是没有形式参数的——它的作用是创建一个“ 默认对象 ”。如果你写的类中没有构造器,则编译器会自动帮助你创建一个默认构造器。

     例如 :

     

默认构造器

 

     表达式: new Bird()

     行创建了一个新对象,并调用其默认构造器——即使你没有明确定义它。没有它的话,就没有方法可调用,就无法创建对象。

     但是,如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器:

      

非默认构造器

 

 

        要是这样写:

         new Brid2()

       编译器就会报错:没有找到匹配的构造器。这就好比,要是你没有提供任何构造器,编译器会认为“ 你需要一个构造器,让我给你制造一个吧 ”;但假如你已写了一个构造器,编译器则会认为“ 啊,你已写了一个构造器,所以你知道你在做什么;你是可以省略了默认构造器 ”。

      5.4 this关键字

      如果有额用以类型的两个对象,分别是a和b。你可能想知道,如何才能让这两个对象都能调用peel()方法呢:

     

类的this

       如果只有一个peel()方法,它如何知道是被 a还是被 b 所调用的呢?

       为了能用简便、面向对象的语法来编写代码——即“ 发送消息给对象 ”,编译器做了一些幕后工。它暗自把“ 所操作对象的引用 ”作为第一个参数传给peel()。所以上述两个方法的调用就变成了这样:

       Banana.peel(a,1);

       Banana.peel(b,2);

      这是内部的表示形式。我们并不能这样书写代码,并试图通过编译:但这种写法的确能帮你了解实际所发生的事情。

      假设你希望在方法的内部获得对当前对象的引用。

      由于这个引用是由编译器 “ 偷偷 ”传入的,所以没有标识符可用。但是,为此有个专门的关键字:this。this关键字只能在方法内部使用,表示对“ 调用方法的那个对象 ”的引用。this的用法和其他对象引用并无不同。但要注意,如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。当方法中的this引用会自动应用于同一类中的其他方法。所以可以这样写代码:

  

同一个类中调用其他方法

      在pit()内部,你可以写this.pick(),但无此必要。编译器能帮你自动添加。只有当需要明确指出对当前对象的引用时,才需要使用this关键字。例如,当需要返回对当前对象的引用时,就常常在 return语句里这样写:

     

返回当前对象

 

使用this

     this关键字对于将当前对象传递给其他方法也很有用:

    

类中调用this

     Apple需要调用Peeler.peel()方法,它是一个外部的工具方法,将执行由于某种原因而必须放在Apple外部的操作(也许是因为该外部方法要应用于许多不同的类,而你却不想重复这些代码)。为了将其滋镇传递给外部方法,Apple必须使用this关键字。 

  5.4.1 在构造器中调用构造器

     可能为了一个类写了多个构造器,有时可能想在一个构造器中调用另一个构造器,以便面重复代码。

     可用this关键字做到这一点。

      通常写this的时候,都是指“这个对象” 或者“ 当前对象 ”,而且它本身表示对当前对象的引用。在构造器中,如果this添加了参数列表,那么就有了不同的含义。这将产生对符合此参数列表的某个构造器的明确调用;这样,调用其他构造器就有了直接的途径:

   

构造器中调用构造器

 

       构造器Flower(String s,int petals)表明:尽管可以用this调用一个构造器,但却不能调用两个,此外,必须将构造器调用置于最起始处,否则编译器会报错。

       这个例子也展示了this的另一种用法。由于参数s的名称和数据成员s的名字相同,所以会产生歧义。是用this.s来代表成员能解决这个问题。在Java程序代码中经常出现这种写法,本书中也常这么写。

       printPetalCount()方法表明,除构造器之外,编译器禁止在其他任何方法中调用构造器。

       5.4.2 static的含义

       了解this关键字之后,就能更全面地理解static(静态)方法的含义。static方法就是没有this的方法。在static方法的内部不能调用非静态方法,反过来倒是可以的。而且可以在没有创建任何对象的前提表,仅仅通过类本身调用static方法。实际上正式static 方法的主要用途。它很像全局方法。Java中禁止使用全局方法,但你在类中置入 static 方法就可以访问其他static方法和static域。

      有些人认为 static 方法不是“ 面向对象 ”的,因为他们的确具有全局函数的语义;使用 static 方法时,由于不存在this,所以不是通过“ 向对象发送消息 ”的方式来完成的。

      的确,要是在代码中出现了大量static方法,就该重新考虑自己的设计了。然而,static的概念有其实用之处,许多时候都要用到它。至于它是否真的“ 面向对象 ”,就留给理论家去讨论吧。事实上,Smalltalk语言里的“ 类方法 ”就是与static方法对应的。

     5.5 清理:终结处理和垃圾回收

       程序员都了解初始化的重要性,但常常忘记同样也重要的清理工作。

       毕竟谁需要清理一个int?但在使用程序库时,把一个对象用完后就“ 弃之不顾 ”的做法并非总是安全的。当然,Java有垃圾回收器负责回收无用对象占据的内存资源。但也有特殊额盈眶:嘉定你的对象(并非使用new)获得了一块“ 特殊 ”的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以不知道该如何释放该对象的这块“ 特殊 ”内存。为了应对这种情况,Java允许在类中定义一个名为 finalize()的方法。它的工作原理“ 鉴定 ”是这样的:一旦垃圾回收器准备释放对象占用的存储空间,将首先调用finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以要是你打算用finalize(),就能在垃圾回收时刻做一些重要的清理工作。

       这里有一个潜在的编程陷阱,因为有些程序员(特别是C++程序员)刚开始可能会误会把finalize()当做C++中的析构函数(C++)中销毁对象必须用到的这个函数。所以必须要明确区分一下:在C++中对象一定会被销毁(如果程序中没有缺陷的话);而Java里的对象却并不总是被垃圾回收。或者换句话说:

     1.对象可能不被垃圾回收

     2.垃圾回收并不等于“析构”

     牢记这些,就能原理困扰。

     这意味着在你不需要某个对象之前,如果必须执行某些动作,那么你得自己去做。Java并未提供“ 析构函数 ”或类似的清理工作,必须自己动手创建一个执行清理工作的普通方法。

      例如,假设某个对象在创建过程中会将自己绘制到屏幕上,如果不是明确地从屏幕上将其擦除,它可能永远得不到清理。如果在finalize()里加入某种擦除功能,当“ 垃圾回收 ”发生时(不能保证一定发生),finalize()得到了调用,图像就会被擦除。要是“ 垃圾回收 ”没有发生,图像就会一直保留下来。

       也许你会发现,只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,并且垃圾回收器一直都没有释放你创建的任何对象的存储空间,则随着程序的退出,那些资源也会全部交给操作系统。这个策略是恰当的,因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分开销了。

      5.5.1 finalize()的用途何在

      此时,读者已经明白了不该将finalize()作为通用的清理方法。那么,finalize()的真正用途是什么呢?

      这 引出了要记住的第三点:

      3.垃圾回收只与内存有关。

       也就是说,使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是finalize()方法),它们也必须同内存及其回收有关。

       但这是否意味着要是对象中含有其他对象,finalize()就应该明确释放那些对象呢?不,无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。这就将对finalize()的需求限制到一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间。不过,读者也看到了,Java中一切皆为对象,那这种特殊情况是怎么回事呢?

      看来之所以要有finalize(),是由于在分配内存时有可能采用了类似C语言中的做法,而非Java中的通常做法。这种情况主要发生在使用“ 本地方法 ”的情况下,本地方法是一种Java中调用非Java代码的方式。本地方法目前支持支C和C++,但它们可以调用其他语言的代码,所以实际上可以调用任何代码。在非Java代码中,也许调用C的malloc()函数系列来分配存储空间,而且除非调用了free()函数,否则存储空间将的不到释放,从而造成内存泄露。当然,free()是C和C++中的函数,所以需要在finalize()中用本地方法调用它。

      致辞,读者已经明白了不要过多使用finalize()的道理了。对,它却是不是进行普通清理工作的合适场所。那么,普通的清理工作应该在哪里执行呢?

    5.5.2 你必须实施清理

     要清理一个对象,用户必须在需要清理的时刻调用执行清理动作的方法。这听起来似乎很简单,但却与C++中的“ 析构函数 ”的概念有抵触。在C++中,所有对象都会被销毁,或者说,应该被销毁。如果在C++中创建了一个局部对象(也就是在堆栈上创建,这在Java中行不通),此时的销毁动作发生在以“ 右花括号 ”为边界的、此对象作用域的末尾处。如果对象是用 new创建的(类似于Java中),那么当程序员调用调用C++的delete操作符时,(Java没有这个命令),就会调用相应的析构函数。如果程序员忘记调用delete,那么永远不会调用析构函数,这样会出现内存泄露,对象的其他部分也不会得到清理。这种缺陷很难跟踪,这也是让C++程序员转向Java的一个主要因素。

     相反,Java不允许创建局部对象,必须使用new创建对象。在Java中,也没有用于释放对象的delete,因为垃圾回收器会帮助你释放存储空间。甚至可以肤浅的认为,正是由于垃圾收集机制存在,使得Java没有洗狗函数。

    然而,随着学习的深入,读者就会明白垃圾回收器的存在并不能完全代替析构函数。

    5.5.3 终结条件

       通常,不能指望finalize(),必须创建其他的“ 清理 ”方法,并且明确地调用它们。看来,finalize()只能存在于程序员们很难用到的一些晦涩用法里了。不过,finalize()还有一个有趣的用法,它并不依赖于每次都要对finalize()进行调用,这就是对象终结条件验证。

       当对某个对象不再感兴趣——也就是它可以被清理了,这个对象应该处于某种状态,使它占用的内存可以被安全地释放。

       例如,要是对象代表了一个打开的文件,在对象被回收前程序员应该关闭这个文件。只要对象中存在没有适当清理的部分,程序就存在很隐晦的缺陷。

       finalize(可以用来发现这种情况——尽管)它并不总是会被调用。如果某次finalize()的动作使得缺陷被发现,那么就可据此找出问题所在——这才是人们真正关心的。

       以下是个简单的例子,示范了finalize()可能的使用方式:

     

finalize()回收

     

       注意,Syestem.gc()用于强制进行终结动作。即使不这么做,通过重复地执行程序(假设程序将分配大量的存储空间而导致垃圾回收动作的执行),最终也能找出错误的Book对象。

       你应该总是假设基类版本的finalize()也要做某些重要的事情,因此要使用super来调用它,就像在Book.finalize()中看到的额那样。

     5.5.4 垃圾回收器如何工作

      在以前所用过的程序语言中,在堆上分配对象的代价十分高昂,因此读者自然觉得Java中所有对象(基本类型除外)都在堆上分配的方式也非常高昂。然而,垃圾回收器对于提高对象的创建速度,却具有明显的效果。听起来很奇怪——存储空间的释放竟然会影响存储空间的分配,但这确实是某些Java虚拟机的工作方式。这也意味着,Java从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度向媲美。

     这部分内容太多了,后续整理C++与Java的内存回收机制,期待时间稍充足点

    5.6 成员初始化

     Java尽力保证:所有变量在使用前都能得到恰当的初始化。对于方法的额局部变量,Java以编译时错误的形式来贯彻这种保证。所以如果写成:

     void f()

{

      int i;

 i++;//Error ——i not  initialized

}

    就会得到一条出错消息,告诉你可能尚未初始化。当然,编译器也可以为i赋一个默认值,但是未初始化的局部变量更有可能是程序员的疏忽,所以采用默认值反而会掩盖这种失误。因此强制程序员提供一个初始值,往往能够帮助找出程序里的缺陷。

    要是类的数据成员(即字段)是基本类型,情况就会变得有些不同。正如在“ 一切都是对象 ”一章中所看到的,类的每个基本类型数据成员保证都会有一个初始值。

    下面的程序可以验证这类情况,并显示它们的值:

    

成员初始化

       可见尽管数据成员的初值没有给出,但它们确实有初值(char值为0,所以显示为空白)。这样至少不会冒“ 未初始化变量 ”的风险了。

       在类里定义里定义一个对象引用时,如果不将其初始化,此引用就会获得一个特殊值null。

     5.6.1 指定初始化

      如果想为某个变量赋初值,该怎么做呢?有一种直接的办法,就是在定义类成员变量的地方为其赋值(注意C++里不能这样做,尽管C++新手们总想这样做)。一下代码片段修改InitialValues类成员变量的定义,直接提供了初值。

   

类的成员变量初值

    也可以用同样的方法初始化基本类型的对象。如果Depth是一个类,那么可以像下面这样创建一个对象并初始化它:

      

初始化

 

      甚至可以通过调用某个方法来提供初值:

     

调用方法初始化值

       但像下面这样写就不对了:

顺序,编译器对“向前引用”发出警告

      显然,上述程序的正确性取决于初始化的顺序,而与其编译方式无关。所以,编译器恰当地对“向前引用”发出了警告。

       这种初始化方法既简单有直观。但有个限制:类InitialValues的每个对象都会具有相同的初值。有时,这正是所希望的,但有时却需要更大的灵活性。

     5.7 构造器初始化

      可以用构造器来进行初始化。在运行时刻,可以调用方法或执行某些动作来确定初值,这为编程带来了更大的灵活性。但要牢记:无法组织自动初始化的进行,它将在构造器被调用之前发生。

    5.7.1 初始化顺序

      在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布与方法定义之间,他们仍旧会在任何方法(包括构造器)被调用之前得到初始化

       例如:

    

初始化顺序

     

5.7.2 静态数据的初始化

        无论创建多少个对象,静态数据都只占用一份存储区域。 static关键字不能应用于局部变量,因此它只能用于作用域。如果一个域是静态的基本类型域,且也没有对其它进行初始化,那么它就会获得基本类型的标准初值:如果它是一个对象引用,那么它的默认初始化值就是null。

       如果想在定义处进行初始化,采取的方法与非静态数据没什么不同。

       初始化顺序是先静态对象(如果它们尚未因前面的对象创建过程而被初始化),而后是“ 非静态 ”对象。从输出结果中可以观察到这一点。

    5.7.3 显示的静态初始化

     Java允许将多个静态初始化动作组织成一个特殊的“ 静态子句 ”(有时候叫做“ 静态块 ”)。

    5.7.4 非静态实例初始化

     Java中冶有被称为实例初始化的类似语法 ,用来初始化每一个对象的非静态变量。

     看起来它与静态初始化子句一模一样,只不过少了static关键字。这种语法对于支持“ 匿名内部类 ”的初始化是必须的,但是它也使得你可以保证无论调用了哪个显示构造器,某些操作都会发生。从输出中可以看到实例初始化子句是在两个构造器之前执行的。

    5.8 数组初始化

    数组只是相同类型的、用一个标识符名称封装到一起的对象序列或基本类型序列。

   数组是通过方括号下标操作符 [ ]  来定义和使用的。要定义一个数组,只需在类型名称后加上一对空方括号即可:

  int[] a1;

 方括号也可置于标识符后面:

  int a1[];

   两种含义是一样的,偶一种格式符号C和C++程序员的习惯。不过,前一种格式或许更合理,毕竟它表明类型是“一个int型数组”。

  编译器不允许指定数组的大小。这就又把我们带回到有关“引用”的问题上。现在拥有的只是对数组的一个引用(你已经为该引用分配了做的存储空间),而且也没给数组对象本身分配任何空间。为了给数组创建响应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化由一对花括号括起来的值组成的。

    在这种情况下,存储UKon构件的分配(等价于使用new)将由编译器负责。

   例如:

   int[] a1 = {1,2,3,4,5}

    那么,为什么要在没有数组的时候定义一个数组引用呢?

    int[] a2;

    其实真正做的只是复制了一个引用,就像下面演示的那样:

 

数组初始化

   

    所有数组(无论它们的元素时对象还是基本类型)都有一个固有成员,可以通过它获知数组内包含了多少个元素,但不能对其修改。这个成员就是length。与C和C++类似,Java数组技术也是从第0个元素开始,所以能使用的最大下表是length-2.要是超过这个边界,C和C++会“默默的接受”,并且允许你访问所有内存,许多声名狼藉的程序错误由此而生。

     Java则能保护你免受这一问题的困扰,一旦访问下标过界,就会出现运行时错误(即异常)。

     如果在编写程序时,并不能确定数组里需要多少个元素,那么该怎么办呢?可以直接用new在数组里创建元素。尽管创建的基本类型数组,new仍然可以工作(不能用new创建单个的基本类型数据)。

创建数组

 

   如果忘记了创建对象,并且视图使用数组中的空引用,就会在运行时产生异常。

   也可以用花括号括起来的列表来初始化对象数组。

    有两种形式:

     

初始化数组

 在这两种形式中,初始化列表的最后一个逗号都是可选的(这一个性使维护长列表变得更容易)。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值