天天看点

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

第1章和第2章讲述了面向对象的基本概念。在开始学习关于构建面向对象系统的一些具体设计问题之前,我们需要更进一步了解面向对象的一些概念,比如构造函数、操作符重载以及多重继承。我们也会讲述错误处理技术以及面向对象的设计中作用域的重要性。

其中一些概念可能对深入理解面向对象设计并不是必需的,但设计和实现整个面向对象系统的人有必要了解。

构造函数对于结构化编程的程序员来说是个新概念。非面向对象的语言(比如cobol、c和basic)通常不会用到构造函数。c/c++中的结构体(struct)具有构造函数。前两章提及过这个用于构造对象的特殊方法。在诸如java和c#之类的面向对象的语言中,构造函数名称与类名相同。而visual basic .net使用关键字new,objective-c使用init关键字。这里我们只关注于构造函数的概念,而不会介绍所有语言的特殊语法。接下来用java代码来实现一个构造函数。

例如,第2章中cabbie类的构造函数如下所示:

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

编译器会意识到这个方法名与类名完全相同,所以认为该方法是个构造函数。

小心

注意,java代码(以及c#和c++)中,构造函数没有返回值。如果有返回值,编译器就不认为该方法是构造函数。

例如,如果类中有以下代码,那么编译器不会认为该方法是构造函数,因为它有返回值,这个返回值是一个整数:

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

该语法会导致问题,因为虽然这份代码可以通过编译但得不到期望的行为。

当创建新对象时,首要事情之一是调用构造函数。请看以下代码:

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

new关键字创建了cabbie类的一个新实例,这会按需分配内存。然后会调用构造函数自身,并且可以通过参数列表传递参数。开发人员可以在构造函数内进行相应的初始化

工作。

因此,new cabbie()代码将实例化一个cabbie对象,并调用cabbie方法,即该类的构造函数。

构造函数最重要的功能大概是当遇到new关键字时初始化内存分配。总之,构造函数中的代码会把新创建的对象初始化到稳定、安全的状态。

例如,如果有一个计数器(counter)对象,里面有个属性叫count,你需要在构造函数中将count设置为0:

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

初始化属性

在结构化编程中,名为housekeeping(管家)或initialization(初始化)的例程往往用于初始化目的。初始化属性是构造函数经常执行的功能。

如果编写了一个不包含构造函数的类,这个类仍然可以通过编译,你也可以使用它。如果没有为类提供一个显式的构造函数,那么类会有一个默认构造函数。请记住,无论你是否自定义了构造函数,类始终至少有一个构造函数。如果你没有提供构造函数,系统会为你提供一个默认的构造函数。

除了创建对象本身之外,默认构造函数的另一个行为是调用父类的构造函数。大多数情况下,父类是语言框架的一部分,比如java中的object类。例如,如果没有为cabbie类提供构造函数,系统会提供下面默认的构造函数:

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

如果反编译编译器生成的字节码,你会看到这段代码。这段代码实际上是由编译器插

入的。

在本例中,如果cabbie没有显式继承自其他类,object类将会是它的父类。默认构造函数在有些场景下是适用的。然而,大多数场景下,需要自定义初始化一系列内存。不管在什么情况下,在类中始终包含至少一个构造函数是一个优秀的实践。如果类有属性,最好始终在构造函数中初始化这些属性。延伸开来,无论是否在编写面向对象的代码,初始化变量总是一个优秀的实践。

提供构造函数

通用规则是即使并不需要在构造函数中做任何事情,也应当始终提供一个构造函数。你可以提供一个不包含任何代码的构造函数,稍后再按需添加代码。尽管使用编译器默认提供的构造函数在技术上没有任何问题,但基于文档化和维护目的,这样更容易看懂你的代码。

这里考虑维护问题并不奇怪。如果你使用的是默认的构造函数,后续操作添加了另一个构造函数,那么系统不会再创建默认的构造函数。总之,只有类中没有包含任何构造函数时,系统才会添加默认的构造函数。一旦你提供了一个构造函数,系统就不再提供默认的构造函数。

大多数情况下,可以用多种方式创建对象。这需要提供多个构造函数。例如,请看count类:

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

一方面,可以初始化属性count为0,实现这一点很简单,可以在一个构造函数中初始化count为0,如下所示:

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

另一方面,可以传递一个初始化参数,从而可以设置count为其他数字:

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

这叫作重载方法(重载适用于所有方法,不止是构造函数)。大部分的面向对象语言都提供了重载方法的功能。

1.?重载方法

重载可以让程序员重复使用相同的方法名,只要每次方法签名不同即可。方法签名包含了方法名以及参数列表(如图3-1所示)。

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

所以,以下所有方法拥有不同的签名:

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

方法签名可能包含返回值类型,也可能不包含返回值类型,这取决于不同的语言。在java和c#中,返回值类型并不属于签名的一部分。例如,以下代码即使返回值类型不同,也不能通过编译:

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

了解签名最好的方式是编写一些代码然后进行编译。

通过使用不同的签名,你可以根据不同的构造函数来构造对象。如果你不能保证每次都能掌握足够的信息,那么很适合这种方式。例如,当创建一个购物车时,顾客可能已经登录了自己的账号(你会得到顾客所有的信息)。而一个全新的顾客可能会向购物车中放入产品,但没有任何账号信息,这样的情况下构造函数初始化方式是不同的。

2.?使用uml对类建模

我们再回头看第2章中用到的数据库阅读器例子。构造数据库阅读器有两种方式:

传入数据库名称以及设置游标在数据库中的起始位置。

传入数据库名称以及设置游标在数据库中的期望位置。

图3-2展示了databasereader类的类图。注意,该图列出了此类的两个构造函数。尽管该图显示了两个构造函数,但并未包含参数列表,所以无法区分出这两个构造函数。为了区分这两个构造函数,可以查看下面列出的database-reader类的对应代码。

无返回值类型

注意,在该类图中,构造函数没有返回值类型。除了构造函数之外,其他所有方法必须要有返回值类型。

以下代码片段展示了该类的构造函数,以及构造函数如何初始化属性(见图3-3):

请注意在两个场景中如何初始化startposition。如果没有通过参数列表为构造函数提供位置信息,那么startpostion会被初始化为默认值,即0。

3.?如何构造父类

当使用继承时,你必须知道如何构造父类。请记住,当使用继承时,也继承了父类的所有东西。因此必须熟悉父类的数据和行为。任何继承的属性都是完全可见的。然而,对构造函数的继承则是不可见的。如果遇到了new关键字,那么会分配对象,并发生以下步骤(见图3-4):

  

《面向对象的思考过程(原书第4版)》一 第3章 高级的面向对象概念

1)在构造函数中会调用父类的构造函数。如果没有显式调用父类的构造函数,那么系统会默认自动调用;不过可以在字节码中看到这段代码。

2)对象中的所有属性会被初始化。这些属性是类中定义中的属性(实例变量),不是构造函数或其他方法中的属性(局部变量)。在databasereader代码中,整数start-position是类的实例变量。

3)执行构造函数中的其余代码。

3.1.5 设计构造函数

我们已经看到了,设计类的一个最佳实践是初始化所有属性。有些语言中,编译器会提供一部分初始化工作。与往常一样,不要依赖编译器来初始化属性!在java中,只有属性被初始化后你才能使用它。如果属性在代码中很靠前,请确保你初始化属性为一些有效值,比如设置整数为0。

构造函数用来确保应用程序处于稳定的状态(我喜欢称之为“安全”的状态)。例如,如果把属性作为除法运算中的分母,那么初始化该属性为0会导致应用程序崩溃。你必须考虑除法中用0作为除数是非法操作。始终初始化属性为0并不总是最好的方式。

在设计时,优秀的实践应该是为所有属性识别一个稳定的状态,然后在构造函数中初始化这些属性为稳定的状态。