第12章 充分利用现有对象
本章介绍如下内容:
超类和子类的设计;
建立继承层次;
覆盖方法。
Java 对象非常适合繁殖。当创建了一个对象(一组属性和行为)时,实际上你设计了可遗传给后代的品质。这些子对象将继承父对象的属性和行为,而且它们还可以添加父对象没有的属性和行为。
这种规律称为继承,也就是超类(父母)遗传给子类(孩子)。继承是面向对象编程最有用的方面,本章将详细介绍它。
面向对象编程的另一个有用的方面是能够创建可在不同程序中使用的对象。可重用性使得开发没有错误的可靠程序更加容易。
12.1 继承的威力
每当读者使用标准Java类(如String或Integer)时,你使用的就是继承。Java类被组织成金字塔型的类层次结构,其中所有的类都是从Object类派生而来的。
类继承其上面所有的超类。要了解其中的原理,来看看JApplet类。这个类是所有applet的超类,后者是使用图形用户界面框架(称之为 Swing)而且基于浏览器的 Java 程序。JApplet是Applet的子类。
图 12.1 显示了 JApplet 的部分家谱,其中每个方块都是一个类,超类与其下面的子类用直线连接。
最顶端是Object类。在继承层次结构中,JApplet有5个超类:Applet、Panel、Container、Component、Object。
图12.1 Applet类的家谱
JApplet从这些类继承属性和行为,因为在继承层次结构中,它们都在JApplet的正上方。JApplet不继承图12.1中用阴影标识的5个类,包括Dialog和Frame,因为在继承层次结构中,它们不在JApplet的上方。
如果感到迷惑,可将层次结构视为家谱。JApplet继承父亲、祖父,不断向上直到太祖父Object类。然而,JApplet不会从兄弟或堂兄那里继承。
创建一个新的类可以归结为执行如下任务:你指需要定义其不同于现有类的地方即可,其他工作都已经为你做好。
类的行为和属性由两部分组成:自己的行为和属性以及从超类继承的行为和属性。
下面是Applet的一些行为和属性:
equals( )方法确定 JApplet 对象的值是否与另一个对象相同;
setBackground( )方法设置 applet 窗口的背景颜色;
add( )方法给applet 添加用户界面组件,如按钮和文本框等;
setLayout( )方法定义如何组织 applet 的图形用户界面。
JApplet 类可以使用所有这些方法,即使 setLayout( )方法不是从其他类继承来的。equals( )方法是在 Object 类中定义的,setBackground( )方法来自 Component 类,而 add( )方法来自Container类。
在 JApplet 类中定义的有些方法也在其超类中定义了,例如,update( )方法在 JApplet 和Component 类中都有定义。当方法在子类和超类中都定义了时,将使用子类中的定义;因此子类可以修改、替换或完全删除超类的行为和属性。就 update( )方法而言,它旨在删除超类中的一些行为。
在子类中创建新方法以修改从超类继承来的行为被称为“覆盖”(overriding)方法。如果继承的行为不能获得所需的结果,则需要覆盖相应的方法。
12.2 建立继承
使用关键字extends将一个类指定为另一个类的子类,如下所示:
上述语句创建AnimatedLogo类,并将其指定为JApplet的子类。读者在第17章将会看到,所有的 Swing applet 都必须是 JApplet 的子类,因为在 Web 页面中运行时,它们需要这个类提供的功能。
AnimatedLogo 必须覆盖方法 paint( ),该方法用于绘制要在程序窗口中显示的所有内容。paint( )方法是由 Component 类实现的,并向下传递给 AnimatedLogo 类,但 paint( )不做任何事情,Component类提供该方法旨在让其子类需要显示内容时可使用它。
要覆盖该方法,必须以与超类相同的方式声明它,即public方法仍必须是public的,方法的返回值类型必须相同,且参数的数量和类型都不能变。
在 Component 类中,paint( )方法的声明如下:
在AnimatedLogo中覆盖该方法时,也使用类似于下面的语句:
唯一的差别是Graphics对象的名称,这对于判断创建方法的方式是否相同没有影响。这两个语句是相同的,因为下面几项相同:
两个 paint( )方法都是 public 的;
两个方法都没有返回值,因为声明时使用了关键字void;
都接受Graphics对象作为唯一的参数。
在子类中使用this和super
在子类中this和super是两个很有用的关键字。
前一章讲到,关键字this用于引用当前对象。创建类时,要引用根据该类创建的特定对象,可使用this,如下面的语句所示:
这条语句将对象的title变量设置为文本Cagney。
关键字super的用途与此类似:引用对象的上一级超类。可以下面几种方式使用super:
引用超类的构造函数,如 super("Adam", 12);
引用超类的变量,如 super.hawaii = 50;
引用超类的方法,如 super.dragnet( )。
使用关键字 super 的方式之一是在子类的构造函数中。子类从其超类继承所有的行为和属性,因此将子类的构造函数与超类的构造函数关联起来,否则有些行为和属性可能不能正确设置,导致子类不能正常工作。
为此,在子类的构造函数中,第一条语句必须调用超类的构造函数,因此需要使用关键字super,如下面的语句所示:
这是子类的构造函数,它使用 super(name, length)调用超类相应的构造函数。
如果不使用super来调用超类的构造函数,则在子类构造函数执行时,Java将自动调用无参数的超类构造函数。如果该超类构造函数不存在或提供了意料之外的行为,将导致错误,因此最好手工调用超类的构造函数。
12.3 使用现有的对象
面向对象编程鼓励代码重用。如果在某个Java编程项目中开发了一个对象,可以将其用于其他项目中,而无需做任何修改。
如果Java类是设计良好的,完全可以在其他程序中使用它。在程序中可以使用的对象越多,编写软件需要做的工作就越少。如果有优秀的拼写检查对象能够满足你的需要,便可以使用它而不用自己编写。这甚至可以给老板错觉:将拼写检查功能添加到程序中需要做很多的工作,这样你就有更多的时间在办公室打长途电话。
注意:
本书作者像很多人一样,是个自由职业者,在家办公。评估其上述建议时,别忘了考虑工作环境。
Java刚面世时,共享对象的系统很不正规。程序员尽可能独立地开发其对象,并且通过使用私有变量和读写变量的公有方法来进行保护,以防误用。
有了开发可重用对象的标准后,共享对象变得功能强大。
标准的好处体现在下面几个方面:
可以更少地编写关于对象工作原理的文档,因为知道标准的人都对其工作原理很清楚;
可以根据标准来设计开发工具,使得使用对象更容易;
两个遵循标准的对象可以相互交互,而无需通过特殊编程使其互相兼容。
12.4 将相同类型的对象存储到 Vector 中
编写计算机程序时,需要做出的一个重要决策是,将数据存储在哪里。在本书的前半部分,介绍了3种存储信息的地方:基本数据类型(如int和char)、数组和String对象。
任何Java类都能够存储数据。最有用的一种是Vector,它是一种存储相同类对象的数据结构。
Vector类似于数组,也存储相关的数据,但其长度可动态地增减。
Vector类位于java.util包中,这是Java类库中最有用的一个包。在程序中使用它,可使用一条import语句:
Vector 存储的对象要么属于同一个类,要么有相同的超类。要创建 Vector,需要引用两个类:Vector和要存储在Vector的类。
将要在Vector中存储的类的名称放在“<”和“>”之间,如下述语句所示:
上述语句创建一个存储字符串的Vector。以这种方式指定的存储在Vector中的类使用了通用类型,通用类型用来指示数据结构(比如Vector)可以存储的对象类型。在以前的Java版本中,要使用Vector,需要调用构造函数,如下所示:
虽然仍可以这样做,但通用类型使代码更可靠,因为它向编译器提供了一种防止程序员误用Vector的方法。如果试图将一个Integer对象存储到本应存储String对象的Vector中,编译器将报错。
与数组不同,Vector存储的元素数量并非固定的。Vector默认包含10个元素,如果你知道需要存储更多的存储,可通过构造函数的参数指定长度。下面的语句创建一个包含300个元素的Vector:
要将对象加入到 Vector中,可调用 Vector 的 add( )方法,并将对象作为其唯一的参数:
对象按照顺序加入到Vector中,因此如果这是加入到victoria中的前3个元素,则元素0为“Vance”,元素1为“Vernon”,元素2为“Velma”。
要从 Vector中检索元素,可使用方法 get( )并将元素的索引号作为其参数:
该语句将“Vernon”存储到字符串变量name中。
要查看 Vector 的元素中是否包含某个对象,可调用 contains( )方法并将该对象作为参数:
要将对象从 Vector 中删除,可调用 remove( )方法并将该对象或其索引号作为参数:
这两条语句导致Vector中只留下“Velma”。
遍历Vector
Java新增了一种for循环以方便载入Vector以及依次检查其每个元素。
该循环由两部分组成,比第8章介绍的for循环少一部分。
第一部分是初始化部分:变量的类及其名称,该变量用于存储从Vector中取出的每个对象。该对象与Vector中存储的对象必须属于同一个类。
第二部分指出要遍历的Vector。
下面的代码遍历 Vector victoria,并将其中的每个对象的名字显示到屏幕上:
本章将要创建的第一个应用程序使用Vector和特殊的for循环将一系列字符串按字母顺序显示到屏幕上。这些字符串来自一个数组和命令行参数。
在 NetBeans中打开 Java24 项目,然后选择 File->New File,创建一个新的 Java 空文件,并将其命名为StringLister。然后在源代码编辑器中输入程序清单12.1中的所有文本,并保存。
程序清单12.1 StringLister.java程序的完整代码
在运行该应用程序时(通过 Run->Run File 来运行),应该先选择 Run->Set Project Con figuration->Customize 命令,将 main class 设置为StringLister,并将参数设置为名字,当有多个名字时,需要使用空格分开,比如 Jackie Nickey Farina Woim。
命令行中指定的名字将添加到第 4~5 行的字符串数组中。由于在程序运行之前无法得知名字的个数,因此使用Vector来存储这些字符串比数组更合适。
使用Collection类的一个方法对存储在Vector中的字符串按字母顺序排序:
Collections.sort(list);
和Vector一样,这个类也位于java.util包中。在Java中,Vector以及其他有用的数据结构被称为集合。
当运行该程序时,输出结果应该是一组按字母排序的名字(见图12.2)。Vector在存储空间上的灵活性可以让你将额外的名字添加到数据结构中,并与其他名字一起排序。
图12.2 StringLister程序的输出结果
12.5 创建子类
为了看一个关于继承的示例,现在创建一个名为Point3D的类,它代表三维空间中的一个点。二维点可用坐标(x, y)表示。applet 使用(x, y)坐标来决定在哪里显示文本和图形,而三维空间添加第三个坐标,可将其称为z。
Point3D对象需要做下面3件事:
记录对象的(x, y, z)坐标;
需要时将对象移到新坐标(x, y, z)处;
需要时沿x、y和z轴移动特定的距离。
Java有一个表示二维点的标准类,名为Point。它有两个整型变量x和y,用于存储Point对象(x, y)坐标。它还有名为 move( )的方法,用于将一个点放在指定的位置,还有一个名为translate( )的方法,将对象沿 x 和 y轴移动特定的距离。
在NetBeans中的Java24项目中,创建一个新的空文件,将其命名为Point3D,然后在源代码编辑器窗口中输入程序清单12.2中的全部文本,并保存。
程序清单12.2 Point3D.java的完整源代码
Point3D 类没有 main( )块语句,所以不能使用 Java 解释器运行它。但是你可以在需要三维空间点的Java程序中使用它。
Point3D 类只需完成其超类 Point 不能完成的工作,主要是记录整型变量 z,将 z 作为move( )方法、translate( )方法和 Point3D( )构造函数的参数。
这些方法都使用关键字supper和this。this用于引用当前的Point3D对象,因此第8行的this.z=z;语句将变量z设置为第6行作为参数传入的z的值。
super语句引用当前对象的超类Point,用于设置Point3D类继承的变量和调用继承的方法。第 7 行的语句 super(x, y)调用超类的构造函数 Point(x, y),该构造函数用来设置 Point3D对象的(x, y)坐标。由于 Point 类能够处理坐标轴 x 和 y,如果 Point3D 类也这样做将多余。
要测试编译后的Point3D类,可以创建一个使用Point对象和Point3D对象的程序,并在屏幕上移动它们。在NetBeans中创建一个名为PointTester的新文件,然后输入程序清单12.3中的全部文本。当保存该文件时,将会自动对其编译。
程序清单12.3 PointTester.java程序的完整源代码
通过选择Run->Run File运行该文件时,如果该程序已经成功编译,则会看到如图12.3所示的输出。否则,查看源代码编辑器窗口旁边的红色图标,它可以指示是哪一行代码触发了错误。
图12.3 PointTester 程序的输出
12.6 总结
当人们谈论生育奇迹时,他们谈论的可能不是从Java超类派生子类的方式或在类层次结构中继承行为和属性的方式。
如果现实世界与面向对象编程的工作方式相同,则莫扎特的每个子孙都可以选择成为一名卓越的作曲家,马克吐温的所有子孙都能写出密西西比河边生活的诗歌。所有从祖先那里继承的技能都可以由你直接支配,不需要通过任何努力。
从奇迹的程度看,继承不能同物种延续和减免大笔税款相比。然而,这是一种设计软件的高效方法,可最大限度地减少重复工作。
12.7 问与答
问:到目前为止,所创建的大多数Java程序都没有使用extends来从超类继承,这是否就意味着它们不在层次结构中?
答:使用 Java 创建的所有类都是 Java 类层次结构的一部分,因为编写程序时如果不使用关键字 extends,则默认超类为 Object 对象。所有类的方法 equals( )和 toString( )都是自动从Object类继承而来的行为。
12.8 测验
为测试读者从本章继承了什么知识,请回答下列问题。
1.如果在子类中要以不同于超类的方式处理方法,应该如何做?
a.删除超类中的方法。
b.覆盖超类中的方法。
c.给《San Jose Mercury News》的编辑写信,希望 Java 的开发者能看到这封信。
2.哪个方法用于检索存储在Vector中的元素?
a.get( )。
b.read( )。
c.elementAt( )。
3.可以使用哪个关键字来引用当前对象的方法和变量?
a.this。
b.that。
c.theOther。
1.b.由于可以覆盖方法,所以无需修改超类的任何方面。
2.a.方法get( )接受一个参数——元素的索引号。
3.a.this关键字引用当前对象。
12.9 练习
如果你脑海中出现了学习更多知识的强烈愿望,可以通过下面的练习来获得更多有关继承的知识。
创建一个 Point4D 类,在Point3D 类创建的(x, y, z)坐标系的基础上再加上 t 坐标。t坐标代表时间,因此必须确保它不会被设置为负值。
使用足球队的进攻成员——边锋、外接手、阻挡线队员、跑锋、四分卫,设计一个代表这些球员技能的类层次结构,将通用技能放在层次结构的较上层。例如,阻挡行为应由边锋和阻挡线队员继承,速度应由外接手和跑锋继承。
有关为完成这些练习而编写的Java程序,请访问本书的配套网站www.java24hours.com。