文档库 最新最全的文档下载
当前位置:文档库 › 第8章_对象的容纳

第8章_对象的容纳

第8章对象的容纳

“如果一个程序只含有数量固定的对象,而且已知它们的存在时间,那么这个程序可以说是相当简单的。”

通常,我们的程序需要根据程序运行时才知道的一些标准创建新对象。除非程序正式运行,否则我们根本不知道自己到底需要多少数量的对象,甚至不知道它们的准确类型。为了满足常规编程的需要,我们要求能在任何时候、任何地点创建任意数量的对象。所以不可依赖一个已命名的句柄来容纳自己的每一个对象,就象下面这样:

MyObject myHandle;

因为根本不知道自己实际需要多少这样的东西。

为解决这个非常关键的问题,Java提供了容纳对象(或者对象的句柄)的多种方式。其中内建的类型是数组,我们之前已讨论过它,本章准备加深大家对它的认识。此外,Java的工具(实用程序)库提供了一些“集合类”(亦称作“容器类”,但该术语已由AWT使用,所以这里仍采用“集合”这一称呼)。利用这些集合类,我们可以容纳乃至操纵自己的对象。本章的剩余部分会就此进行详细讨论。

8.1 数组

对数组的大多数必要的介绍已在第4章的最后一节进行。通过那里的学习,大家已知道自己该如何定义及初始化一个数组。对象的容纳是本章的重点,而数组只是容纳对象的一种方式。但由于还有其他大量方法可容纳数组,所以是哪些地方使数组显得如此特别呢?

有两方面的问题将数组与其他集合类型区分开来:效率和类型。对于Java 来说,为保存和访问一系列对象(实际是对象的句柄)数组,最有效的方法莫过于数组。数组实际代表一个简单的线性序列,它使得元素的访问速度非常快,但我们却要为这种速度付出代价:创建一个数组对象时,它的大小是固定的,而且不可在那个数组对象的“存在时间”内发生改变。可创建特定大小的一个数组,然后假如用光了存储空间,就再创建一个新数组,将所有句柄从旧数组移到新数组。这属于“矢量”(V ector)类的行为,本章稍后还会详细讨论它。然而,由于为这种大小的灵活性要付出较大的代价,所以我们认为矢量的效率并没有数组高。

C++的矢量类知道自己容纳的是什么类型的对象,但同Java的数组相比,它却有一个明显的缺点:C++矢量类的operator[]不能进行范围检查,所以很容易超出边界(然而,它可以查询vector有多大,而且at()方法确实能进行范围检查)。在Java中,无论使用的是数组还是集合,都会进行范围检查——若超过边界,就会获得一个RuntimeException(运行期违例)错误。正如大家在第9章会学到的那样,这类违例指出的是一个程序员错误,所以不需要在代码中检查它。在另一方面,由于C++的vector不进行范围检查,所以访问速度较快——在Java中,由于对数组和集合都要进行范围检查,所以对性能有一定的影响。

本章还要学习另外几种常见的集合类:V ector(矢量)、Stack(堆栈)以及Hashtable(散列表)。这些类都涉及对对象的处理——好象它们没有特定的类型。

换言之,它们将其当作Object类型处理(Object类型是Java中所有类的“根”类)。从某个角度看,这种处理方法是非常合理的:我们仅需构建一个集合,然后任何Java对象都可以进入那个集合(除基本数据类型外——可用Java的基本类型封装类将其作为常数置入集合,或者将其封装到自己的类内,作为可以变化的值使用)。这再一次反映了数组优于常规集合:创建一个数组时,可令其容纳一种特定的类型。这意味着可进行编译期类型检查,预防自己设置了错误的类型,或者错误指定了准备提取的类型。当然,在编译期或者运行期,Java会防止我们将不当的消息发给一个对象。所以我们不必考虑自己的哪种做法更加危险,只要编译器能及时地指出错误,同时在运行期间加快速度,目的也就达到了。此外,用户很少会对一次违例事件感到非常惊讶的。

考虑到执行效率和类型检查,应尽可能地采用数组。然而,当我们试图解决一个更常规的问题时,数组的局限也可能显得非常明显。在研究过数组以后,本章剩余的部分将把重点放到Java提供的集合类身上。

8.1.1 数组和第一类对象

无论使用的数组属于什么类型,数组标识符实际都是指向真实对象的一个句柄。那些对象本身是在内存“堆”里创建的。堆对象既可“隐式”创建(即默认产生),亦可“显式”创建(即明确指定,用一个new表达式)。堆对象的一部分(实际是我们能访问的唯一字段或方法)是只读的length(长度)成员,它告诉我们那个数组对象里最多能容纳多少元素。对于数组对象,“[]”语法是我们能采用的唯一另类访问方法。

下面这个例子展示了对数组进行初始化的不同方式,以及如何将数组句柄分配给不同的数组对象。它也揭示出对象数组和基本数据类型数组在使用方法上几乎是完全一致的。唯一的差别在于对象数组容纳的是句柄,而基本数据类型数组容纳的是具体的数值(若在执行此程序时遇到困难,请参考第3章的“赋值”小节):

325-327页程序

其中,数组a只是初始化成一个null句柄。此时,编译器会禁止我们对这个句柄作任何实际操作,除非已正确地初始化了它。数组b被初始化成指向由Weeble句柄构成的一个数组,但那个数组里实际并未放置任何Weeble对象。然而,我们仍然可以查询那个数组的大小,因为b指向的是一个合法对象。这也为我们带来了一个难题:不可知道那个数组里实际包含了多少个元素,因为length 只告诉我们可将多少元素置入那个数组。换言之,我们只知道数组对象的大小或容量,不知其实际容纳了多少个元素。尽管如此,由于数组对象在创建之初会自动初始化成null,所以可检查它是否为null,判断一个特定的数组“空位”是否容纳一个对象。类似地,由基本数据类型构成的数组会自动初始化成零(针对数值类型)、null(字符类型)或者false(布尔类型)。

数组c显示出我们首先创建一个数组对象,再将Weeble对象赋给那个数组的所有“空位”。数组d揭示出“集合初始化”语法,从而创建数组对象(用new 命令明确进行,类似于数组c),然后用Weeble对象进行初始化,全部工作在一条语句里完成。

下面这个表达式:

a = d;

向我们展示了如何取得同一个数组对象连接的句柄,然后将其赋给另一个数组对象,就象我们针对对象句柄的其他任何类型做的那样。现在,a和d都指向内存堆内同样的数组对象。

Java 1.1加入了一种新的数组初始化语法,可将其想象成“动态集合初始化”。由d采用的Java 1.0集合初始化方法则必须在定义d的同时进行。但若采用Java 1.1的语法,却可以在任何地方创建和初始化一个数组对象。例如,假设hide()方法用于取得一个Weeble对象数组,那么调用它时传统的方法是:hide(d);

但在Java 1.1中,亦可动态创建想作为参数传递的数组,如下所示:

hide(new Weeble[] {new Weeble(), new Weeble() });

这一新式语法使我们在某些场合下写代码更方便了。

上述例子的第二部分揭示出这样一个问题:对于由基本数据类型构成的数组,它们的运作方式与对象数组极为相似,只是前者直接包容了基本类型的数据值。

1. 基本数据类型集合

集合类只能容纳对象句柄。但对一个数组,却既可令其直接容纳基本类型的数据,亦可容纳指向对象的句柄。利用象Integer、Double之类的“封装器”类,可将基本数据类型的值置入一个集合里。但正如本章后面会在WordCount.java 例子中讲到的那样,用于基本数据类型的封装器类只是在某些场合下才能发挥作用。无论将基本类型的数据置入数组,还是将其封装进入位于集合的一个类内,都涉及到执行效率的问题。显然,若能创建和访问一个基本数据类型数组,那么比起访问一个封装数据的集合,前者的效率会高出许多。

当然,假如准备一种基本数据类型,同时又想要集合的灵活性(在需要的时候可自动扩展,腾出更多的空间),就不宜使用数组,必须使用由封装的数据构成的一个集合。大家或许认为针对每种基本数据类型,都应有一种特殊类型的V ector。但Java并未提供这一特性。某些形式的建模机制或许会在某一天帮助Java 更好地解决这个问题(注释①)。

①:这儿是C++比Java做得好的一个地方,因为C++通过template关键字提供了对“参数化类型”的支持。

8.1.2 数组的返回

假定我们现在想写一个方法,同时不希望它仅仅返回一样东西,而是想返回一系列东西。此时,象C和C++这样的语言会使问题复杂化,因为我们不能返回一个数组,只能返回指向数组的一个指针。这样就非常麻烦,因为很难控制数组的“存在时间”,它很容易造成内存“漏洞”的出现。

Java采用的是类似的方法,但我们能“返回一个数组”。当然,此时返回的实际仍是指向数组的指针。但在Java里,我们永远不必担心那个数组的是否可用——只要需要,它就会自动存在。而且垃圾收集器会在我们完成后自动将其清除。

作为一个例子,请思考如何返回一个字串数组:

329-330页程序

flavorSet()方法创建了一个名为results的String数组。该数组的大小为n——具体数值取决于我们传递给方法的自变量。随后,它从数组flav里随机挑选一些“香料”(Flavor),并将它们置入results里,并最终返回results。返回数组与返回其他任何对象没什么区别——最终返回的都是一个句柄。至于数组到底是在flavorSet()里创建的,还是在其他什么地方创建的,这个问题并不重要,因为反正返回的仅是一个句柄。一旦我们的操作完成,垃圾收集器会自动关照数组的清除工作。而且只要我们需要数组,它就会乖乖地听候调遣。

另一方面,注意当flavorSet()随机挑选香料的时候,它需要保证以前出现过的一次随机选择不会再次出现。为达到这个目的,它使用了一个无限while循环,不断地作出随机选择,直到发现未在picks数组里出现过的一个元素为止(当然,也可以进行字串比较,检查随机选择是否在results数组里出现过,但字串比较的效率比较低)。若成功,就添加这个元素,并中断循环(break),再查找下一个(i值会递增)。但假若t是一个已在picks里出现过的数组,就用标签式的continue往回跳两级,强制选择一个新t。用一个调试程序可以很清楚地看到这个过程。

main()能显示出20个完整的香料集合,所以我们看到flavorSet()每次都用一个随机顺序选择香料。为体会这一点,最简单的方法就是将输出重导向进入一个文件,然后直接观看这个文件的内容。

8.2 集合

现在总结一下我们前面学过的东西:为容纳一组对象,最适宜的选择应当是数组。而且假如容纳的是一系列基本数据类型,更是必须采用数组。在本章剩下的部分,大家将接触到一些更常规的情况。当我们编写程序时,通常并不能确切地知道最终需要多少个对象。有些时候甚至想用更复杂的方式来保存对象。为解决这个问题,Java提供了四种类型的“集合类”:V ector(矢量)、BitSet(位集)、Stack(堆栈)以及Hashtable(散列表)。与拥有集合功能的其他语言相比,尽管这儿的数量显得相当少,但仍然能用它们解决数量惊人的实际问题。

这些集合类具有形形色色的特征。例如,Stack实现了一个LIFO(先入先出)序列,而Hashtable是一种“关联数组”,允许我们将任何对象关联起来。除此以外,所有Java集合类都能自动改变自身的大小。所以,我们在编程时可使用数量众多的对象,同时不必担心会将集合弄得有多大。

8.2.1 缺点:类型未知

使用Java集合的“缺点”是在将对象置入一个集合时丢失了类型信息。之所以会发生这种情况,是由于当初编写集合时,那个集合的程序员根本不知道用户到底想把什么类型置入集合。若指示某个集合只允许特定的类型,会妨碍它成为一个“常规用途”的工具,为用户带来麻烦。为解决这个问题,集合实际容纳的是类型为Object的一些对象的句柄。这种类型当然代表Java中的所有对象,因为它是所有类的根。当然,也要注意这并不包括基本数据类型,因为它们并不是从“任何东西”继承来的。这是一个很好的方案,只是不适用下述场合:

(1) 将一个对象句柄置入集合时,由于类型信息会被抛弃,所以任何类型的对象都可进入我们的集合——即便特别指示它只能容纳特定类型的对象。举个例子来说,虽然指示它只能容纳猫,但事实上任何人都可以把一条狗扔进来。

(2) 由于类型信息不复存在,所以集合能肯定的唯一事情就是自己容纳的是指向一个对象的句柄。正式使用它之前,必须对其进行造型,使其具有正确的类型。

值得欣慰的是,Java不允许人们滥用置入集合的对象。假如将一条狗扔进一个猫的集合,那么仍会将集合内的所有东西都看作猫,所以在使用那条狗时会得到一个“违例”错误。在同样的意义上,假若试图将一条狗的句柄“造型”到一只猫,那么运行期间仍会得到一个“违例”错误。

下面是个例子:

332-333页程序

可以看出,V ector的使用是非常简单的:先创建一个,再用addElement()置入对象,以后用elementAt()取得那些对象(注意V ector有一个size()方法,可使我们知道已添加了多少个元素,以便防止误超边界,造成违例错误)。

Cat和Dog类都非常浅显——除了都是“对象”之外,它们并无特别之处(倘若不明确指出从什么类继承,就默认为从Object继承。所以我们不仅能用V ector 方法将Cat对象置入这个集合,也能添加Dog对象,同时不会在编译期和运行期得到任何出错提示。用V ector方法elementAt()获取原本认为是Cat的对象时,实际获得的是指向一个Object的句柄,必须将那个对象造型为Cat。随后,需要将整个表达式用括号封闭起来,在为Cat调用print()方法之前进行强制造型;否则就会出现一个语法错误。在运行期间,如果试图将Dog对象造型为Cat,就会得到一个违例。

这些处理的意义都非常深远。尽管显得有些麻烦,但却获得了安全上的保证。我们从此再难偶然造成一些隐藏得深的错误。若程序的一个部分(或几个部分)将对象插入一个集合,但我们只是通过一次违例在程序的某个部分发现一个错误的对象置入了集合,就必须找出插入错误的位置。当然,可通过检查代码达到这个目的,但这或许是最笨的调试工具。另一方面,我们可从一些标准化的集合类开始自己的编程。尽管它们在功能上存在一些不足,且显得有些笨拙,但却能保证没有隐藏的错误。

1. 错误有时并不显露出来

在某些情况下,程序似乎正确地工作,不造型回我们原来的类型。第一种情况是相当特殊的:String类从编译器获得了额外的帮助,使其能够正常工作。只要编译器期待的是一个String对象,但它没有得到一个,就会自动调用在Object 里定义、并且能够由任何Java类覆盖的toString()方法。这个方法能生成满足要求的String对象,然后在我们需要的时候使用。

因此,为了让自己类的对象能显示出来,要做的全部事情就是覆盖toString()方法,如下例所示:

334-335页程序

可在Mouse里看到对toString()的重定义代码。在main()的第二个for循环中,可发现下述语句:

System.out.println("Free mouse: " +

mice.elementAt(i));

在“+”后,编译器预期看到的是一个String对象。elementAt()生成了一个Object,所以为获得希望的String,编译器会默认调用toString()。但不幸的是,只有针对String才能得到象这样的结果;其他任何类型都不会进行这样的转换。

隐藏造型的第二种方法已在Mousetrap里得到了应用。caughtY a()方法接收的不是一个Mouse,而是一个Object。随后再将其造型为一个Mouse。当然,这样做是非常冒失的,因为通过接收一个Object,任何东西都可以传递给方法。然而,假若造型不正确——如果我们传递了错误的类型——就会在运行期间得到一个违例错误。这当然没有在编译期进行检查好,但仍然能防止问题的发生。注意在使用这个方法时毋需进行造型:

MouseTrap.caughtY a(mice.elementAt(i));

2. 生成能自动判别类型的V ector

大家或许不想放弃刚才那个问题。一个更“健壮”的方案是用V ector创建一个新类,使其只接收我们指定的类型,也只生成我们希望的类型。如下所示:

335-336页程序

这前一个例子类似,只是新的GopherV ector类有一个类型为V ector的private 成员(从V ector继承有些麻烦,理由稍后便知),而且方法也和Vector类似。然而,它不会接收和产生普通Object,只对Gopher对象感兴趣。

由于GopherV ector只接收一个Gopher(地鼠),所以假如我们使用:

gophers.addElement(new Pigeon());

就会在编译期间获得一条出错消息。采用这种方式,尽管从编码的角度看显得更令人沉闷,但可以立即判断出是否使用了正确的类型。

注意在使用elementAt()时不必进行造型——它肯定是一个Gopher。

3. 参数化类型

这类问题并不是孤立的——我们许多时候都要在其他类型的基础上创建新类型。此时,在编译期间拥有特定的类型信息是非常有帮助的。这便是“参数化类型”的概念。在C++中,它由语言通过“模板”获得了直接支持。至少,Java 保留了关键字generic,期望有一天能够支持参数化类型。但我们现在无法确定这一天何时会来临。

8.3 枚举器(反复器)

在任何集合类中,必须通过某种方法在其中置入对象,再用另一种方法从中取得对象。毕竟,容纳各种各样的对象正是集合的首要任务。在V ector中,addElement()便是我们插入对象采用的方法,而elementAt()是提取对象的唯一方

法。V ector非常灵活,我们可在任何时候选择任何东西,并可使用不同的索引选择多个元素。

若从更高的角度看这个问题,就会发现它的一个缺陷:需要事先知道集合的准确类型,否则无法使用。乍看来,这一点似乎没什么关系。但假若最开始决定使用V ector,后来在程序中又决定(考虑执行效率的原因)改变成一个List(属于Java1.2集合库的一部分),这时又该如何做呢?

可利用“反复器”(Iterator)的概念达到这个目的。它可以是一个对象,作用是遍历一系列对象,并选择那个序列中的每个对象,同时不让客户程序员知道或关注那个序列的基础结构。此外,我们通常认为反复器是一种“轻量级”对象;也就是说,创建它只需付出极少的代价。但也正是由于这个原因,我们常发现反复器存在一些似乎很奇怪的限制。例如,有些反复器只能朝一个方向移动。

Java的Enumeration(枚举,注释②)便是具有这些限制的一个反复器的例子。除下面这些外,不可再用它做其他任何事情:

(1) 用一个名为elements()的方法要求集合为我们提供一个Enumeration。我们首次调用它的nextElement()时,这个Enumeration会返回序列中的第一个元素。

(2) 用nextElement()获得下一个对象。

(3) 用hasMoreElements()检查序列中是否还有更多的对象。

②:“反复器”这个词在C++和OOP的其他地方是经常出现的,所以很难确定为什么Java的开发者采用了这样一个奇怪的名字。Java 1.2的集合库修正了这个问题以及其他许多问题。

只可用Enumeration做这些事情,不能再有更多。它属于反复器一种简单的实现方式,但功能依然十分强大。为体会它的运作过程,让我们复习一下本章早些时候提到的CatsAndDogs.java程序。在原始版本中,elementAt()方法用于选择每一个元素,但在下述修订版中,可看到使用了一个“枚举”:

338-339页程序

我们看到唯一的改变就是最后几行。不再是:

for(int i = 0; i < cats.size(); i++)

((Cat)cats.elementAt(i)).print();

而是用一个Enumeration遍历整个序列:

while(e.hasMoreElements())

((Cat2)e.nextElement()).print();

使用Enumeration,我们不必关心集合中的元素数量。所有工作均由hasMoreElements()和nextElement()自动照管了。

下面再看看另一个例子,让我们创建一个常规用途的打印方法:

339-340页程序

仔细研究一下打印方法:

340页上程序

注意其中没有与序列类型有关的信息。我们拥有的全部东西便是Enumeration。为了解有关序列的情况,一个Enumeration便足够了:可取得下一个对象,亦可知道是否已抵达了末尾。取得一系列对象,然后在其中遍历,从而执行一个特定的操作——这是一个颇有价值的编程概念,本书许多地方都会沿用这一思路。

这个看似特殊的例子甚至可以更为通用,因为它使用了常规的toString()方法(之所以称为常规,是由于它属于Object类的一部分)。下面是调用打印的另一个方法(尽管在效率上可能会差一些):

System.out.println("" + e.nextElement());

它采用了封装到Java内部的“自动转换成字串”技术。一旦编译器碰到一个字串,后面跟随一个“+”,就会希望后面又跟随一个字串,并自动调用toString()。在Java 1.1中,第一个字串是不必要的;所有对象都会转换成字串。亦可对此执行一次造型,获得与调用toString()同样的效果:

System.out.println((String)e.nextElement())

但我们想做的事情通常并不仅仅是调用Object方法,所以会再度面临类型造型的问题。对于自己感兴趣的类型,必须假定自己已获得了一个Enumeration,然后将结果对象造型成为那种类型(若操作错误,会得到运行期违例)。

8.4 集合的类型

标准Java 1.0和1.1库配套提供了非常少的一系列集合类。但对于自己的大多数编程要求,它们基本上都能胜任。正如大家到本章末尾会看到的,Java 1.2提供的是一套重新设计过的大型集合库。

8.4.1 V ector

V ector的用法很简单,这已在前面的例子中得到了证明。尽管我们大多数时候只需用addElement()插入对象,用elementAt()一次提取一个对象,并用elements()获得对序列的一个“枚举”。但仍有其他一系列方法是非常有用的。同我们对于Java库惯常的做法一样,在这里并不使用或讲述所有这些方法。但请务必阅读相应的电子文档,对它们的工作有一个大概的认识。

1. 崩溃Java

Java标准集合里包含了toString()方法,所以它们能生成自己的String表达方式,包括它们容纳的对象。例如在V ector中,toString()会在V ector的各个元素中步进和遍历,并为每个元素调用toString()。假定我们现在想打印出自己类的地址。看起来似乎简单地引用this即可(特别是C++程序员有这样做的倾向):

341页下程序

若只是简单地创建一个CrashJava对象,并将其打印出来,就会得到无穷无

尽的一系列违例错误。然而,假如将CrashJava对象置入一个V ector,并象这里演示的那样打印V ector,就不会出现什么错误提示,甚至连一个违例都不会出现。此时Java只是简单地崩溃(但至少它没有崩溃我的操作系统)。这已在Java 1.1中测试通过。

此时发生的是字串的自动类型转换。当我们使用下述语句时:

"CrashJava address: " + this

编译器就在一个字串后面发现了一个“+”以及好象并非字串的其他东西,所以它会试图将this转换成一个字串。转换时调用的是toString(),后者会产生一个递归调用。若在一个V ector内出现这种事情,看起来堆栈就会溢出,同时违例控制机制根本没有机会作出响应。

若确实想在这种情况下打印出对象的地址,解决方案就是调用Object的toString方法。此时就不必加入this,只需使用super.toString()。当然,采取这种做法也有一个前提:我们必须从Object直接继承,或者没有一个父类覆盖了toString方法。

8.4.2 BitSet

BitSet实际是由“二进制位”构成的一个V ector。如果希望高效率地保存大量“开-关”信息,就应使用BitSet。它只有从尺寸的角度看才有意义;如果希望的高效率的访问,那么它的速度会比使用一些固有类型的数组慢一些。

此外,BitSet的最小长度是一个长整数(Long)的长度:64位。这意味着假如我们准备保存比这更小的数据,如8位数据,那么BitSet就显得浪费了。所以最好创建自己的类,用它容纳自己的标志位。

在一个普通的V ector中,随我们加入越来越多的元素,集合也会自我膨胀。在某种程度上,BitSet也不例外。也就是说,它有时会自行扩展,有时则不然。而且Java的 1.0版本似乎在这方面做得最糟,它的BitSet表现十分差强人意(Java1.1已改正了这个问题)。下面这个例子展示了BitSet是如何运作的,同时演示了1.0版本的错误:

342-344页程序

随机数字生成器用于创建一个随机的byte、short和int。每一个都会转换成BitSet内相应的位模型。此时一切都很正常,因为BitSet是64位的,所以它们都不会造成最终尺寸的增大。但在Java 1.0中,一旦BitSet大于64位,就会出现一些令人迷惑不解的行为。假如我们设置一个只比BitSet当前分配存储空间大出1的一个位,它能够正常地扩展。但一旦试图在更高的位置设置位,同时不先接触边界,就会得到一个恼人的违例。这正是由于BitSet在Java 1.0里不能正确扩展造成的。本例创建了一个512位的BitSet。构建器分配的存储空间是位数的两倍。所以假如设置位1024或更高的位,同时没有先设置位1023,就会在Java 1.0里得到一个违例。但幸运的是,这个问题已在Java 1.1得到了改正。所以如果是为Java 1.0写代码,请尽量避免使用BitSet。

8.4.3 Stack

Stack有时也可以称为“后入先出”(LIFO)集合。换言之,我们在堆栈里最后“压入”的东西将是以后第一个“弹出”的。和其他所有Java集合一样,我

们压入和弹出的都是“对象”,所以必须对自己弹出的东西进行“造型”。

一种很少见的做法是拒绝使用V ector作为一个Stack的基本构成元素,而是从V ector里“继承”一个Stack。这样一来,它就拥有了一个V ector的所有特征及行为,另外加上一些额外的Stack行为。很难判断出设计者到底是明确想这样做,还是属于一种固有的设计。

下面是一个简单的堆栈示例,它能读入数组的每一行,同时将其作为字串压入堆栈。

345页程序

months数组的每一行都通过push()继承进入堆栈,稍后用pop()从堆栈的顶部将其取出。要声明的一点是,V ector操作亦可针对Stack对象进行。这可能是由继承的特质决定的——Stack“属于”一种V ector。因此,能对V ector进行的操作亦可针对Stack进行,例如elementAt()方法。

8.4.4 Hashtable

V ector允许我们用一个数字从一系列对象中作出选择,所以它实际是将数字同对象关联起来了。但假如我们想根据其他标准选择一系列对象呢?堆栈就是这样的一个例子:它的选择标准是“最后压入堆栈的东西”。这种“从一系列对象中选择”的概念亦可叫作一个“映射”、“字典”或者“关联数组”。从概念上讲,它看起来象一个V ector,但却不是通过数字来查找对象,而是用另一个对象来查找它们!这通常都属于一个程序中的重要进程。

在Java中,这个概念具体反映到抽象类Dictionary身上。该类的接口是非常直观的size()告诉我们其中包含了多少元素;isEmpty()判断是否包含了元素(是则为true);put(Object key, Object value)添加一个值(我们希望的东西),并将其同一个键关联起来(想用于搜索它的东西);get(Object key)获得与某个键对应的值;而remove(Object Key)用于从列表中删除“键-值”对。还可以使用枚举技术:keys()产生对键的一个枚举(Enumeration);而elements()产生对所有值的一个枚举。这便是一个Dictionary(字典)的全部。

Dictionary的实现过程并不麻烦。下面列出一种简单的方法,它使用了两个V ector,一个用于容纳键,另一个用来容纳值:

346-347页程序

在对AssocArray的定义中,我们注意到的第一个问题是它“扩展”了字典。这意味着AssocArray属于Dictionary的一种类型,所以可对其发出与Dictionary 一样的请求。如果想生成自己的Dictionary,而且就在这里进行,那么要做的全部事情只是填充位于Dictionary内的所有方法(而且必须覆盖所有方法,因为它们——除构建器外——都是抽象的)。

V ector key和value通过一个标准索引编号链接起来。也就是说,如果用“roof”的一个键以及“blue”的一个值调用put()——假定我们准备将一个房子的各部分与它们的油漆颜色关联起来,而且AssocArray里已有100个元素,那么“roof”就会有101个键元素,而“blue”有101个值元素。而且要注意一下get(),假如我们作为键传递“roof”,它就会产生与keys.index.Of()的索引编号,然后用那个

索引编号生成相关的值矢量内的值。

main()中进行的测试是非常简单的;它只是将小写字符转换成大写字符,这显然可用更有效的方式进行。但它向我们揭示出了AssocArray的强大功能。

标准Java库只包含Dictionary的一个变种,名为Hashtable(散列表,注释③)。Java的散列表具有与AssocArray相同的接口(因为两者都是从Dictionary继承来的)。但有一个方面却反映出了差别:执行效率。若仔细想想必须为一个get()做的事情,就会发现在一个V ector里搜索键的速度要慢得多。但此时用散列表却可以加快不少速度。不必用冗长的线性搜索技术来查找一个键,而是用一个特殊的值,名为“散列码”。散列码可以获取对象中的信息,然后将其转换成那个对象“相对唯一”的整数(int)。所有对象都有一个散列码,而hashCode()是根类Object的一个方法。Hashtable获取对象的hashCode(),然后用它快速查找键。这样可使性能得到大幅度提升(④)。散列表的具体工作原理已超出了本书的范围(⑤)——大家只需要知道散列表是一种快速的“字典”(Dictionary)即可,而字典是一种非常有用的工具。

③:如计划使用RMI(在第15章详述),应注意将远程对象置入散列表时会遇到一个问题(参阅《Core Java》,作者Conrell和Horstmann,Prentice-Hall 1997年出版)

④:如这种速度的提升仍然不能满足你对性能的要求,甚至可以编写自己的散列表例程,从而进一步加快表格的检索过程。这样做可避免在与Object之间进行造型的时间延误,也可以避开由Java类库散列表例程内建的同步过程。

⑤:我的知道的最佳参考读物是《Practical Algorithms for Programmers》,作者为Andrew Binstock和John Rex,Addison-Wesley 1995年出版。

作为应用散列表的一个例子,可考虑用一个程序来检验Java的Math.random()方法的随机性到底如何。在理想情况下,它应该产生一系列完美的随机分布数字。但为了验证这一点,我们需要生成数量众多的随机数字,然后计算落在不同范围内的数字多少。散列表可以极大简化这一工作,因为它能将对象同对象关联起来(此时是将Math.random()生成的值同那些值出现的次数关联起来)。如下所示:

348-349页程序

在main()中,每次产生一个随机数字,它都会封装到一个Integer对象里,使句柄能够随同散列表一起使用(不可对一个集合使用基本数据类型,只能使用对象句柄)。containKey()方法检查这个键是否已经在集合里(也就是说,那个数字以前发现过吗?)若已在集合里,则get()方法获得那个键关联的值,此时是一个Counter(计数器)对象。计数器内的值i随后会增加1,表明这个特定的随机数字又出现了一次。

假如键以前尚未发现过,那么方法put()仍然会在散列表内置入一个新的“键-值”对。在创建之初,Counter会自己的变量i自动初始化为1,它标志着该随机数字的第一次出现。

为显示散列表,只需把它简单地打印出来即可。Hashtable toString()方法能遍历所有键-值对,并为每一对都调用toString()。Integer toString()是事先定义好的,可看到计数器使用的toString。一次运行的结果(添加了一些换行)如下:

350页上程序

大家或许会对Counter类是否必要感到疑惑,它看起来似乎根本没有封装类Integer的功能。为什么不用int或Integer呢?事实上,由于所有集合能容纳的仅有对象句柄,所以根本不可以使用整数。学过集合后,封装类的概念对大家来说就可能更容易理解了,因为不可以将任何基本数据类型置入集合里。然而,我们对Java封装器能做的唯一事情就是将其初始化成一个特定的值,然后读取那个值。也就是说,一旦封装器对象已经创建,就没有办法改变一个值。这使得Integer 封装器对解决我们的问题毫无意义,所以不得不创建一个新类,用它来满足自己的要求。

1. 创建“关键”类

在前面的例子里,我们用一个标准库的类(Integer)作为Hashtable的一个键使用。作为一个键,它能很好地工作,因为它已经具备正确运行的所有条件。但在使用散列表的时候,一旦我们创建自己的类作为键使用,就会遇到一个很常见的问题。例如,假设一套天气预报系统将Groundhog(土拔鼠)对象匹配成Prediction(预报)。这看起来非常直观:我们创建两个类,然后将Groundhog作为键使用,而将Prediction作为值使用。如下所示:

350-351页程序

每个Groundhog都具有一个标识号码,所以赤了在散列表中查找一个Prediction,只需指示它“告诉我与Groundhog号码3相关的Prediction”。Prediction 类包含了一个布尔值,用Math.random()进行初始化,以及一个toString()为我们解释结果。在main()中,用Groundhog以及与它们相关的Prediction填充一个散列表。散列表被打印出来,以便我们看到它们确实已被填充。随后,用标识号码为3的一个Groundhog查找与Groundhog #3对应的预报。

看起来似乎非常简单,但实际是不可行的。问题在于Groundhog是从通用的Object根类继承的(若当初未指定基础类,则所有类最终都是从Object继承的)。事实上是用Object的hashCode()方法生成每个对象的散列码,而且默认情况下只使用它的对象的地址。所以,Groundhog(3)的第一个实例并不会产生与Groundhog(3)第二个实例相等的散列码,而我们用第二个实例进行检索。

大家或许认为此时要做的全部事情就是正确地覆盖hashCode()。但这样做依然行不能,除非再做另一件事情:覆盖也属于Object一部分的equals()。当散列表试图判断我们的键是否等于表内的某个键时,就会用到这个方法。同样地,默认的Object.equals()只是简单地比较对象地址,所以一个Groundhog(3)并不等于另一个Groundhog(3)。

因此,为了在散列表中将自己的类作为键使用,必须同时覆盖hashCode()和equals(),就象下面展示的那样:

352页程序

注意这段代码使用了来自前一个例子的Prediction,所以SpringDetector.java

必须首先编译,否则就会在试图编译SpringDetector2.java时得到一个编译期错误。

Groundhog2.hashCode()将土拔鼠号码作为一个标识符返回(在这个例子中,程序员需要保证没有两个土拔鼠用同样的ID号码并存)。为了返回一个独一无二的标识符,并不需要hashCode(),equals()方法必须能够严格判断两个对象是否相等。

equals()方法要进行两种检查:检查对象是否为null;若不为null,则继续检查是否为Groundhog2的一个实例(要用到instanceof关键字,第11章会详加论述)。即使为了继续执行equals(),它也应该是一个Groundhog2。正如大家看到的那样,这种比较建立在实际ghNumber的基础上。这一次一旦我们运行程序,就会看到它终于产生了正确的输出(许多Java库的类都覆盖了hashcode()和equals()方法,以便与自己提供的内容适应)。

2. 属性:Hashtable的一种类型

在本书的第一个例子中,我们使用了一个名为Properties(属性)的Hashtable 类型。在那个例子中,下述程序行:

Properties p = System.getProperties();

p.list(System.out);

调用了一个名为getProperties()的static方法,用于获得一个特殊的Properties 对象,对系统的某些特征进行描述。list()属于Properties的一个方法,可将内容发给我们选择的任何流式输出。也有一个save()方法,可用它将属性列表写入一个文件,以便日后用load()方法读取。

尽管Properties类是从Hashtable继承的,但它也包含了一个散列表,用于容纳“默认”属性的列表。所以假如没有在主列表里找到一个属性,就会自动搜索默认属性。

Properties类亦可在我们的程序中使用(第17章的ClassScanner.java便是一例)。在Java库的用户文档中,往往可以找到更多、更详细的说明。

8.4.5 再论枚举器

我们现在可以开始演示Enumeration(枚举)的真正威力:将穿越一个序列的操作与那个序列的基础结构分隔开。在下面的例子里,PrintData类用一个Enumeration在一个序列中移动,并为每个对象都调用toString()方法。此时创建了两个不同类型的集合:一个V ector和一个Hashtable。并且在它们里面分别填充Mouse和Hamster对象(本章早些时候已定义了这些类;注意必须先编译HamsterMaze.java和WorksAnyway.java,否则下面的程序不能编译)。由于Enumeration隐藏了基层集合的结构,所以PrintData不知道或者不关心Enumeration来自于什么类型的集合:

354页程序

注意PrintData.print()利用了这些集合中的对象属于Object类这一事实,所以它调用了toString()。但在解决自己的实际问题时,经常都要保证自己的Enumeration穿越某种特定类型的集合。例如,可能要求集合中的所有元素都是一个Shape(几何形状),并含有draw()方法。若出现这种情况,必须从

Enumeration.nextElement()返回的Object进行下溯造型,以便产生一个Shape。

8.5 排序

Java 1.0和1.1库都缺少的一样东西是算术运算,甚至没有最简单的排序运算方法。因此,我们最好创建一个V ector,利用经典的Quicksort(快速排序)方法对其自身进行排序。

编写通用的排序代码时,面临的一个问题是必须根据对象的实际类型来执行比较运算,从而实现正确的排序。当然,一个办法是为每种不同的类型都写一个不同的排序方法。然而,应认识到假若这样做,以后增加新类型时便不易实现代码的重复利用。

程序设计一个主要的目标就是“将发生变化的东西同保持不变的东西分隔开”。在这里,保持不变的代码是通用的排序算法,而每次使用时都要变化的是对象的实际比较方法。因此,我们不可将比较代码“硬编码”到多个不同的排序例程内,而是采用“回调”技术。利用回调,经常发生变化的那部分代码会封装到它自己的类内,而总是保持相同的代码则“回调”发生变化的代码。这样一来,不同的对象就可以表达不同的比较方式,同时向它们传递相同的排序代码。

下面这个“接口”(Interface)展示了如何比较两个对象,它将那些“要发生变化的东西”封装在内:

355页中程序

对这两种方法来说,lhs代表本次比较中的“左手”对象,而rhs代表“右手”对象。

可创建V ector的一个子类,通过Compare实现“快速排序”。对于这种算法,包括它的速度以及原理等等,在此不具体说明。欲知详情,可参考Binstock和Rex编著的《Practical Algorithms for Programmers》,由Addison-Wesley于1995年出版。

355-356页程序

现在,大家可以明白“回调”一词的来历,这是由于quickSort()方法“往回调用”了Compare中的方法。从中亦可理解这种技术如何生成通用的、可重复利用(再生)的代码。

为使用SortV ector,必须创建一个类,令其为我们准备排序的对象实现Compare。此时内部类并不显得特别重要,但对于代码的组织却是有益的。下面是针对String对象的一个例子:

356-357页程序

内部类是“静态”(Static)的,因为它毋需连接一个外部类即可工作。

大家可以看到,一旦设置好框架,就可以非常方便地重复使用象这样的一个设计——只需简单地写一个类,将“需要发生变化”的东西封装进去,然后将一个对象传给SortV ector即可。

比较时将字串强制为小写形式,所以大写A会排列于小写a的旁边,而不会

移动一个完全不同的地方。然而,该例也显示了这种方法的一个不足,因为上述测试代码按照出现顺序排列同一个字母的大写和小写形式:A a b B c C d D。但这通常不是一个大问题,因为经常处理的都是更长的字串,所以上述效果不会显露出来(Java 1.2的集合提供了排序功能,已解决了这个问题)。

继承(extends)在这儿用于创建一种新类型的V ector——也就是说,SortV ector 属于一种V ector,并带有一些附加的功能。继承在这里可发挥很大的作用,但了带来了问题。它使一些方法具有了final属性(已在第7章讲述),所以不能覆盖它们。如果想创建一个排好序的V ector,令其只接收和生成String对象,就会遇到麻烦。因为addElement()和elementAt()都具有final属性,而且它们都是我们必须覆盖的方法,否则便无法实现只能接收和产生String对象。

但在另一方面,请考虑采用“合成”方法:将一个对象置入一个新类的内部。此时,不是改写上述代码来达到这个目的,而是在新类里简单地使用一个SortVector。在这种情况下,用于实现Compare接口的内部类就可以“匿名”地创建。如下所示:

358-359页程序

这样便可快速再生来自SortV ector的代码,从而获得希望的功能。然而,并不是来自SortVector和V ector的所有public方法都能在StrSortV ector中出现。若按这种形式再生代码,可在新类里为包含类内的每一个方法都生成一个定义。当然,也可以在刚开始时只添加少数几个,以后根据需要再添加更多的。新类的设计最终会稳定下来。

这种方法的好处在于它仍然只接纳String对象,也只产生String对象。而且相应的检查是在编译期间进行的,而非在运行期。当然,只有addElement()和elementAt()才具备这一特性;elements()仍然会产生一个Enumeration(枚举),它在编译期的类型是未定的。当然,对Enumeration以及在StrSortV ector中的类型检查会照旧进行;如果真的有什么错误,运行期间会简单地产生一个违例。事实上,我们在编译或运行期间能保证一切都正确无误吗?(也就是说,“代码测试时也许不能保证”,以及“该程序的用户有可能做一些未经我们测试的事情”)。尽管存在其他选择和争论,使用继承都要容易得多,只是在造型时让人深感不便。同样地,一旦为Java加入参数化类型,就有望解决这个问题。

大家在这个类中可以看到有一个名为“sorted”的标志。每次调用addElement()时,都可对V ector进行排序,而且将其连续保持在一个排好序的状态。但在开始读取之前,人们总是向一个V ector添加大量元素。所以与其在每个addElement()后排序,不如一直等到有人想读取V ector,再对其进行排序。后者的效率要高得多。这种除非绝对必要,否则就不采取行动的方法叫作“懒惰求值”(还有一种类似的技术叫作“懒惰初始化”——除非真的需要一个字段值,否则不进行初始化)。

8.6 通用集合库

通过本章的学习,大家已知道标准Java库提供了一些特别有用的集合,但距完整意义的集合尚远。除此之外,象排序这样的算法根本没有提供支持。C++出色的一个地方就是它的库,特别是“标准模板库”(STL)提供了一套相当完整的集合,以及许多象排序和检索这样的算法,可以非常方便地对那些集合进行操

作。有感这一现状,并以这个模型为基础,ObjectSpace公司设计了Java版本的“通用集合库”(从前叫作“Java通用库”,即JGL;但JGL这个缩写形式侵犯了Sun公司的版权——尽管本书仍然沿用这个简称)。这个库尽可能遵照STL的设计(照顾到两种语言间的差异)。JGL实现了许多功能,可满足对一个集合库的大多数常规需求,它与C++的模板机制非常相似。JGL包括相互链接起来的列表、设置、队列、映射、堆栈、序列以及反复器,它们的功能比Enumeration(枚举)强多了。同时提供了一套完整的算法,如检索和排序等。在某些方面,ObjectSpace的设计也显得比Sun的库设计方案“智能”一些。举个例子来说,JGL集合中的方法不会进入final状态,所以很容易继承和改写那些方法。

JGL已包括到一些厂商发行的Java套件中,而且ObjectSpace公司自己也允许所有用户免费使用JGL,包括商业性的使用。详细情况和软件下载可访问https://www.wendangku.net/doc/da12960817.html,。与JGL配套提供的联机文档做得非常好,可作为自己的一个绝佳起点使用。

8.7 新集合

对我来说,集合类属于最强大的一种工具,特别适合在原创编程中使用。大家可能已感觉到我对Java 1.1提供的集合多少有点儿失望。因此,看到Java 1.2对集合重新引起了正确的注意后,确实令人非常愉快。这个版本的集合也得到了完全的重新设计(由Sun公司的Joshua Bloch)。我认为新设计的集合是Java 1.2中两项最主要的特性之一(另一项是Swing库,将在第13章叙述),因为它们极大方便了我们的编程,也使Java变成一种更成熟的编程系统。

有些设计使得元素间的结合变得更紧密,也更容易让人理解。例如,许多名字都变得更短、更明确了,而且更易使用;类型同样如此。有些名字进行了修改,更接近于通俗:我感觉特别好的一个是用“反复器”(Inerator)代替了“枚举”(Enumeration)。

此次重新设计也加强了集合库的功能。现在新增的行为包括链接列表、队列以及撤消组队(即“双终点队列”)。

集合库的设计是相当困难的(会遇到大量库设计问题)。在C++中,STL用多个不同的类来覆盖基础。这种做法比起STL以前是个很大的进步,那时根本没做这方面的考虑。但仍然没有很好地转换到Java里面。结果就是一大堆特别容易混淆的类。在另一个极端,我曾发现一个集合库由单个类构成:colleciton,它同时作为V ector和Hashtable使用。新集合库的设计者则希望达到一种新的平衡:实现人们希望从一个成熟集合库上获得的完整功能,同时又要比STL和其他类似的集合库更易学习和使用。这样得到的结果在某些场合显得有些古怪。但和早期Java库的一些决策不同,这些古怪之处并非偶然出现的,而是以复杂性作为代价,在进行仔细权衡之后得到的结果。这样做也许会延长人们掌握一些库概念的时间,但很快就会发现自己很乐于使用那些新工具,而且变得越来越离不了它。

新的集合库考虑到了“容纳自己对象”的问题,并将其分割成两个明确的概念:

(1) 集合(Collection):一组单独的元素,通常应用了某种规则。在这里,一个List(列表)必须按特定的顺序容纳元素,而一个Set(集)不可包含任何重复的元素。相反,“包”(Bag)的概念未在新的集合库中实现,因为“列表”已提供了类似的功能。

(2) 映射(Map):一系列“键-值”对(这已在散列表身上得到了充分的体现)。从表面看,这似乎应该成为一个“键-值”对的“集合”,但假若试图按那种方式实现它,就会发现实现过程相当笨拙。这进一步证明了应该分离成单独的概念。另一方面,可以方便地查看Map的某个部分。只需创建一个集合,然后用它表示那一部分即可。这样一来,Map就可以返回自己键的一个Set、一个包含自己值的List或者包含自己“键-值”对的一个List。和数组相似,Map可方便扩充到多个“维”,毋需涉及任何新概念。只需简单地在一个Map里包含其他Map(后者又可以包含更多的Map,以此类推)。

Collection和Map可通过多种形式实现,具体由编程要求决定。下面列出的是一个帮助大家理解的新集合示意图:

363页图

这张图刚开始的时候可能让人有点儿摸不着头脑,但在通读了本章以后,相信大家会真正理解它实际只有三个集合组件:Map,List和Set。而且每个组件实际只有两、三种实现方式(注释⑥),而且通常都只有一种特别好的方式。只要看出了这一点,集合就不会再令人生畏。

⑥:写作本章时,Java 1.2尚处于β测试阶段,所以这张示意图没有包括以后会加入的TreeSet。

虚线框代表“接口”,点线框代表“抽象”类,而实线框代表普通(实际)类。点线箭头表示一个特定的类准备实现一个接口(在抽象类的情况下,则是“部分”实现一个接口)。双线箭头表示一个类可生成箭头指向的那个类的对象。例如,任何集合都可以生成一个反复器(Iterator),而一个列表可以生成一个ListIterator(以及原始的反复器,因为列表是从集合继承的)。

致力于容纳对象的接口是Collection,List,Set和Map。在传统情况下,我们需要写大量代码才能同这些接口打交道。而且为了指定自己想使用的准确类型,必须在创建之初进行设置。所以可能创建下面这样的一个List:List x = new LinkedList();

当然,也可以决定将x作为一个LinkedList使用(而不是一个普通的List),并用x负载准确的类型信息。使用接口的好处就是一旦决定改变自己的实施细节,要做的全部事情就是在创建的时候改变它,就象下面这样:List x = new ArrayList();

其余代码可以保持原封不动。

在类的分级结构中,可看到大量以“Abstract”(抽象)开头的类,这刚开始可能会使人感觉迷惑。它们实际上是一些工具,用于“部分”实现一个特定的接口。举个例子来说,假如想生成自己的Set,就不是从Set接口开始,然后自行实现所有方法。相反,我们可以从AbstractSet继承,只需极少的工作即可得到自己的新类。尽管如此,新集合库仍然包含了足够的功能,可满足我们的几乎所有需求。所以考虑到我们的目的,可忽略所有以“Abstract”开头的类。

因此,在观看这张示意图时,真正需要关心的只有位于最顶部的“接口”以及普通(实际)类——均用实线方框包围。通常需要生成实际类的一个对象,将

其上溯造型为对应的接口。以后即可在代码的任何地方使用那个接口。下面是一个简单的例子,它用String对象填充一个集合,然后打印出集合内的每一个元素:

364-365页程序

新集合库的所有代码示例都置于子目录newcollections下,这样便可提醒自己这些工作只对于Java 1.2有效。这样一来,我们必须用下述代码来调用程序:java c08.newcollections.SimpleCollection

采用的语法与其他程序是差不多的。

大家可以看到新集合属于java.util库的一部分,所以在使用时不需要再添加任何额外的import语句。

main()的第一行创建了一个ArrayList对象,然后将其上溯造型成为一个集合。由于这个例子只使用了Collection方法,所以从Collection继承的一个类的任何对象都可以正常工作。但ArrayList是一个典型的Collection,它代替了V ector 的位置。

显然,add()方法的作用是将一个新元素置入集合里。然而,用户文档谨慎地指出add()“保证这个集合包含了指定的元素”。这一点是为Set作铺垫的,后者只有在元素不存在的前提下才会真的加入那个元素。对于ArrayList以及其他任何形式的List,add()肯定意味着“直接加入”。

利用iterator()方法,所有集合都能生成一个“反复器”(Iterator)。反复器其实就象一个“枚举”(Enumeration),是后者的一个替代物,只是:

(1) 它采用了一个历史上默认、而且早在OOP中得到广泛采纳的名字(反复器)。

(2) 采用了比Enumeration更短的名字:hasNext()代替了hasMoreElement(),而next()代替了nextElement()。

(3) 添加了一个名为remove()的新方法,可删除由Iterator生成的上一个元素。所以每次调用next()的时候,只需调用remove()一次。

在SimpleCollection.java中,大家可看到创建了一个反复器,并用它在集合里遍历,打印出每个元素。

8.7.1 使用Collections

下面这张表格总结了用一个集合能做的所有事情(亦可对Set和List做同样的事情,尽管List还提供了一些额外的功能)。Map不是从Collection继承的,所以要单独对待。

366-367页表

boolean add(Object) *保证集合内包含了自变量。如果它没有添加自变量,就返回false(假)

boolean addAll(Collection) *添加自变量内的所有元素。如果没有添加元素,则返回true(真)

void clear() *删除集合内的所有元素

boolean contains(Object) 若集合包含自变量,就返回“真”

boolean containsAll(Collection) 若集合包含了自变量内的所有元素,就返回“真”

boolean isEmpty() 若集合内没有元素,就返回“真”

Iterator iterator() 返回一个反复器,以用它遍历集合的各元素

boolean remove(Object) *如自变量在集合里,就删除那个元素的一个实例。如果已进行了删除,就返回“真”

boolean removeAll(Collection) *删除自变量里的所有元素。如果已进行了任何删除,就返回“真”

boolean retainAll(Collection) *只保留包含在一个自变量里的元素(一个理论的“交集”)。如果已进行了任何改变,就返回“真”

int size() 返回集合内的元素数量

Object[] toArray() 返回包含了集合内所有元素的一个数组

*这是一个“可选的”方法,有的集合可能并未实现它。若确实如此,该方法就会遇到一个UnsupportedOperatiionException,即一个“操作不支持”违例,详见第9章。

下面这个例子向大家演示了所有方法。同样地,它们只对从集合继承的东西有效,一个ArrayList作为一种“不常用的分母”使用:

367-369页程序

通过第一个方法,我们可用测试数据填充任何集合。在当前这种情况下,只是将int转换成String。第二个方法将在本章其余的部分经常采用。

newCollection()的两个版本都创建了ArrayList,用于包含不同的数据集,并将它们作为集合对象返回。所以很明显,除了Collection接口之外,不会再用到其他什么。

print()方法也会在本节经常用到。由于它用一个反复器(Iterator)在一个集合内遍历,而任何集合都可以产生这样的一个反复器,所以它适用于List和Set,也适用于由一个Map生成的Collection。

main()用简单的手段显示出了集合内的所有方法。

在后续的小节里,我们将比较List,Set和Map的不同实现方案,同时指出在各种情况下哪一种方案应成为首选(带有星号的那个)。大家会发现这里并未包括一些传统的类,如V ector,Stack以及Hashtable等。因为不管在什么情况下,新集合内都有自己首选的类。

8.7.2 使用Lists

369-370页表

List(接口)顺序是List最重要的特性;它可保证元素按照规定的顺序排列。List为Collection添加了大量方法,以便我们在List中部插入和删除元素(只推荐对LinkedList这样做)。List也会生成一个ListIterator(列表反复器),利用它可在一个列表里朝两个方向遍历,同时插入和删除位于列表中部的元素(同样地,只建议对LinkedList这样做)

ArrayList*由一个数组后推得到的List。作为一个常规用途的对象容器使

用,用于替换原先的V ector。允许我们快速访问元素,但在从列表中部插入和删除元素时,速度却嫌稍慢。一般只应该用ListIterator对一个ArrayList进行向前和向后遍历,不要用它删除和插入元素;与LinkedList相比,它的效率要低许多LinkedList 提供优化的顺序访问性能,同时可以高效率地在列表中部进行插入和删除操作。但在进行随机访问时,速度却相当慢,此时应换用ArrayList。也提供了addFirst(),addLast(),getFirst(),getLast(),removeFirst()以及removeLast()(未在任何接口或基础类中定义),以便将其作为一个规格、队列以及一个双向队列使用

下面这个例子中的方法每个都覆盖了一组不同的行为:每个列表都能做的事情(basicTest()),通过一个反复器遍历(iterMotion())、用一个反复器改变某些东西(iterManipulation())、体验列表处理的效果(testVisual())以及只有LinkedList 才能做的事情等:

370-373页程序

在basicTest()和iterMotiion()中,只是简单地发出调用,以便揭示出正确的语法。而且尽管捕获了返回值,但是并未使用它。在某些情况下,之所以不捕获返回值,是由于它们没有什么特别的用处。在正式使用它们前,应仔细研究一下自己的联机文档,掌握这些方法完整、正确的用法。

8.7.3 使用Sets

Set拥有与Collection完全相同的接口,所以和两种不同的List不同,它没有什么额外的功能。相反,Set完全就是一个Collection,只是具有不同的行为(这是实例和多形性最理想的应用:用于表达不同的行为)。在这里,一个Set只允许每个对象存在一个实例(正如大家以后会看到的那样,一个对象的“值”的构成是相当复杂的)。

374页表

Set(接口)添加到Set的每个元素都必须是独一无二的;否则Set就不会添加重复的元素。添加到Set里的对象必须定义equals(),从而建立对象的唯一性。Set拥有与Collection完全相同的接口。一个Set不能保证自己可按任何特定的顺序维持自己的元素

HashSet*用于除非常小的以外的所有Set。对象也必须定义hashCode()

ArraySet 由一个数组后推得到的Set。面向非常小的Set设计,特别是那些需要频繁创建和删除的。对于小Set,与HashSet相比,ArraySet创建和反复所需付出的代价都要小得多。但随着Set的增大,它的性能也会大打折扣。不需要HashCode()

TreeSet 由一个“红黑树”后推得到的顺序Set(注释⑦)。这样一来,我们就可以从一个Set里提到一个顺序集合

⑦:直至本书写作的时候,TreeSet仍然只是宣布,尚未正式实现。所以这里没有提供使用TreeSet的例子。

c++面向对象课后答案第8章

1.1在C++中,三种派生方式的说明符号为public、private、protected不加说明,则默认的派生方式为private。 1.2当公有派生时,基类的公有成员成为派生类的公有成员;保护成员成为派生类的保护成员;私有成员成为派生类的不能直接访问成员。当保护派生时,基类的公有成员成为派生类的保护成员;保护成员成为派生类的保护成员;私有成员成为派生类的不能直接访问成员。 1.3 派生类的构造函数一般有3项工作要完成:首先基类初始化,其次成员对象初始化,最后执行派生类构造函数体。 1.4多继承时,多个基类中的同名的成员在派生类中由于标识符不唯一而出现二义性。在派生类中采用虚基类或作用域分辨符来消除该问题。 2.简答题 2.1 派生类如何实现对基类私有成员的访问? 2.2什么是类型兼容规则? 2.3派生类的构造函数是怎样的执行顺序,析构函数的执行顺序是如何实现的? 2.4继承与组合之间的区别与联系是什么? 2.5什么是虚基类?它有什么作用?含有虚基类的派生类的构造函数有什么要求,什么是最远派生类,建立一个含有虚基类的派生类的对象时,为什么由最远派生类的构造函数负责虚基类的初始化? 3.选择题 3.1下面对派生类的描述中,错误的是(D )。 A.一个派生类可以作为另外一个派生类的基类 B.派生类至少有一个基类 C.派生类的成员除了它自己的成员外,还包含了它的基类的成员 D.派生类中继承的基类成员的访问权限到派生类中保持不变 3.2下列对友元关系叙述正确的是(A)。 A.不能继承 B.是类与类的关系 C.是一个类的成员函数与另一个类的关系 D.提高程序的运行效率 3.3当保护继承时,基类的(B)在派生类中成为保护成员,不能通过派生类的对象来直接访问。 A.任何成员B.公有成员和保护成员 C.公有成员和私有成员D.私有成员 3.4设置虚基类的目的是(B)。 A.简化程序B.消除二义性 C.提高运行效率D.减少目标代码 3.5在公有派生情况下,有关派生类对象和基类对象的关系,不正确的叙述是( C )。 A.派生类的对象可以赋给基类的对象 B.派生类的对象可以初始化基类的引用 C.派生类的对象可以直接访问基类中的成员 D.派生类的对象的地址可以赋给指向基类的指针 3.6有如下类定义:

第八章 类与对象

第八章类与对象 【学习目标】 本章主要介绍类类型的定义、构造函数与析构函数的定义与作用、友元函数与友元类的声明与作用、派生类的定义与作用、虚函数和多态性的概念、静态数据成员的声明与定义、模板类的定义与实例化等内容。通过本章的学习,要求同学们: 掌握类的概念,类类型的定义格式,类与结构的关系,类与操作符重载,类的成员属性,类的封装性,类的继承性,构造函数和析构函数的作用,this指针的含义,类对象的定义,友元函数与友元类的作用;了解inline成员函数,静态数据成员,类的多态性与虚函数,类模板等内容。 类:是对具有共同属性和行为的一类事物的抽象描述,共同属性被描述为类中的数据成员,共同行为被描述为成员函数。类所描述的事物具有共同的属性和行为(操作),但每一个具体事物(又称为个体,实例或对象)都具有属于自己的属性值和行为特征。 人的共同属性有姓名,性别,出生日期等等。人的共同行为有爱好,工作,学习等等。 同结构与联合一样,类是一种自定义类型,它包括定义数据成员和定义函数成员(又称成员函数),用数据成员来描述同类事物的属性,用成员函数来描述它们的行为。 类定义变量以及访问数据成员与函数成员与结构类型相同。 一、类的定义 1.类的定义格式: clsaa类名{成员表}; 其中: 类名:为自定义的类的名称,可用来定义变量,函数的数据类型。 成员表:为类包含的数据成员和函数成员。 每一个成员都具有一定的存取权限,或者称存取属性,访问权限,访问属性。C++有三个指明符: public:公用(公有,公共)访问属性,成员可以任意函数所访问。 private:私有访问属性,成员只能为该类的成员函数所访问。 protected:保护访问属性,成员只能为该类成员函数以及该类的派生类中的成员函数访问。 类中的数据成员一般定义为私有,这样只允许该类的成员函数访问,不允许该类以外的任何函数访问,从而使类对象中的数据得到隐藏和保护。 类与结构在定义时的区别: 结构类型定义时在第一个存取指明符前定义的成员具有默认的public访问属性,类在定义时为private访问属性。 2.定义格式举例:

第八章和第九章练习题

一、填空题 1. 税收的“三性”是指税收的 、 和 。 2. 在现代社会,国家的征税收对象主要包括 、 和 三大类,国家的税制也是以对应于这三类课税对象的 、 和 为主体。 3. 累进税率因计算方法的不同,又分为 和 两种。 4. 起征点和免征额的不同点在于,当课税对象大于起征点和免征额时,采用 制度的要对课税对象的全部数额征税,采用 制度的仅对课税对象超过免征额部分征税。 5. 我国税种一般分为所得课税、商品课税、 、 和财产课税五大类。 6. 现代社会,直接税和间接税的划分方法随着税收理论的发展而进一步完善,最终以 为标准。 7. 一般来说,增值税的计税依据是 商商品品和和劳劳务务价价款款中中的的增增值值额额 。 8. 所得课税是对所有以 对象的税种的总称。 9. 我国1994年税制改革时确定的个人所得税法采取 分分项项定定率率 分分项项扣扣除除 分分项项征征收收 、 、 的模式。 答答案案:: 11.. 强强制制性性 无无偿偿性性 固固定定性性 22.. 所所得得 商商品品 财财产产 所所得得税税 商商品品税税 财财产产税税 33.. 全全额额累累进进税税率率 超超额额累累进进税税率率 44.. 起起征征点点 免免征征额额 55.. 资资源源课课税税 行行为为课课税税 66.. 税税负负能能否否转转嫁嫁 77商商品品和和劳劳务务价价款款中中的的增增值值额额 88所所得得额额为为课课税税 99分分项项定定率率 分分项项扣扣除除 分分项项征征收收 二、判断题 1. 税收的强制性、无偿性和固定性是税收区别于其他财政收入的形式特征。 2. 税收的强制性体现了政治权力凌驾于所有权之上,即私有财产并非神圣不可侵犯。 3. 从2003年9月1日起,北京市的个人所得税起征点由1000元提高到现在的1200元。 4. 我国1994年税制改革后的增值税是一项完全的价外税。 5. 税负转嫁的前转方式是指向生产的前一环节转嫁,即通过压低进价等方式转嫁给上游厂家。 6.计税依据中不准许抵扣任何购进固定资产价款,就国民经济整体而言,计税依据相当于国民生产总值的增值税,称为生产型增值税。 7.只准许抵扣当期应计入产品成本的折旧部分,就国民经济整体而言,计税依据相当于国民收入的增值税,称为消费型增值税。 8.消费税实行价外征收的办法。 9.从价定率的办法是指根据商品销售价格和税法规定的税率计算征税。

第八章 类和对象 复习题知识讲解

第八章类和对象 复习题

第八章类和对象复习题 1.系统为每个类提供一个this指针,在类的成员函数内,通过this指针可以 间接访问这个类的( ) 所有成员 C.友元类的public成员 D.所有派生类中的public成员 2.如果在class类的定义中既不指定private,也不指定public,则系统就默认为( ) A. private B. public C. protected D. 不确定 3. 对静态数据成员的描述, 正确的是( ) A. 静态数据成员可以在类体内进行初始化 B. 静态数据成员不可以被类的对象调用 C. 静态数据成员不能受private控制符的作用 D. 静态数据成员可以直接用类名调用 4. 下面叙述错误的是( ) A. 基类的protected成员在派生类中仍然是protected的 B. 基类的protected成员在public派生类中仍然是protected的 C. 基类的protected成员在private派生类中是private的 D. 基类的protected成员不能被派生类的对象访问 5.对于友元函数的描述,正确的是( ) A. 友元函数的实现必须在类的内部定义

B. 友元函数是类的成员函数 C. 友元函数破坏了类的封装性和隐藏性 D. 友元函数不能访问类的私有成员 6.关于内联函数的描述,正确的是( ) A.使用内联函数可以缩短程序代码,减少占用的内存空间 B.使用内联函数可以减少函数调用时入栈和出栈的时间和空间开销,但是会使程序的代码量增加 C.内联函数只能在类的内部进行声明和定义,不能作为全局函数 D.内联函数可以做虚函数 7. 类是对象的( ) A. 具体 B. 抽象 C. 封装 D. 多态 8. struct声明类时,若不作private或public声明,系统默认为( ) A. private B. public C. protected D. 不能确定 9.引入内联函数的主要目的是( ) A.缩短程序代码,少占用内存空间 B.既可以保证程序的可读性,又能提高程序的运行效率 C.占用内存空间少,执行速度快 D.使程序的结构比较清晰 10. 类的具体表现是通过定义来操作的。对象 11.说法错误的是() A.一个类是由一批数据以及对其操作的函数组成

第八章第二节调查研究的类型和方法

第二节调查研究的类型和方法 教学目标: 知识目标:了解调查研究中常见的调查方式与国外流行的几种方式。 能力目标:熟练掌握调查研究中常用的调查方法。 德育目标:培养实事求是、严谨务实工作态度和坚忍不拔的毅力。 教学重点:几种常用的调查方法。 教学难点:几种常见的调查方式。 教学方法:列举法、示范教学法。 教具安排:调查表格。 课时安排:2课时 教学过程: 第一课时 【导入新课】 上一节我们介绍了调查研究对秘书人员的重要性,但是仅有正确的认识和工作热情还 是不够的,还必须掌握一些调查研究的类型和方法,才能提高调查研究的效率。 (板书:第二节调查研究的类型和方法) 【讲授新课】 (板书:一、调查研究的方法) 秘书人员根据调查任务和对象的实际情况,采用科学的调查方式和方法,才能保证调 查研究的客观性和科学性。那么,调查研究中有哪几种类型呢? [明确]一般说来,调查研究的类型大体可分为两类,一类是普遍调查,一类是非普遍调查。普遍调查主要用于对调查对象进行全面调查,,用人多,时间长,费用大,如人口普查等。秘书部门一般不采用这种方法,而多采用在一定范围内,选取部分调查对象进行的非全面调查方法。具体可分为: 1.典型调查法 即在被调查对象总体范围内选择具有代表性的特定对象进行调查。优点是调查范围小, 耗时少,调研较有深度;不足是缺乏量的积累,典型的选择容易受调查者主观意志左右,难免带有一定的主观随意性,因此,结论的普遍性意义还有待进一步的完善。要做好典型

调查,就必须正确选择典型,保证典型具有真实性和客观性;对复杂的事物,必须多层次、多类型地选择典型,使被选择事物具有更大的代表性。 2.重点调查法 即从调查对象中就某一专门项目,选取起主要或决定作用的对象进行调查。优点是调 查的对象集中,人力、物力花费不大,材料却能反映对全局有决定性影响的情况,因而秘书部门的调研常用这种方法。 重点调查与典型调查都属小范围的调查,但两者又有所区别: (1)选择调查对象的标准不同。典型调查的对象是具有代表性的特定对象;重点调查 的对象则是同类中具有集中性的对象。 (2)调查的主要目的不同。典型调查主要是为定性决策服务的,是要找出事物本质及 其发展规律;重点调查则是为定量分析服务的,即对对象的总体数量状况做基本的估计。 3.抽样调查法 即从调查对象的总体范围内,随机抽取一部分样本,通过对样本的调查和统计,推断 对象总体的性质与状态。抽样调查是一种普遍调查与典型调查相结合的形式,随机抽样时,总体中的每一单个对象都有同等可能被抽中的机会。因此,它的优点是:调查的结果比较公正客观;通过数学方法计算,便于对调查总体作定量分析。不足之处是不宜于作定性性调查;调查对象的范围不太明确时,抽样调查的意义也就不大了。 4.综合调查法 即对调查对象运用多学科知识进行的多角度的调查。其目的是在于掌握事物全局和各 个部分的联系。宏观的调查,如领导机关要制定发展规划,常采用这种方法。 二、调查研究中常见的调查方式 1.个别调查 即通过对被调查者个别交谈来了解情况。这种调查方式简便易行,并可能发现一些用 其他公开的方式难以获取的材料。采用这种方式要注意: (1)选好调查对象。通过事前了解,选择能提供所需材料的知情人。 (2)注意方式方法。要做好谈话对象的思想工作,使其消除各种思想顾虑,提供真实 材料。要尊重对方,防止审讯式的讯问。 个别调查常用于提供线索、探讨问题,所得的材料通常只作参考,数据性资料必须核 实后才可使用。 2.开调查会 即召集多个访谈对象、深入地探讨要调查的问题。采用这种方式,访谈对象相对集中, 访谈的内容因相互启发而得到补充和深入。访谈者需要注意的是: (1)开调查会前,要确定好参加的人数,参加者应是真正了解情况并敢于讲真话的人。(2)要事先通知,让与会者明确调查的内容和目的,以便有所准备。 (3)调查者要做好各项准备工作,熟练地掌握和运用各种访问技巧,并有效地控制整 个访谈过程。 第二课时 (继续讲授新课)

第8章 类与对象

8.1 知识要点 1.1.掌握类与对象的概念和定义方法,掌握类成员的访问属性。 2.2.掌握构造函数的概念、定义方法和使用方法。 3.3.掌握析构函数的概念、定义方法和使用方法。 4.4.掌握拷贝构造函数的概念、定义方法和使用方法。 5.5.掌握包含对象成员的类构造函数的定义方法。 6.6.掌握静态成员的概念、定义方法、作用和特点。 7.7.掌握友元的概念、定义方法、作用和特点。 8.8.掌握类模板的定义格式与使用方法。 8.2典型例题分析与解答 例题1:下列有关类的说法不正确的是()。 A.A.对象是类的一个实例 B.B.任何一个对象只能属于一个具体的类 C.C.一个类只能有一个对象 D.D.类与对象的关系和数据类型与变量的关系相似 答案:C 分析:对象是类的一个实例,类与对象的关系和数据与变量的关系相似,所以一个类可以有多个对象。 例题2:下面()项是对构造函数和析构函数的正确定义。 A.void X::X(), void X::~X() B.X::X(参数), X::~X() C.X::X(参数), X::~X(参数) D.void X::X(参数), void X::~X(参数) 答案:B 分析构造函数无返回类型、可带参数、可重载;析构函数无返回类型、不可带参数、不可重载。 例题3:()的功能是对象进行初始化。 A.析构函数 B. 数据成员 C.构造函数 D.静态成员函数 答案:C 分析:当一个对象定义时,C++编译系统自动调用构造函数建立该对象并进行初始化;当一个对象的生命周期结束时,C++编译系统自动调用析构函数注销该对象并进行善后工作; 例题4:下列表达方式正确的是()。 A.class P{ B. class P{ public: public: int x=15; int x; void show(){cout<

第八章类和对象复习题.doc

第八章类和对象复习题 1 ?系统为每个类提供一个this 指针,在类的成员函数内,通过this 指针可以间 接 访问这个类的( ) C ?友元类的public 成员 D.所有派生类中的public 成员 2. 如果在class 类的定义中既不指定private,也不指定public,则系统就默认 为( ) A. private B. public C. protected D.不确定 3. 对静态数据成员的描述,正确的是( ) A. 静态数据成员可以在类体内进行初始化 B. 静态数据成员不可以被类的对象调用 C. 静态数据成员不能受private 控制符的作用 D. 静态数据成员可以直接用类名调用 4. 下面叙述错误的是( ) A. 基类的protected 成员在派生类中仍然是protected 的 B. 基类的protected 成员在public 派生类中仍然是protected 的 C. 基类的protected 成员在private 派生类中是private 的 D. 基类的protected 成员不能被派生类的对象访问 5?对于友元函数的描述,正确的是( ) A. 友元函数的实现必须在类的内部定义 B. 友元函数是类的成员函数 C. 友元函数破坏了类的封装性和隐藏性 D. 友元函数不能访问类的私有成员 6?关于内联函数的描述,正确的是( ) A ?使用内联函数可以缩短程序代码,减少占用的内存空间 B ?使用内联函数可以减少函数调用时入栈和出栈的时间和空间开销,但是会使 程序的代码量增加 C ?内联函数只能在类的内部进行声明和定义,不能作为全局函数 D ?内联函数可以做虚函数 7.类是对象的( ) B.所有成员

第八章 类和对象 复习题

第八章类和对象复习题 1.系统为每个类提供一个this指针,在类的成员函数内,通过this指针可以间 接访问这个类的( ) A.在public段中声明的友元 B.所有成员 C.友元类的public成员 D.所有派生类中的public成员2.如果在class类的定义中既不指定private,也不指定public,则系统就默认为( ) A. private B. public C. protected D. 不确定 3. 对静态数据成员的描述, 正确的是( ) A. 静态数据成员可以在类体内进行初始化 B. 静态数据成员不可以被类的对象调用 C. 静态数据成员不能受private控制符的作用 D. 静态数据成员可以直接用类名调用 4. 下面叙述错误的是( ) A. 基类的protected成员在派生类中仍然是protected的 B. 基类的protected成员在public派生类中仍然是protected的 C. 基类的protected成员在private派生类中是private的 D. 基类的protected成员不能被派生类的对象访问 5.对于友元函数的描述,正确的是( ) A. 友元函数的实现必须在类的内部定义 B. 友元函数是类的成员函数 C. 友元函数破坏了类的封装性和隐藏性 D. 友元函数不能访问类的私有成员 6.关于内联函数的描述,正确的是( ) A.使用内联函数可以缩短程序代码,减少占用的内存空间 B.使用内联函数可以减少函数调用时入栈和出栈的时间和空间开销,但是会使程序的代码量增加 C.内联函数只能在类的内部进行声明和定义,不能作为全局函数 D.内联函数可以做虚函数 7. 类是对象的( ) A. 具体 B. 抽象

基于对象的程序设计——第八章基于类和对象的特性

基于对象的程序设计——第八章基于类和对象的特性 2015年4月28日星期二 13:45 8.1 面向对象程序设计方法概述 面向对象的程序设计方法的一个重要特点就是“封装性”(Encapsulation),所谓“封装”,指两方面的含义:一是将有关的数据和操作代码封装在一个对象中,形成一个基本单位,各个对象之间相互独立,互不干扰。而是将对象中某些部分对外隐藏,即隐藏其内部细节,只留下少量节后,以便与外界进行联系,接受外界的消息。这种对外隐藏的方法称为信息隐蔽(information hiding).信息隐蔽还有利于数据安全,防止无关的人了解和修改数据。 8.2类的声明和对象的定义 类是抽象的,不占用内存,而对象时具体的,占用存储空间。 如果在类的定义中既不指定private,也不指定public ,则系统默认为是私有的。 8.3类的成员函数 8.3.2 在类外定义成员函数 “::”是作用域限定符(field qualifier)或称为作用域运算符。例如:void Student :: display(){} 如果在作用域运算符::的前面没有类名,或者作用域前面既无类名又无作用域运算符::,如 :: display() 或display(),则表示display不属于任何类,这个函数不是成员函数,而是全局函数。 类函数必须先在类体中做原型声明,然后再类外定义,也就是说类体的位置应该在函数之前,否则编译时会报错。 8.3.3内置成员函数 C++要求对一般的内置函数用关键字inline声明,但对类内定义的成员函数可以省略inline,因为这些成员函数已被隐含地指定为内置函数。 如果成员不在类体内定义,而在类体外定义,系统并不把它默认为内置函数,调用这些成员函数的过程和调用一般函数的过程是相同的。如果想把这些成员函数指定为内置函数,应当用inline作显式声明。 类内声明时或函数类外实现时作inline 声明均可(二者有其一即可)。但是如果类体外定义inline函数,则必须将类定义和成员函数的定义放在同一个头文件中,否则无法进行置换。 8.3.4 成员函数的存储方式 同一个类的不同对象的数据成员的值一般是不相同的,而不同对象的函数代码是相同的,不论调用哪一个对象的函数的代码,其实调用的都是同样的内容的代码。因此,每个对象所占用的存储空间的只是该对象那个数据成员所占用的存储空间,而不包括函数代码所占用的存储空间。 需要说明: (1)不论成员函数在类内定义还是在类外定义,成员函数的代码段的存储方式是相对的,都不占用对象的存储空间。 (2)不论是否用inline声明,成员函数的代码段都不占用对象的存储空间。Inline 只影响程序的执行效率,而与成员函数是否占用对象的存储空间无关。 (3)银行保险箱的例子,虽然成员函数并没有放在对象的存储空间中,但从逻辑的角度,成员函数和数据一起封装在一个对象中的,只允许本对象成员的函数访问同一对象中的数据、 8.4 对象成员的引用 8.4.1 通过对象名和成员运算符访问对象中的成员 ①对象名.成员名

第八章 面向对象程序设计

第八章面向对象程序设计 一、选择题 1、面向对象程序设计采用了以为中心的软件构造方法。 A)数据结构B)数据流C)功能分析D)算法 2、下面关于属性、方法和事件的叙述中,错误的是______。 A)属性用于描述对象的状态,方法用于表示对象的行为 B)基于同一个类产生的两个对象可以分别设置自己的属性值 C)事件代码也可以像方法一样被显示调用 D)在新建一个表单时,可以添加新的属性、方法和事件 3、在下面关于面向对象数据库的叙述中,错误的是______。 A)每个对象在系统中都有唯一的对象标识 B)事件作用于对象,对象识别事件并作出相应反应 C)一个子类能够继承其所有父类的属性和方法 D)一个父类包括其所有子类的属性和方法 4、每个对象都可以对一个被称为事件的动作进行识别和响应。下面对于事件的 描述中,_____是错误的。 A)事件是一种预先定义好的特定的动作,由用户或系统激活 B)VFP基类的事件集合是由系统预先定义好的,是唯一的 C)VFP基类的事件也可以由用户创建 D)可以激活事件的用户动作有按键、单击鼠标、移动鼠标等 5、_____是面向对象程序设计中程序运行的最基本实体。 A)对象B)类C)方法D)函数

6、创建类时首先要定义类的。 A)名称B)属性C)事件D)方法 7、有关窗口的操作,是正确的。 A) deactivate window命令挂起的窗口将从内存中清除 B) show window命令显示的窗口不一定是活动窗口,但活动窗口一定是显示 窗口 C) hide window命令将活动窗口在屏幕上隐藏起来,输出仍然面向该窗口 D)用release window删除的窗口还可以再重新显示或激活 8、有关窗口的叙述,是正确的。 A) ACTIV A TE WINDOW 命令激活窗口后,@…SA Y的 坐标是相对屏幕的物理坐标 B)当多次使用激活窗口命令时,则最后一个被激活的窗口是当前窗口 C)当前窗口是无法改变的 D)窗口休眠后第一个激活的窗口变为当前窗口 9、以下属于窗口类控件的是。 A)Text B) Form C) Lable D) CommandButton 10、以下属于非容器类控件的是。 A) Form B) Lable C) Page D) Container 11、以下属于容器类控件的是______。 A) Text B) Form C) Label D) CommandButton 12、面向对象的程序设计是近年来程序设计方法的主流方式,简称OOP。下面 这些对于OOP的描述错误的是。

相关文档