当前位置:文档之家› 第七章 多态

第七章 多态

第七章 多态
第七章 多态

第七章多态

在面向对象的程序设计语言中,多态(polymorphic)是继数据抽象和继承之后的第三种基本特性。

多态通过分离“做什么”和“怎么做”,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建“可扩展的”程序,即无论在项目最初创建时,还是在需要添加新功能时,都可以进行扩充。

“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过细节“私有化(private)”将接口和实现分离开来。这种类型的组织机制对那些有过程化程序设计背景的人来说,更容易理解。而多态的作用则是消除类型之间的耦合关系。在前一章中,我们已经知道继承允许将对象视为自己本身的类型或它的基类型进行处理。这种能力极为重要,因为它可以使多种类型(从同一基类导出而来的)被视为同一类型进行处理,而同一份代码也就可以毫无差别地运行在这些不同类型之上了。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一基类导出而来的。这种区别是根据方法行为的不同来表示出来的,虽然这些方法都可以通过同一个基类来调用。

在本章中,通过一些基本简单的例子(这些例子中所有与多态无关的代码都被删掉,只剩下与多态有关的部分)来深入浅出地学习多态(也称作动态绑定 dynamic binding、后期绑定 late binding或运行时绑定 run-time binding)。

向上转型

在第 6章中,我们已经知道对象既可以作为它自己本身的类型使用,也可以作为它的基类型使用。而这种将对某个对象的引用视为对其基类型的引用的做法被称作“向上转型(upcasting)”――因为在继承树的画法中,基类是放置在上方的。

但是,这样做也会引起一个的问题,具体看下面这个有关乐器的例子。既然几个例子都要演奏乐符(Note),我们就应该在包中单独创建一个 Note类。

//: c07:music:Note.java

// Notes to play on musical instruments.

package c07.music;

import com.bruceeckel.simpletest.*;

public class Note {

private String noteName;

private Note(String noteName) {

this.noteName = noteName;

}

public String toString() { return noteName; }

public static final Note

MIDDLE_C = new Note("Middle C"),

C_SHARP = new Note("C Sharp"),

B_FLAT = new Note("B Flat");

// Etc.

}

///:~

这是一个枚举(enumeration)类,包含固定数目的可供选择的不变对象。不能再产生另外的对象,因为其构造器是私有的。在下面的例子中,Wind是一种 Instrument,因此可以继承 Instrument类。

//: c07:music:Wind.java

package c07.music;

// Wind objects are instruments

// because they have the same interface:

public class Wind extends Instrument {

// Redefine interface method:

public void play(Note n) {

System.out.println("Wind.play() " + n);

}

}

///:~

//: c07:music:Music.java

// Inheritance & upcasting.

package c07.music;

import com.bruceeckel.simpletest.*;

public class Music {

private static Test monitor = new Test();

public static void tune(Instrument i) {

// ...

i.play(Note.MIDDLE_C);

}

public static void main(String[] args) {

Wind flute = new Wind();

tune(flute); // Upcasting

monitor.expect(new String[] {

"Wind.play() Middle C"

});

}

}

///:~

Music.tune( )方法接受一个 Instrument引用参数,同时也接受任何导出自 Instrument 的类。在 Main()方法中,当一个 Wind引用传递到 tune()方法时,就会出现这种情况,而不需要任何类型转换。这样做是允许的――因为 Wind从 Instrument继承而来,所以

Instrument的接口必定存在于 Wind中。从 Wind向上转型到 Instrument可能会“缩小”接口,但无论如何也不会比 Instrument的全部接口更窄。

忘记对象类型

Music.java这个程序看起来似乎有些奇怪。为什么所有人都应该故意忘记一个对象的类型呢?在进行向上转型时,就会产生这种情况;并且如果让 tune()方法直接接受一个 Wind 引用作为自己的参数,似乎会更为直观。但这样会引发的一个重要问题是:如果你那样做,就需要为系统内 Instrument的每种类型都编写一个新的 tune()方法。假设按照这种推理,现在再加入 Stringed(弦乐)和 Brass(管乐)这两种 Instrument(乐器):

//: c07:music:Music2.java

// Overloading instead of upcasting.

package c07.music;

import com.bruceeckel.simpletest.*;

class Stringed extends Instrument {

public void play(Note n) {

System.out.println("Stringed.play() " + n);

}

}

class Brass extends Instrument {

public void play(Note n) {

System.out.println("Brass.play() " + n);

}

}

public class Music2 {

private static Test monitor = new Test();

public static void tune(Wind i) {

i.play(Note.MIDDLE_C);

}

public static void tune(Stringed i) {

i.play(Note.MIDDLE_C);

}

public static void tune(Brass i) {

i.play(Note.MIDDLE_C);

}

public static void main(String[] args) {

Wind flute = new Wind();

Stringed violin = new Stringed();

Brass frenchHorn = new Brass();

tune(flute); // No upcasting

tune(violin);

tune(frenchHorn);

monitor.expect(new String[] {

"Wind.play() Middle C",

"Stringed.play() Middle C",

"Brass.play() Middle C"

});

}

}

///:~

这样做行得通,但有一个主要缺点:必须为添加的每一个新 Instrument类编写特定类型

的方法。这意味着在开始时就需要更多的编程,这也意味着如果以后想添加类似 Tune()的新方法,或者添加自 Instrument导出的新类,仍需要做大量的工作。此外,如果我们

忘记重载某个方法,编译器不会返回任何错误信息,这样关于类型的整个处理过程就变得难以操纵。

如果我们只写这样一个简单方法,它仅接收基类作为参数,而不是那些特殊的导出类。这样做情况会变得更好吗?也就是说,如果我们不管导出类的存在,编写的代码只是与基类打交道,会不会好呢?

这正是多态所允许的。然而,大多数程序员具有面向过程程序设计的背景,对多态的运作方式可能会感到有一点迷惑。

曲解

运行 Music.java这个程序后,我们便会发现难点所在。Wind.play( )方法将产生输出结果。这无疑是我们所期望的输出结果,但它看起来似乎又没有什么意义。请观察一下 tune()方法:

public static void tune(Instrument i) {

// ...

i.play(Note.MIDDLE_C);

}

它接受一个 Instrument引用。那么在这种情况下,编译器怎样才可能知道这个Instrument 引用指向的是 Wind对象,而不是 Brass对象或 Stringed对象呢?实际上,编译器无法得知。为了深入理解这个问题,有必要研究一下“绑定( binding)”这个话题。

方法调用绑定

将一个方法调用同一个方法主体关联起来被称作“绑定( binding)”。若在程序执行前进行绑定(如果有的话,由编译器和链接程序实现),叫做“前期绑定(early binding)”。

可能你以前从来没有听说过这个术语,因为它是面向过程的语言中不需要选择就默认的绑定方式。C编译器只有一种方法调用,那就是前期绑定。

上述程序之所以令人迷惑,主要是因为提前绑定。因为,当编译器只有一个Instrument引用时,它无法知道究竟调用哪个方法才对。

解决的办法叫做“后期绑定( late binding)”,它的含义就是在运行时,根据对象的类型进行绑定。后期绑定也叫做“动态绑定(dynamic binding)”或“运行时绑定(run-time binding)”。如果一种语言想实现后期绑定,就必须具有某些机制,以便在运行时能判断

对象的类型,以调用恰当的方法。也就是说,编译器仍不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是我们只要想象一下就会得知,不管怎样都必须在对象中安置某种“类型信息”。

Java中除了 static和 final方法(private方法属于 final)之外,其他所有的方法都是后期绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定---它会自动发生。

为什么要将某个方法声明为 final呢?正如前一章提到的那样,它可以防止其他人重载该方法。但更重要的一点或许是:这样做可以有效地“关闭”动态绑定,或者是想告诉编译器不需要对其进行动态绑定。这样,编译器就可以为 final方法调用生成更有效的代码。然而,大多数情况下,这样做对我们程序的整体性能不会产生什么改观。所以,最好根据设计来决定是否使用 final,而不是出于试图提高性能。

产生正确的行为

一旦知道 Java中所有方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只与基类打交道的程序代码了,并且这些代码对所有的导出类都可以正确运行。或者换种说法,发送消息给某个对象,让该对象去断定应该做什么事。

面向对象程序设计中,有一个最经典的“几何形状(shape)”例子。因为它很容易被可

视化,所以经常用到;但不幸的是,它可能使初学者认为面向对象程序设计仅适用于图形化程序设计,实际当然不是这种情形了。

在“几何形状”这个例子中,包含一个 Shape基类和多个导出类,如:Circle, Square, Triangle等。这个例子之所以好用,是因为我们可以说“圆是一种形状”,这种说法也很

容易被理解。下面的继承图展示了它们之间的关系:

向上转型可以像下面这条语句这么简单:

Shape s = new Circle();

这里,创建了一个 Circle对象,并把得到的引用立即赋值给 Shape,这样做看似错误(将一种类型赋值给另一类型);但实际上是没问题的,因为通过继承,Circle就是一种Shape。因此,编译器认可这条语句,也就不会产生错误信息。

假设我们调用某个基类方法(已被导出类所重载):

s.draw();

同样地,我们可能会认为调用的是 shape的 draw(),因为这毕竟是一个 shape引用,

那么编译器是怎样知道去做其他的事情呢?由于后期绑定(多态),程序还是正确调用了Circle.draw( )方法。

下面的例子稍微有所不同:

//: c07:Shapes.java

// Polymorphism in Java.

import com.bruceeckel.simpletest.*;

import java.util.*;

class Shape {

void draw() {}

void erase() {}

}

class Circle extends Shape {

void draw() {

System.out.println("Circle.draw()");

}

void erase() {

System.out.println("Circle.erase()");

}

}

class Square extends Shape {

void draw() {

System.out.println("Square.draw()");

}

void erase() {

System.out.println("Square.erase()");

}

}

class Triangle extends Shape {

void draw() {

System.out.println("Triangle.draw()");

}

void erase() {

System.out.println("Triangle.erase()");

}

}

// A "factory" that randomly creates shapes: class RandomShapeGenerator {

private Random rand = new Random();

public Shape next() {

switch(rand.nextInt(3)) {

default:

case 0: return new Circle();

case 1: return new Square();

case 2: return new Triangle();

}

}

}

public class Shapes {

private static Test monitor = new Test();

private static RandomShapeGenerator gen =

new RandomShapeGenerator();

public static void main(String[] args) {

Shape[] s = new Shape[9];

// Fill up the array with shapes:

for(int i = 0; i < s.length; i++)

s[i] = gen.next();

// Make polymorphic method calls:

for(int i = 0; i < s.length; i++)

s[i].draw();

monitor.expect(new Object[] {

new TestExpression("%% (Circle|Square|Triangle)"

+ "\\.draw\\(\\)", s.length)

});

}

}

///:~

Shape基类为自它那里继承而来的所有导出类,建立了一个通用接口——也就是说,所有形状都可以描绘和擦除。导出类重载了这些定义,以便为每种特殊类型的几何形状提供独特的行为。

RandomShapeGenerator是一种“工厂(factory)”,在我们每次调用 next()方法时,它可以为随机选择的 shape对象产生一个引用。请注意向上转型是在 return语句里发生的。每个 return语句取得一个指向某个 Circle、Square或者 Triangle的句柄,并将其以Shape类型从 next()方法中发送出去。所以无论我们在什么时候调用 next()方法时,是绝对没有可能知道它所获的具体类型到底是什么,因为我们总是只能获得一个通用的Shape引用。

main()包含了 Shape句柄的一个数组,通过调用 RandomShapeGenerator.next( )来填入数据。此时,我们只知道自己拥有一些 Shape,不会知道除此之外的更具体情况(编译器一样不知)。然而,当我们遍历这个数组,并为每个数组元素调用 draw()方法时,与各类型有关的专属行为竟会神奇般地正确发生,我们可以从运行该程序时,产生的输出结果中发现这一点。

随机选择几何形状是为了让大家理解:在编译期间,编译器不需要获得任何特殊的信息,就能进行正确的调用。对 draw()方法的所有调用都是通过动态绑定进行的。

扩展性

现在,让我们返回到乐器( Instrument)示例。由于有多态机制,我们可根据自己的需求向系统中添加任意多的新类型,而不需更修改 true()方法。在一个设计良好的 OOP程序中,

我们的大多数或者所有方法都会遵循 tune()的模型,而且只与基类接口通信。我们说这样的程序是“可扩展的”,因为我们可以从通用的基类继承出新的数据类型,从而新添一些功能。那些操纵基类接口的如方法不需要任何改动就可以应用于新类。

考虑一下:对于乐器例子,如果我们向基类中添加更多的方法,并加入一些新类,将会出现什么情况呢?如下图所示:

事实上,不需要改动 tune()方法,所有的新类都能与原有类一起正确运行。即使 tune ()方法是存放在某个单独文件中,并且在 Instrument接口中还添加了其他的新方法,tune()也不需再编译就仍能正确运行。下面是上述示意图的具体实现:

//: c07:music3:Music3.java

// An extensible program.

package c07.music3;

import com.bruceeckel.simpletest.*;

import c07.music.Note;

class Instrument {

void play(Note n) {

System.out.println("Instrument.play() " + n);

}

String what() { return "Instrument"; }

void adjust() {}

}

class Wind extends Instrument {

void play(Note n) {

System.out.println("Wind.play() " + n);

}

String what() { return "Wind"; }

void adjust() {}

}

class Percussion extends Instrument {

void play(Note n) {

System.out.println("Percussion.play() " + n);

}

String what() { return "Percussion"; }

void adjust() {}

}

class Stringed extends Instrument {

void play(Note n) {

System.out.println("Stringed.play() " + n);

}

String what() { return "Stringed"; }

void adjust() {}

}

class Brass extends Wind {

void play(Note n) {

System.out.println("Brass.play() " + n);

}

void adjust() {

System.out.println("Brass.adjust()");

}

}

class Woodwind extends Wind {

void play(Note n) {

System.out.println("Woodwind.play() " + n); }

String what() { return "Woodwind"; }

}

public class Music3 {

private static Test monitor = new Test();

// Doesn't care about type, so new types

// added to the system still work right: public static void tune(Instrument i) {

// ...

i.play(Note.MIDDLE_C);

}

public static void tuneAll(Instrument[] e) { for(int i = 0; i < e.length; i++)

tune(e[i]);

}

public static void main(String[] args) {

// Upcasting during addition to the array:

Instrument[] orchestra = {

new Wind(),

new Percussion(),

new Stringed(),

new Brass(),

new Woodwind()

};

tuneAll(orchestra);

monitor.expect(new String[] {

"Wind.play() Middle C",

"Percussion.play() Middle C",

"Stringed.play() Middle C",

"Brass.play() Middle C",

"Woodwind.play() Middle C"

});

}

///:~

新添加的方法 what()返回一个 String引用及类的描述说明;另一个新添加的方法 adjust ()则提供每种乐器的调音方法。

在 main()中,当我们将某种引用置入orchestra数组中,就会自动向上转型到Instrument。可以看到,tune()方法完全可以忽略它周围代码所发生的全部变化,依旧正常运行。这正是期望多态所具有的特性。我们所作的代码修改,不会对程序中其他不应受到影响的部分产生破坏。从另一方面说就是,多态是一项让程序员“将改变的事物与未变的事物分离开来”的重要技术。

缺陷:“重载”私有方法

我们试图这样做也是无可厚非的:

//: c07:PrivateOverride.java

// Abstract classes and methods.

import com.bruceeckel.simpletest.*;

public class PrivateOverride {

private static Test monitor = new Test();

private void f() {

System.out.println("private f()");

}

public static void main(String[] args) {

PrivateOverride po = new Derived();

po.f();

monitor.expect(new String[] {

"private f()"

});

}

}

class Derived extends PrivateOverride {

public void f() {

System.out.println("public f()");

}

} ///:~

我们所期望的输出是“public f( )”,但是由于 private方法被自动认为就是 final方法,而且对导出类是屏蔽的。因此,在这种情况下,Derived类中的 f( )方法就是一个全新的方法;既然基类中 f( )方法在子类 Derived中不可见,因此也就没有被重载。

结论就是:只有非 private方法才可以被重载;但是我们还需要密切注意重载 private方法的现象,虽然编译不会出错,但是不会按照我们所期望的来执行。明白地说,在导出类中,对于基类中的 private方法,我们最好用一个不同的名字。

抽象类和抽象方法

在所有乐器的例子中,基类 Instrument中的方法往往是“哑( dummy)”方法。若要调

用这些方法,就会出现一些错误。这是因为 Instrument类的目的是为它的所有导出类创

建一个通用接口。

建立这个通用接口的唯一原因是,不同的子类可以用不同的方式表示此接口。它建立起一个基本形式,用来表示所有导出类的共同部分。另一种说法是将 Instrument类称作“抽象

基类”(或简称抽象类)。当我们想通过这个通用接口操纵一系列类时,就需创建一个抽象类。与任何基类所声明的签名相符的导出类方法,都会通过动态绑定机制来调用。(然而,正如前一节所讲,如果方法名与基类中的相同,但是参数不同,就会出现重载,这或许不是我们想要的)

如果我们只有一个像 Instrument这样的抽象类,那么该类的对象几乎没有任何意义。也

就是说,Instrument只是表示了一个接口,没有具体的实现内容;因此,创建一个Instrument对象没有什么意义,并且我们可能还想阻止使用者这样做。通过在 Instrument 的所有方法中打印出错误信息,就可以实现这个目的。但是这样做会将错误信息延迟到运行期才可获得,并且需要在客户端进行可靠、详尽的测试。所以最好是在编译期间捕获这些问题。

为此,Java提供一个叫做“抽象方法( abstract method)”的机制。这种方法是不完整的;仅有声明而没有方法体。下面是抽象方法声明所采用的语法:

abstract void f();

包含抽象方法的类叫做“抽象类 (abstract class)”。如果一个类包含一个或多个抽象方法,该类必须被限制为是抽象的。(否则,编译器就会报错)

如果一个抽象类不完整,那么当我们试图产生该类的对象时,编译器会怎样处理呢?由于为一个抽象类创建对象是不安全的,所以我们会从编译器那里得到一条出错信息。这里,编译器会确保抽象类的纯粹性,我们不必担心会误用它。

如果从一个抽象类继承,并想创建该新类的对象,那么我们就必须为基类中的所有抽象方法提供方法定义。如果不这样做(可以选择不做),那么导出类便也是抽象类,且编译器将会强制我们用 abstract关键字来限制修饰这个类。

我们也可能会创建一个没有任何抽象方法的抽象类。考虑这种情况:如果我们有一个类,让其包含任何 abstract方法都显得没有实际意义,但是我们却想要阻止产生这个类的任何对象,那么这时这样做就很有用了。

Instrument类可以很容易地转化成抽象类。既然使某个类成为抽象类并不需要所有的方

法都是抽象的,所以仅需将某些方法声明为抽象的即可。下面所示是它的模样:

对于C++程序设计员来说,这相当于 C++语言中的纯虚函数。

下面是修改过的“管弦乐队”的例子,其中采用了抽象类和抽象方法:

//: c07:music4:Music4.java

// Abstract classes and methods.

package c07.music4;

import com.bruceeckel.simpletest.*;

import java.util.*;

import c07.music.Note;

abstract class Instrument {

private int i; // Storage allocated for each

public abstract void play(Note n);

public String what() {

return "Instrument";

}

public abstract void adjust();

}

class Wind extends Instrument {

public void play(Note n) {

System.out.println("Wind.play() " + n);

}

public String what() { return "Wind"; }

public void adjust() {}

}

class Percussion extends Instrument {

public void play(Note n) {

System.out.println("Percussion.play() " + n);

}

public String what() { return "Percussion"; }

public void adjust() {}

}

class Stringed extends Instrument {

public void play(Note n) {

System.out.println("Stringed.play() " + n);

}

public String what() { return "Stringed"; }

public void adjust() {}

}

class Brass extends Wind {

public void play(Note n) {

System.out.println("Brass.play() " + n);

}

public void adjust() {

System.out.println("Brass.adjust()");

}

}

class Woodwind extends Wind {

public void play(Note n) {

System.out.println("Woodwind.play() " + n);

}

public String what() { return "Woodwind"; }

}

public class Music4 {

private static Test monitor = new Test();

// Doesn't care about type, so new types

// added to the system still work right:

static void tune(Instrument i) {

// ...

i.play(Note.MIDDLE_C);

}

static void tuneAll(Instrument[] e) {

for(int i = 0; i < e.length; i++)

tune(e[i]);

}

public static void main(String[] args) {

// Upcasting during addition to the array:

Instrument[] orchestra = {

new Wind(),

new Percussion(),

new Stringed(),

new Brass(),

new Woodwind()

};

tuneAll(orchestra);

monitor.expect(new String[] {

"Wind.play() Middle C",

"Percussion.play() Middle C",

"Stringed.play() Middle C",

"Brass.play() Middle C",

"Woodwind.play() Middle C"

});

}

}

///:~

我们可以看出,除了基类,实际上并没有什么改变。

创建抽象类和抽象方法非常有用,因为它们可以显化一个类的抽象性,并告诉用户和编译器

怎样按照它所预期的方式来使用。

构造器和多态

通常,构造器不同于其他种类的方法。即使涉及到多态,也仍是如此。尽管构造器并不具有多态性(它们实际上是 Static方法,只不过该 Static声明是隐式的),但还是非常有必要理解构造器怎样通过多态在复杂的层次结构中运作。这一理解将有助于大家避免一些令人不快的困扰。

构造器的调用顺序

构造器的调用顺序已在第 4章进行了简要说明,并在第 6章再次提到,但那些都是在多态引入之前介绍的。

基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确地构造。导出类只能访问它自己的成员,不能访问基类中的成员(基类成员通常是 private类型)。只有基类的构造器才具有恰当的知识和权限对自己的元素进行初始化。

因此,必须令所有构造器都得到调用,否则所有对象就不可能被正确构造。这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。在导出类的构造器主体中,如果我们没有明确指定调用某个基类构造器,它就会“默默”地调用缺省构造器。如果不存在缺省构造器,编译器就会报错(若某个类没有构造器,编译器会自动合成出一个缺省构造器)。

让我们来看下面这个例子,它展示了组合、继承以及多态的在构建顺序上的效果:

//: c07:Sandwich.java

// Order of constructor calls.

package c07;

import com.bruceeckel.simpletest.*;

class Meal {

Meal() { System.out.println("Meal()"); }

}

class Bread {

Bread() { System.out.println("Bread()"); }

}

class Cheese {

Cheese() { System.out.println("Cheese()"); }

}

class Lettuce {

Lettuce() { System.out.println("Lettuce()"); }

}

class Lunch extends Meal {

Lunch() { System.out.println("Lunch()"); }

}

class PortableLunch extends Lunch {

PortableLunch() { System.out.println("PortableLunch()");}

}

public class Sandwich extends PortableLunch {

private static Test monitor = new Test();

private Bread b = new Bread();

private Cheese c = new Cheese();

private Lettuce l = new Lettuce();

public Sandwich() {

System.out.println("Sandwich()");

}

public static void main(String[] args) {

new Sandwich();

monitor.expect(new String[] {

"Meal()",

"Lunch()",

"PortableLunch()",

"Bread()",

"Cheese()",

"Lettuce()",

"Sandwich()"

});

}

}

///:~

在这个例子中,用其他类创建了一个复杂的类,而且每个类都有一个它声明自己的构造器。其中最重要的类是 Sandwich,它反映出了三层级别的继承(若将从 Object的隐含继承也算在内,就是四级)以及三个成员对象。当在 main()里创建一个 Sandwich对象后,我们就可以看到输出结果。这也表明了这一复杂对象调用构造器要遵照下面的顺序:

1. 调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,

然后是下一层导出类,等等。直到最低层的导出类。

2. 按声明顺序调用成员的初始状态设置模块。

3. 调用导出类构造器的主体。构造器的调用顺序是很重要的。当进行继承时,我们已经知道基类的一切,并且可以访问基类中任何声明为 public和 protected的成员。这意味着在导出类中,必须假定基类的所有成员都是有效的。一种标准方法是,构造动作一经发生,那么对象所有部分的全体成员都会得到构建。然而,在构造器内部,我们必须确保所要使用的成员都已经构建完毕。为确保这一目的,唯一的办法就是首先调用基类构造器。那么在进入导出类构造器时,在基类中可供我们访问的成员都已得到初始化。此外,在构造器中的所有成员必须有效也是因为当成员对象在类内进行定义的时候(比如上例中的 b,c和 l),我们应尽可能地对它们进行初始化(也就是,通过组合方法将对象置于类内)。若遵循这一规

则,那么我们就能确定所有基类成员以及当前对象的成员对象都已初始化。但遗憾的是,这种做法并不适用于所有情况,我们会在下一节看到。

继承与清除

通过组合和继承方法来创建新类时,我们永远不必担心对象的清除问题,子对象(subobject)通常都会留给垃圾回收器进行处理。如果要是遇到清除的问题,那么我们必须不断地为我们的新类创建 dispose( )方法(在这里我选用此名称;你可以提出更好的)。并且由于继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清除动作,就必须重载导出类中的dispose( )方法。重载继承类的 dispose( )方法时,务必记住调用基类版本 dispose( )方法。否则,基类的清除动作就不会发生。下例将予以证明:

//: c07:Frog.java

// Cleanup and inheritance.

import com.bruceeckel.simpletest.*;

class Characteristic {

private String s;

Characteristic(String s) {

this.s = s;

System.out.println("Creating Characteristic " + s);

}

protected void dispose() {

System.out.println("finalizing Characteristic " + s);

}

}

class Description {

private String s;

Description(String s) {

this.s = s;

System.out.println("Creating Description " + s);

}

protected void dispose() {

System.out.println("finalizing Description " + s);

}

}

class LivingCreature {

private Characteristic p = new Characteristic("is alive");

private Description t =

new Description("Basic Living Creature");

LivingCreature() {

System.out.println("LivingCreature()");

}

protected void dispose() {

System.out.println("LivingCreature dispose");

t.dispose();

p.dispose();

}

}

class Animal extends LivingCreature {

private Characteristic p= new Characteristic("has heart"); private Description t = new Description("Animal not Vegetable"); Animal() {

System.out.println("Animal()");

}

protected void dispose() {

System.out.println("Animal dispose");

t.dispose();

p.dispose();

super.dispose();

}

}

class Amphibian extends Animal {

private Characteristic p =

new Characteristic("can live in water");

private Description t =

new Description("Both water and land");

Amphibian() {

System.out.println("Amphibian()");

}

protected void dispose() {

System.out.println("Amphibian dispose");

t.dispose();

p.dispose();

super.dispose();

}

}

public class Frog extends Amphibian {

private static Test monitor = new Test();

private Characteristic p = new Characteristic("Croaks");

private Description t = new Description("Eats Bugs");

public Frog() {

System.out.println("Frog()");

}

protected void dispose() {

System.out.println("Frog dispose");

t.dispose();

p.dispose();

super.dispose();

}

public static void main(String[] args) {

Frog frog = new Frog();

System.out.println("Bye!");

frog.dispose();

monitor.expect(new String[] {

"Creating Characteristic is alive",

"Creating Description Basic Living Creature",

"LivingCreature()",

"Creating Characteristic has heart",

"Creating Description Animal not Vegetable",

"Animal()",

"Creating Characteristic can live in water",

"Creating Description Both water and land",

"Amphibian()",

"Creating Characteristic Croaks",

"Creating Description Eats Bugs",

"Frog()",

"Bye!",

"Frog dispose",

"finalizing Description Eats Bugs",

"finalizing Characteristic Croaks",

"Amphibian dispose",

"finalizing Description Both water and land",

"finalizing Characteristic can live in water",

"Animal dispose",

"finalizing Description Animal not Vegetable",

"finalizing Characteristic has heart",

"LivingCreature dispose",

"finalizing Description Basic Living Creature",

"finalizing Characteristic is alive"

});

}

}

///:~

层次结构中的每个类都包含 Characteristic和 Description这两种类型的成员对象,并且也必须对它们进行处理。所以万一某个子对象要依赖于其他对象,处理的顺序应该和初始化顺序相反。对于属性,则意味着与声明的顺序相反(因为属性的初始化是按照声明的顺序进行)。对于基类(遵循 C++中析构函数的形式),我们应该首先对其导出类进行清除,然后才是基类。这是因为导出类的清除可能会调用基类中的某些方法,所以需要使基类中的构件仍起作用而不应过早地销毁她。从输出结果我们可以看到,Frog对象的所有部分都是按照创建的逆序进行销毁。

在这个例子中可以看到,尽管我们通常不必执行清除处理,但是一旦你选择要执行,就必须谨慎和小心。

构造器内部的多态方法的行为

构造器调用的层次结构带来了一个有趣的两难问题。如果在一个构造器的内部,同时调用正在构造的那个对象的某个动态绑定方法,那会发生什么情况呢?在一般的方法内部,我们可以想象会发生什么:动态绑定的调用是在运行期才被决定,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。为保持一致性,大家也许会认为这应该发生在构造器内部。

但事情并非完全如此。如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被重载的定义。然而,产生的效果可能相当难于预料,并且可能造成一些难于发现的隐藏错误。

从概念上讲,构造器的工作实际上是创建对象(这并非是一件平常的工作)。在任何构造器内部,整个对象可能只有部分形成——我们只知道基类对象已经进行初始化,但却不知道哪些类是从我们这里继承而来的。然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部。它可以调用导出类里的方法。如果我们是在构造器内部这样做,那么我们可能会调用某个方法,而这个方法所操纵的成员可能还未进行初始化——这肯定是招惹灾难的端倪。

通过下面这个例子,我们会看到问题所在:

//: c07:PolyConstructors.java

// Constructors and polymorphism

// don't produce what you might expect.

import com.bruceeckel.simpletest.*;

abstract class Glyph {

abstract void draw();

Glyph() {

System.out.println("Glyph() before draw()");

draw();

System.out.println("Glyph() after draw()");

}

}

class RoundGlyph extends Glyph {

private int radius = 1;

RoundGlyph(int r) {

radius = r;

System.out.println(

"RoundGlyph.RoundGlyph(), radius = " + radius);

}

void draw() {

System.out.println(

"RoundGlyph.draw(), radius = " + radius);

}

}

public class PolyConstructors {

private static Test monitor = new Test();

public static void main(String[] args) {

new RoundGlyph(5);

monitor.expect(new String[] {

"Glyph() before draw()",

"RoundGlyph.draw(), radius = 0",

"Glyph() after draw()",

"RoundGlyph.RoundGlyph(), radius = 5"

});

}

}

///:~

在Glyph中,draw()方法是抽象的,是为了让其他方法重载。事实上,我们在 RoundGlyph 中被迫对其进行重载。但是Glyph构造器会调用这个方法,而且调用会在RoundGlyph.draw()中结束,这看起来似乎是我们的目的。但是如果我们看到输出结果,我们会发现当 Glyph的构造器调用 draw()方法时, radius不是默认初始值 1,而是 0。这可能导致在屏幕上只画了一个点,或是根本什么东西都没有;我们只能干瞪眼,试图找出程序无法运转的原因所在。

前一节讲述的初始化顺序并不十分完整,而这正是解决这一谜题的关键所在。初始化的实际过程是:

1. 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。

2. 如前所述的那样,调用基类构造器。此时,调用被重载的 draw()方法(是的,是在调用RoundGlyph构造器之前调用的),由于步骤 (1)的缘故,我们此时会发现 radius的值为 0。

3. 按照声明的顺序调用成员的初始化代码。

4. 调用导出类的构造器主体。这样做有一个优点,那就是所有东西都至少初始化成零(或者是某些特殊数据类型中与“零”等价的值),而不是仅仅留作垃圾。其中包括通过“组合”而嵌入一个类内部的对象引用。其值是 null。所以如果忘记为该引用进行初始化,就会在运行期间抛出异常。查看输出结果时,我们会发现其他所有东西的值都会是零,这通常也正是发现问题的证据。

另一方面,我们应该对这个程序的结果相当震惊。在逻辑方面,我们做的已经十分完美,而它的行为却不可思议地错了,并且编译器也没有报错。(在这种情况下,C++语言会出现更合理的行为)。诸如此类的错误会很容易地被人忽略,而且要花很长的时间才能发现。

因此,编写构造器时有一条有益的规则:“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法”。在构造器内唯一能够安全调用的那些方法是基类中的 final 方法(也适用于 private方法,它们自动属于 final方法)。这些方法不能被重载,因此也

JAVA面向对象基础测试题,继承,封装,多态等测试题

JAVA面向对象基础测试题 提示:本题为第一阶段,JAVA面向对象基础部分练习题,包括对象,类,继承,封装,多态,接口,内部类等等,java核心基础,适合初学者对面向对象基础的知识进行测试,以便查漏补缺。 1. 程序执行的结果是:()。 01 public class Point{ 02 int y = 7; 03 public void step(int y) { 04 y += y; 05 System.out.println(y); 06 } 07 public static void main(String[] args) { 08

Point p = new Point(); 09 p.step(10); 10 } 11 } A.14 B.20 C.10 D.17 正确答案:B解析: 2. 程序的执行结果是:()。 01 public class Question { 02 private int num; 03 public static void main(String [] args){ 04 Question q = new Question();

05 q.num=13; 06 update(q); 07 System.out.println(q.num); 08 } 09 public static void update(Question q){ 10 q.num=9; 11 } 12 } A.13 B.9 C.0 D.4 正确答案:B解析: 3.

程序执行的结果是:()。 01 public class Answer { 02 public static void main(String[] args) { 03 int score = 20; 04 Answer ans= new Answer(); 05 ans.add(score); 06 System.out.println(" main:score = " + score); 07 } 08 void add(int score) { 09 System.out.println(" add:score=" + score++); 10 } 11

如何体现封装、继承、多态 编程人员看看

如何体现封装、继承、 编程人员看看! 如何体现封装、继承、多态 编程人员看看!! ! 什么事封装? 1. 保护数据成员, 不让类以外的程序直接访问或 修改,只能通过提供的公共的接口访问==>数据封 装。 2. 方法的细节对用户是隐藏的,只要接口不变, 内容的修改不会影响到外部的调用者==>方法封装。 3. 当对象含有完整的属性和与之对应的方法时称为 封装。 4. 从对象外面不能直接访问对象的属性, 只能通 过和该属性对应的方法访问。 5. 对象的方法可以接收对象外面的消息。 比如: Class A { private int prop; puplic int getProp() { return prop; }

public void setProp(int prop) { this.prop = prop; } } 属性 prop 是 private 的,外界不能直接访问, 但是外界可以通过调用 getProp()和 setProp()的方 法, 给对象发消息,从而完成某种功能。 什么事多态? 多态性的概念经常被说成事“一个接口,多种方 法”。这意味着可以为一组相关的动作作设计一个通 用 的接口。多态允许同一个接口被必于同一个类的多个 动作使用,这样就降低了程序的复杂性。再拿狗作比 喻, 一条狗的嗅觉是多态的。如果狗闻到猫的气味,它会 在吠叫并且追着它跑。如果狗闻到食物的气味,它将 分 泌唾液并向盛着食物的碗跑去。两种状况下同一种嗅 觉器官在工作,差别在于问到了什么气味,也就是有

两 种不同类型的数据作用于狗的鼻子!在 java 中,同一 个类中的 2 个或 2 个以上的方法可以有同一个名字, 只要 参数声明不同即可。在这种情况下,该方法就被称为 重载(Overload),这个过程称为方法重载(Method overloading)。方法重载是 java 实现多态的一种方 式。 有两种方式可以实现多态:* 1. 继承(子类继承父类(包括 abstract class,interf ace ect)) 2. 重载(同一个类中) 如果是面向对象程序设计的话,面向对象程序设 计中的另外一个重要概念是多态性。在运行时,通过 指向 基类的指针,来调用实现派生类中的方法。可以把一 组对象放到一个数组中,然后调用它们的方法,在这 种场 合下,多态性作用就体现出来了,这些对象不必是相 同类型的对象。当然它们都继承自某个类,你可以把 这些 派生类都放到一个数组中。如果这些对象都有同名方

第七章继承多态练习题

第七章继承多态 一、选择题: 1、分析: class A { A() { } } class B extends A { //系统自动生成的构造方法和类的访问权限一样 } 哪两种说法是正确的? ( ) A:类B的构造方法是public的. B:类B的构造方法包含对this()的调用. C:类B的构造方法没有参数. D:类B的构造方法包含对super()的调用. 2、运行结果是:() class Base { Base() { System.out.print("Base"); } } public class Alpha extends Base { public static void main( String[] args ) { new Alpha(); new Base(); } } A: Base B: BaseBase C: 编译失败. D: 没有输出. E: 运行时异常. 3. 程序的运行结果是?() A: 编译失败. B: hello from a C: hello from b D: hello from b E: hello from a hello from a hello from b

4. 运行结果是:() class TestSuper { TestSuper(int i) { } } class TestSub extends TestSuper{ } class TestAll { public static void main (String [] args) { new TestSub(); } } A: 编译失败. B: 程序运行没有异常. C: 第7行抛出异常. D: 第2行抛出异常. 5. 程序的运行结果是?() A: 0 B: 1 C: 2 D: 编译失败. 6. 对于语句"B is a D" 和"B has a C",一下哪两种说法是正确的? ( ) A:D是B. B:B是D. C:D是C. D:B是C. E:D继承B. F:B 继承D. 7. 运行结果是?()

C 的封装性、继承性和多态性概念

C++的封装性、继承性和多态性概念 封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,一特定的访问权限来使用类的成员。例如,在抽象的基础上,我们可以将时钟的数据和功能封装起来,构成一个时钟类。按c++的语法,时钟类的声明如下:class Clock { public: //共有成员,外部借口void SetTime(int NewH,int NewM,int NewS); void ShowTime(); private: //私有成员,外部无法访问int Hour,Minute,Second; } 可以看到通过封装使一部分成员充当类与外部的接口,而将其他的成员隐蔽起来,这样就达到了对成员访问权限的合理控制,使不同类之间的相互影响减少到最低限度,进而增强数据的安全性和简化程序的编写工作。什么是多态(Polymorphisn)?按字面的意思就是“多种形状”。引用Charlie Calverts对多态的描述——多态性是允许你将父对象设置成为和一个或更多的他的子对象相等 的技术,赋值之后,>>>父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作<<<(摘自“Delphi4 编程技术内幕”)。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。多态性在Object Pascal和C++中都是通过虚函数(Virtual Function)实现的。好,接着是“虚函数”(或者是“虚方法”)。虚函数就是允许被其子类重新定

义的成员函数。而子类重新定义父类虚函数的做法,称为“覆盖”(override),或者称为“重写”。“继承”是面向对象软件技术当中的一个概念。如果一个类A继承自另一个类B,就把这个A称为"B的子类",而把B称为"A的父类"。继承可以使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码。在令子类继承父类的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类的原有属性和方法,使其获得与父类不同的功能。 ... 继承是指一个对象直接使用另一对象的属性和方法。事实上,我们遇到的很多实体都有继承的含义。例如,若把汽车看成一个实体,它可以分成多个子实体,如:卡车、公共汽车等。这些子实体都具有汽车的特性,因此,汽车是它们的"父亲",而这些子实体则是汽车的"孩子"。19. 多态的作用?主要是两个:1. 隐藏实现细节,使得代码能够模块化;扩展代码模块,实现代码重用; 2. 接口重用:为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用。

第七章派生与继承2

第七章派生与继承2 /*7.4多继承与虚基类 7.4.1多继承中的二义性问题 解决方式 1<对象名>.<基类名>::<成员名> //数据成员 <对象名>.<基类名>::<成员名>(<参数名>) //成员函数 */ /*#include using namespace std; class Base1 { public: int date; void fun(){cout<<"Member of Base1"<<";base1="<

} } 上述代码在第一阶段Java的课程中经常见到,大致一看没什么问题,但是仔细分析过之后会发现:把年龄设置成1000合理吗? 由于Person类的属性都是公有的(public),那也就意味着在Person类的外部,通过Person类的实例化对象可以对这些公有属性任意修改,这就使得我们无法对类的属性进行有效的保护和控制。这属于设计上的缺陷,那能不能避免这种情况呢?这就需要用到下面的封装了。 1.1.2现实生活中的封装 现实生活中封装的例子随处可见,例如药店里出售的胶囊类药品,我们只需要知道这个胶囊有什么疗效,怎么服用就行了,根本不用关心也不可能去操作胶囊的药物成分和生产工艺。再例如家家户户都用的电视机,我们只需要知道电视机能收看电视节目,知道怎么使用就行了,不用关心也不可能去搞清楚电视机内部都有哪些硬件以及是如何组装的。这些都是现实生活中封装的例子。 在刚才的两个例子中,我们可以认为药物成分是胶囊的属性,但是用户不需要也不可能去操作它。我们也可以认为内部硬件是电视机的属性,但是用户也不需要去操作它。这就是现实生活中封装的特征,程序中的封装与此类似。 1.1.3程序中的封装 封装就是:将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部的信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。简而言之,封装就是将类的属性私有化,并提供公有方法访问私有属性的机制,我们看示例1.2。 示例1.2 public class Person{ //将属性使用private修饰,从而隐藏起来 private String name; private int age; public void sayHello() { System.out.print("你好!"); } } public class Test { public static void main(String[] args) { Person p=new Person(); https://www.doczj.com/doc/c411894872.html,="杰克"; //编译报错 p.age=1000; //编译报错 p.sayHello(); } }

2016级java语言实验3指导(面向对象程序设计(继承、封装、多态))

上机实验三:面向对象程序设计(继承、封装、多态) 类是面向对象程序设计的基础,是Java的核心和本质所在,在Java中,所有的语言元素都封装在类中。编写java程序的过程就是从现实世界中抽象出java可实现的类,并用合适的语句定义它们的过程,本节将学习类的应用,以及如何创建类的实例,通过类的继承更有效的组织程序结构,明确类之间的关系。掌握本节所讲的内容后,读者就可以使用面向对象技术编写java程序了。 接口是特殊的抽象类,只包含常量和方法的定义,而没有方法的实现,也就是说接口是方法定义和常量值的集合。 包是Java语言中有效管理类的一个机制。通过关键字package声明包语句,package语句作为Java源文件的第一条语句,指明该源文件定义的类所在的包。使用import语句可以引入包中的类。 一、实验目的 1)掌握类的定义和使用 2)掌握对象的声明和使用 3)了解构造函数的概念和使用 4)掌握类的继承关系和派生方法 5)掌握多态的概念与使用 6)掌握接口的定义和使用 7)掌握Java中包的应用 二、实验内容 1)类的声明 2)定义类成员变量以及成员方法 3)实例化类、创建类对象以及类方法的调用 4)类的继承 5)通过实例理解接口的定义 6)通过实例熟悉接口的应用 7)正确应用Java中包和import语句 三、实验步骤 1)类和类的实例化 一个类的实现包括两部分:类声明和类体。 (1)、类声明 [public][abstract][final] class className [extends superclassName] [implements interfaceNameList] {……} 期中修饰符[public][abstract][final]说明类的属性 className为类名 superclassName为父类的名字

Java封装、继承、多态

第一章 抽象和封装 1.为什么使用面向对象 面向对象就是采用“现实模拟”的方法设计和开发程序。 面向对象实现了虚拟世界和现实世界的一致性,符合人们的思维习惯,使得客户和软件设计开发人员之间,软件设计开发人员内部交流更加顺畅,同时还带来了代码重用性高、可靠性高等优点,大大提高了软件尤其是大型软件的设计和开发效率 2.使用面向对象进行设计 面向对象设计的过程就是抽象的过程。 根据业务相关的属性和行为,忽略不必要的属性和行为,由现实世界中“对象”抽象出软件开发中的对象 第一步:发现类 第二步:发现类的属性 第三步:发现类的方法 类的基本结构,其主要由属性和行为组成,称为类的成员变量(或者成员属性)和成员方法,统称为类的成员(除此之外类的成员还包括构造方法,代码块等) 对象的创建: 通过够造方法来创建对象。 通过对象名.属性名的方式调用属性 通过对象名.方法名的方式调用方法 Static 可以用来修饰属性、方法和代码快。Static的变量属于这个类所有,即由这个 类创建的所有对象共同用一个 Static 变量。通常把Static修饰的属性和方法称为类 属性(类变量)、类方法。不使用Static修饰的属性和方法,属于单个对象,通常称为 实例属性(实例变量),实例方法。 类属性、类方法可以通过类名和对象名访问,实例属性、实例方法只能通过对象名访问。Final 可以用来修饰属性、方法和类。用final修饰的变量称为常量,其值固定不变。 构造方法的名字和类名相同,没有返回值类型。构造方法的作用主要就是在创建对象时 执行一些初始化操作,如给成员属性赋初值。

在没有给类提供任何构造方法时,系统会提供一个无参的方法体为空的默认构造方法。一旦提供了自定义构造方法,系统将不会再提供这个默认的构造方法,如果要使用,必须手动添加。 如果一个类中包含了两个或两个以上方法,他们的方法名相同,方法参数个数或参数类型不同,则称该方法被重载了,这个过程称为方法重载,成员方法和构造方法都可以进行重载。 常见错误: 在类中可以定义Static变量,在方法里是否可以定义Static变量? 结论:在方法里不可以定义Static变量,也就是说类变量不能是局部变量。 给构造函数加上返回值类型会出现什么情况? 结论:构造方法没有返回值类型,如果有,就不是构造方法,而是和构造方法同名的成员变量。 4.用封装优化类 封装:将类的状态信息隐藏在类内不能,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。 封装的具体步骤:修改属性的可见性来限制对属性的访问;为每个属性创建一对赋值方法(setter)和取值方法(getter),用于对这些属性的存取;在赋值方法中加入对属性的存取控制语句。 封装的好处主要有:隐藏类的实现细节;让使用者只能通过程序员规定的方法来访问数据;可以方便地加入存取控制语句,限制不合理操作。 封装时会用到多个权限控制符来修饰成员变量和方法,区别如下。 Private:成员变量和方法只能在类内被访问,具有类可见性 默认:成员变量和方法只能被同一个包里的类访问,具有包可见性。 Protected:可以被同一个包中的类访问,被同一个项目中不同包中的子类访问Public:可以被同一个项目中所有的类访问,具有项目可见性,这是最大的访问权限 第二章 继承 2.1 继承基础 1.在java中,继承通过extends关键字来实现, 2.父类又叫基类和超类。

20XX级java语言实验3指导(面向对象程序设计(继承、封装、多态))

20XX级java语言实验3指导(面向对象程序设计(继承、封装、多态)) 上机实验三:面向对象程序设计(继承、封装、多态) 类是面向对象程序设计的基础,是Java的核心和本质所在,在Java中,所有的语言元素都封装在类中。编写java 程序的过程就是从现实世界中抽象出java可实现的类,并用合适的语句定义它们的过程,本节将学习类的应用,以及如何创建类的实例,通过类的继承更有效的组织程序结构,明确类之间的关系。掌握本节所讲的内容后,读者就可以使用面向对象技术编写java程序了。 接口是特殊的抽象类,只包含常量和方法的定义,而没有方法的实现,也就是说接口是方法定义和常量值的集合。 包是Java语言中有效管理类的一个机制。通过关键字package声明包语句,package语句作为Java源文件的第一条语句,指明该源文件定义的类所在的包。使用import语句可以引入包中的类。 一、实验目的 1) 2) 3) 4) 5) 6) 7) 掌握类的定义和使用掌握对象的声明和使用 了解构造函数的概念和使用掌握类的继承关系和派生方法掌握多态的概念与使用掌握接口的定义和使用掌握

Java中包的应用 二、实验内容 1) 2) 3) 4) 5) 6) 7) 类的声明 定义类成员变量以及成员方法 实例化类、创建类对象以及类方法的调用类的继承 通过实例理解接口的定义通过实例熟悉接口的应用 正确应用Java中包和import语句 三、实验步骤 1) 类和类的实例化 一个类的实现包括两部分:类声明和类体。、类声明[public][abstract][final] class className [extends superclassName] [implements interfaceNameList] {……} 期中修饰符[public][abstract][final]说明类的属性className为类名 superclassName为父类的名字 interfaceNameList为类实现的接口列表、类体类体定义如下 class className { [public|protected|private] [static] [final] [transient] [volatile] Type variableName; //成员变量

C++习题3(继承和多态)

习题3 一、选择题 1.在C++中,类与类之间的继承关系具有( C ) A)自反性B)对称性C)传递性D)反对称性 2.在公有继承的情况下,基类的成员(私有的除外)在派生类中的访问权限( B ) A)受限制B)保持不变C)受保护D)不受保护 3.按解释中的要求在下列程序划线处填入的正确语句是:( C ) #include class Base{ public: void fun(){cout<<"Base::fun"<fun(); 4.在保护继承的情况下,基类的成员(私有的除外)在派生类中的访问权限( C ) A)受限制B)保持不变C)受保护D)不受保护5.在哪种派生方式中,派生类可以访问基类中的protected 成员(B ) A)public和private B)public、protected和private C)protected和private D)仅protected 6.当一个派生类仅有protected继承一个基类时,基类中的所

有公有成员成为派生类的(C) A) public成员B) private成员C) protected成员 D) 友元 7.不论派生类以何种方法继承基类,都不能使用基类的(B ) A) public成员B) private成员C) protected成员D) public成员和protected成员 8下面叙述错误的是(S )。 A)基类的protected成员在派生类中仍然是protected的 B)基类的protected成员在public派生类中仍然是protected 的 C)基类的protected成员在private派生类中是private的 D)基类的protected成员不能被派生类的对象访问 9.下列说法中错误的是(S )。 A) 保护继承时基类中的public成员在派生类中仍是public 的 B)公有继承时基类中的private成员在派生类中仍是private 的 C)私有继承时基类中的public成员在派生类中是private的 D)保护继承时基类中的public成员在派生类中是protected 的 10下面叙述错误的是(C)。 A)派生类可以使用private派生 B)对基类成员的访问必须是无二义性的 C)基类成员的访问能力在派生类中维持不变 D)赋值兼容规则也适用于多继承的组合 11派生类的构造函数的成员初始化列表中,不能包含(C )。 A)基类的构造函数B)派生类中子对象的初始化 C)基类中子对象的初始化D)派生类中一般数据成员的初始化 12.下列虚基类的声明中,正确的是:( B ) A)class virtual B: public A B)class B: virtual public A

实验七、继承和多态

实验五继承和多态 一.实验目的 1.理解继承的含义,掌握派生类的定义方法和实现; 2.理解公有继承下基类成员对派生类成员和派生类对象的可见性,能正确地访问继承层次中的各种类成员; 3.理解保护成员在继承中的作用,能够在适当的时候选择使用保护成员以便派生类成员可以访问基类的部分非公开的成员;正确使用base关键字; 4.理解虚函数在类的继承层次中的作用,虚函数的引入对程序运行时的影响,能够对使用虚函数的简单程序写出程序结果。 二、实验内容 1.编写一个程序计算出球、圆柱和圆锥的表面积和体积。 要求: (1)定义一个基类圆,至少含有一个数据成员——半径;提供方法来计算其面积并输出其信息; (2)定义基类的派生类球、圆柱、圆锥,根据不同的形状为其添加必须的数据成员(如定义圆柱必须额外给出圆柱的高),并添加或改写其方法成 员,提供各形状求表面积和体积的成员函数,并能正确输出该类对象实 例的信息。 (3)定义主函数,求球、圆柱、圆锥的和体积并输出结果信息。 注意:派生类覆盖基类的同名方法时,如果不是虚方法,需要在前面加关键字new,否则会有警告信息。 public class Circle { private double radius; public Circle () { ……} public Circle (double radiusValue){ this.radius = radiusValue; } public double Radius { ……} public virtual double CircumFerence()//计算周长 { ……}

public virtual double Area()//基类中的面积计算方法 { ……} public override string ToString() { return "圆半径="+radius; } } public class Cylinder : Circle//圆柱继承自圆类 { private double height; public Cylinder() { } public Cylinder(double radiusValue, double heightValue) :base(。。。) { this. height = heightValue; } public override double Area() { … } 2.编写测试用例,对一组不同形状的物体调用其面积和体积的计算,并输出其结果。 提示:在Main方法中定义一个Circle 数组,赋予数组不同的对象(比如第一个元素为圆锥、第二个元素圆柱、第三个球等,然后遍历这个数组,去求取每

相关主题
文本预览
相关文档 最新文档