Java编程思想对象的容纳实例详解
java提供了容纳对象(或者对象的句柄)的多种方式,接下来我们具体看看都有哪些方式。
有两方面的问题将数组与其他集合类型区分开来:效率和类型。对于java来说,为保存和访问一系列对象(实际是对象的句柄)数组,最有效的方法莫过于数组。数组实际代表一个简单的线性序列,它使得元素的访问速度非常快,但我们却要为这种速度付出代价:创建一个数组对象时,它的大小是固定的,而且不可在那个数组对象的“存在时间”内发生改变。可创建特定大小的一个数组,然后假如用光了存储空间,就再创建一个新数组,将所有句柄从旧数组移到新数组。这属于“矢量”(vector)类的行为。然而,由于为这种大小的灵活性要付出较大的代价,所以我们认为矢量的效率并没有数组高。
在java中,无论使用的是数组还是集合,都会进行范围检查——若超过边界,就会获得一个runtimeexception(运行期违例)错误。几种常见的集合类:vector(矢量)、stack(堆栈)以及hashtable(散列表)。这些类都涉及对对象的处理——好象它们没有特定的类型。换言之,它们将其当作object类型处理(object类型是java中所有类的“根”类)。从某个角度看,这种处理方法是非常合理的:我们仅需构建一个集合,然后任何java对象都可以进入那个集合(除基本数据类型外——可用java的基本类型封装类将其作为常数置入集合,或者将其封装到自己的类内,作为可以变化的值使用)。这再一次反映了数组优于常规集合:创建一个数组时,可令其容纳一种特定的类型。
对象数组容纳的是句柄,而基本数据类型数组容纳的是具体的数值。
1、集合
为容纳一组对象,最适宜的选择应当是数组。而且假如容纳的是一系列基本数据类型,更是必须采用数组。当我们编写程序时,通常并不能确切地知道最终需要多少个对象。有些时候甚至想用更复杂的方式来保存对象。为解决这个问题,java提供了四种类型的“集合类”:vector(矢量)、bitset(位集)、stack(堆栈)以及hashtable(散列表)。与拥有集合功能的其他语言相比,尽管这儿的数量显得相当少,但仍然能用它们解决数量惊人的实际问题。
这些集合类具有形形色色的特征。例如,stack实现了一个lifo(先入先出)序列,而hashtable是一种“关联数组”,允许我们将任何对象关联起来。除此以外,所有java集合类都能自动改变自身的大小。所以,我们在编程时可使用数量众多的对象,同时不必担心会将集合弄得有多大。
1.1 缺点:类型未知
使用java集合的“缺点”是在将对象置入一个集合时丢失了类型信息。之所以会发生这种情况,是由于当初编写集合时,那个集合的程序员根本不知道用户到底想把什么类型置入集合。若指示某个集合只允许特定的类型,会妨碍它成为一个“常规用途”的工具,为用户带来麻烦。为解决这个问题,集合实际容纳的是类型为object的一些对象的句柄。这种类型当然代表java中的所有对象,因为它是所有类的根。当然,也要注意这并不包括基本数据类型,因为它们并不是从“任何东西”继承来的。这是一个很好的方案,只是不适用下述场合:
(1) 将一个对象句柄置入集合时,由于类型信息会被抛弃,所以任何类型的对象都可进入我们的集合——即便特别指示它只能容纳特定类型的对象。举个例子来说,虽然指示它只能容纳猫,但事实上任何人都可以把一条狗扔进来。
(2) 由于类型信息不复存在,所以集合能肯定的唯一事情就是自己容纳的是指向一个对象的句柄。正式使用它之前,必须对其进行造型,使其具有正确的类型。
2、stack
stack有时也可以称为“后入先出”(lifo)集合。换言之,我们在堆栈里最后“压入”的东西将是以后第一个“弹出”的。和其他所有java集合一样,我们压入和弹出的都是“对象”,所以必须对自己弹出的东西进行“造型”。
一种很少见的做法是拒绝使用vector作为一个stack的基本构成元素,而是从vector里“继承”一个stack。这样一来,它就拥有了一个vector的所有特征及行为,另外加上一些额外的stack行为。很难判断出设计者到底是明确想这样做,还是属于一种固有的设计。
下面是一个简单的堆栈示例,它能读入数组的每一行,同时将其作为字串压入堆栈。
import java.util.*; public class stacks { static string[] months = { "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december" }; public static void main(string[] args) { stack stk = new stack(); for(int i = 0; i < months.length; i++) stk.push(months[i] + " "); system.out.println("stk = " + stk); // treating a stack as a vector: stk.addelement("the last line"); system.out.println( "element 5 = " + stk.elementat(5)); system.out.println("popping elements:"); while(!stk.empty()) system.out.println(stk.pop()); } }
months数组的每一行都通过push()继承进入堆栈,稍后用pop()从堆栈的顶部将其取出。要声明的一点是,vector操作亦可针对stack对象进行。这可能是由继承的特质决定的——stack“属于”一种vector。因此,能对vector进行的操作亦可针对stack进行,例如elementat()方法。
3、hashtable
vector允许我们用一个数字从一系列对象中作出选择,所以它实际是将数字同对象关联起来了。但假如我们想根据其他标准选择一系列对象呢?堆栈就是这样的一个例子:它的选择标准是“最后压入堆栈的东西”。这种“从一系列对象中选择”的概念亦可叫作一个“映射”、“字典”或者“关联数组”。从概念上讲,它看起来象一个vector,但却不是通过数字来查找对象,而是用另一个对象来查找它们!这通常都属于一个程序中的重要进程。
在java中,这个概念具体反映到抽象类dictionary身上。该类的接口是非常直观的size()告诉我们其中包含了多少元素;isempty()判断是否包含了元素(是则为true);put(object key, object value)添加一个值(我们希望的东西),并将其同一个键关联起来(想用于搜索它的东西);get(object key)获得与某个键对应的值;而remove(object key)用于从列表中删除“键-值”对。还可以使用枚举技术:keys()产生对键的一个枚举(enumeration);而elements()产生对所有值的一个枚举。这便是一个dictionary(字典)的全部。
dictionary的实现过程并不麻烦。下面列出一种简单的方法,它使用了两个vector,一个用于容纳键,另一个用来容纳值:
import java.util.*; public class assocarray extends dictionary { private vector keys = new vector(); private vector values = new vector(); public int size() { return keys.size(); } public boolean isempty() { return keys.isempty(); } public object put(object key, object value) { keys.addelement(key); values.addelement(value); return key; } public object get(object key) { int index = keys.indexof(key); // indexof() returns -1 if key not found: if(index == -1) return null; return values.elementat(index); } public object remove(object key) { int index = keys.indexof(key); if(index == -1) return null; keys.removeelementat(index); object returnval = values.elementat(index); values.removeelementat(index); return returnval; } public enumeration keys() { return keys.elements(); } public enumeration elements() { return values.elements(); } // test it: public static void main(string[] args) { assocarray aa = new assocarray(); for(char c = 'a'; c <= 'z'; c++) aa.put(string.valueof(c), string.valueof(c) .touppercase()); char[] ca = { 'a', 'e', 'i', 'o', 'u' }; for(int i = 0; i < ca.length; i++) system.out.println("uppercase: " + aa.get(string.valueof(ca[i]))); } }
在对assocarray的定义中,我们注意到的第一个问题是它“扩展”了字典。这意味着assocarray属于dictionary的一种类型,所以可对其发出与dictionary一样的请求。如果想生成自己的dictionary,而且就在这里进行,那么要做的全部事情只是填充位于dictionary内的所有方法(而且必须覆盖所有方法,因为它们——除构建器外——都是抽象的)。
vector 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()做的事情,就会发现在一个vector里搜索键的速度要慢得多。但此时用散列表却可以加快不少速度。不必用冗长的线性搜索技术来查找一个键,而是用一个特殊的值,名为“散列码”。散列码可以获取对象中的信息,然后将其转换成那个对象“相对唯一”的整数(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()生成的值同那些值出现的次数关联起来)。如下所示:
//: statistics.java // simple demonstration of hashtable import java.util.*; class counter { int i = 1; public string tostring() { return integer.tostring(i); } } class statistics { public static void main(string[] args) { hashtable ht = new hashtable(); for(int i = 0; i < 10000; i++) { // produce a number between 0 and 20: integer r = new integer((int)(math.random() * 20)); if(ht.containskey(r)) ((counter)ht.get(r)).i++; else ht.put(r, new counter()); } system.out.println(ht); } }
在main()中,每次产生一个随机数字,它都会封装到一个integer对象里,使句柄能够随同散列表一起使用(不可对一个集合使用基本数据类型,只能使用对象句柄)。containkey()方法检查这个键是否已经在集合里(也就是说,那个数字以前发现过吗?)若已在集合里,则get()方法获得那个键关联的值,此时是一个counter(计数器)对象。计数器内的值i随后会增加1,表明这个特定的随机数字又出现了一次。
假如键以前尚未发现过,那么方法put()仍然会在散列表内置入一个新的“键-值”对。在创建之初,counter会自己的变量i自动初始化为1,它标志着该随机数字的第一次出现。
为显示散列表,只需把它简单地打印出来即可。hashtable tostring()方法能遍历所有键-值对,并为每一对都调用tostring()。integer tostring()是事先定义好的,可看到计数器使用的tostring。一次运行的结果(添加了一些换行)如下:
{19=526, 18=533, 17=460, 16=513, 15=521, 14=495, 13=512, 12=483, 11=488, 10=487, 9=514, 8=523, 7=497, 6=487, 5=480, 4=489, 3=509, 2=503, 1=475, 0=505}
大家或许会对counter类是否必要感到疑惑,它看起来似乎根本没有封装类integer的功能。为什么不用int或integer呢?事实上,由于所有集合能容纳的仅有对象句柄,所以根本不可以使用整数。学过集合后,封装类的概念对大家来说就可能更容易理解了,因为不可以将任何基本数据类型置入集合里。然而,我们对java封装器能做的唯一事情就是将其初始化成一个特定的值,然后读取那个值。也就是说,一旦封装器对象已经创建,就没有办法改变一个值。这使得integer封装器对解决我们的问题毫无意义,所以不得不创建一个新类,用它来满足自己的要求。
3.1 创建“关键”类
在前面的例子里,我们用一个标准库的类(integer)作为hashtable的一个键使用。作为一个键,它能很好地工作,因为它已经具备正确运行的所有条件。但在使用散列表的时候,一旦我们创建自己的类作为键使用,就会遇到一个很常见的问题。例如,假设一套天气预报系统将groundhog(土拔鼠)对象匹配成prediction(预报)。这看起来非常直观:我们创建两个类,然后将groundhog作为键使用,而将prediction作为值使用。如下所示:
//: springdetector.java // looks plausible, but doesn't work right. import java.util.*; class groundhog { int ghnumber; groundhog(int n) { ghnumber = n; } } class prediction { boolean shadow = math.random() > 0.5; public string tostring() { if(shadow) return "six more weeks of winter!"; else return "early spring!"; } } public class springdetector { public static void main(string[] args) { hashtable ht = new hashtable(); for(int i = 0; i < 10; i++) ht.put(new groundhog(i), new prediction()); system.out.println("ht = " + ht + "\n"); system.out.println( "looking up prediction for groundhog #3:"); groundhog gh = new groundhog(3); if(ht.containskey(gh)) system.out.println((prediction)ht.get(gh)); } }
每个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(),就象下面展示的那样:
//: springdetector2.java // if you create a class that's used as a key in // a hashtable, you must override hashcode() // and equals(). import java.util.*; class groundhog2 { int ghnumber; groundhog2(int n) { ghnumber = n; } public int hashcode() { return ghnumber; } public boolean equals(object o) { return (o instanceof groundhog2) && (ghnumber == ((groundhog2)o).ghnumber); } } public class springdetector2 { public static void main(string[] args) { hashtable ht = new hashtable(); for(int i = 0; i < 10; i++) ht.put(new groundhog2(i),new prediction()); system.out.println("ht = " + ht + "\n"); system.out.println( "looking up prediction for groundhog #3:"); groundhog2 gh = new groundhog2(3); if(ht.containskey(gh)) system.out.println((prediction)ht.get(gh)); } }
注意这段代码使用了来自前一个例子的prediction,所以springdetector.java必须首先编译,否则就会在试图编译springdetector2.java时得到一个编译期错误。
groundhog2.hashcode()将土拔鼠号码作为一个标识符返回(在这个例子中,程序员需要保证没有两个土拔鼠用同样的id号码并存)。为了返回一个独一无二的标识符,并不需要hashcode(),equals()方法必须能够严格判断两个对象是否相等。
equals()方法要进行两种检查:检查对象是否为null;若不为null,则继续检查是否为groundhog2的一个实例(要用到instanceof关键字)。即使为了继续执行equals(),它也应该是一个groundhog2。正如大家看到的那样,这种比较建立在实际ghnumber的基础上。这一次一旦我们运行程序,就会看到它终于产生了正确的输出(许多java库的类都覆盖了hashcode()和equals()方法,以便与自己提供的内容适应)。
3.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库的用户文档中,往往可以找到更多、更详细的说明。
4、 再论枚举器
我们现在可以开始演示enumeration(枚举)的真正威力:将穿越一个序列的操作与那个序列的基础结构分隔开。在下面的例子里,printdata类用一个enumeration在一个序列中移动,并为每个对象都调用tostring()方法。此时创建了两个不同类型的集合:一个vector和一个hashtable。并且在它们里面分别填充mouse和hamster对象(本章早些时候已定义了这些类;注意必须先编译hamstermaze.java和worksanyway.java,否则下面的程序不能编译)。由于enumeration隐藏了基层集合的结构,所以printdata不知道或者不关心enumeration来自于什么类型的集合:
//: enumerators2.java // revisiting enumerations import java.util.*; class printdata { static void print(enumeration e) { while(e.hasmoreelements()) system.out.println( e.nextelement().tostring()); } } class enumerators2 { public static void main(string[] args) { vector v = new vector(); for(int i = 0; i < 5; i++) v.addelement(new mouse(i)); hashtable h = new hashtable(); for(int i = 0; i < 5; i++) h.put(new integer(i), new hamster(i)); system.out.println("vector"); printdata.print(v.elements()); system.out.println("hashtable"); printdata.print(h.elements()); } }
注意printdata.print()利用了这些集合中的对象属于object类这一事实,所以它调用了tostring()。但在解决自己的实际问题时,经常都要保证自己的enumeration穿越某种特定类型的集合。例如,可能要求集合中的所有元素都是一个shape(几何形状),并含有draw()方法。若出现这种情况,必须从enumeration.nextelement()返回的object进行下溯造型,以便产生一个shape。
5、新集合
新的集合库考虑到了“容纳自己对象”的问题,并将其分割成两个明确的概念:
(1) 集合(collection):一组单独的元素,通常应用了某种规则。在这里,一个list(列表)必须按特定的顺序容纳元素,而一个set(集)不可包含任何重复的元素。相反,“包”(bag)的概念未在新的集合库中实现,因为“列表”已提供了类似的功能。
(2) 映射(map):一系列“键-值”对(这已在散列表身上得到了充分的体现)。从表面看,这似乎应该成为一个“键-值”对的“集合”,但假若试图按那种方式实现它,就会发现实现过程相当笨拙。这进一步证明了应该分离成单独的概念。另一方面,可以方便地查看map的某个部分。只需创建一个集合,然后用它表示那一部分即可。这样一来,map就可以返回自己键的一个set、一个包含自己值的list或者包含自己“键-值”对的一个list。和数组相似,map可方便扩充到多个“维”,毋需涉及任何新概念。只需简单地在一个map里包含其他map(后者又可以包含更多的map,以此类推)。
在通读了本章以后,相信大家会真正理解它实际只有三个集合组件:map,list和set。而且每个组件实际只有两、三种实现方式(注释⑥),而且通常都只有一种特别好的方式。只要看出了这一点,集合就不会再令人生畏。
6、使用collections
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() 返回包含了集合内所有元素的一个数组
7、使用lists
list(接口) 顺序是list最重要的特性;它可保证元素按照规定的顺序排列。list为collection添加了大量方法,以便我们在list中部插入和删除元素(只推荐对linkedlist这样做)。list也会生成一个listiterator(列表反复器),利用它可在一个列表里朝两个方向遍历,同时插入和删除位于列表中部的元素(同样地,只建议对linkedlist这样做)
arraylist* 由一个数组后推得到的list。作为一个常规用途的对象容器使用,用于替换原先的vector。允许我们快速访问元素,但在从列表中部插入和删除元素时,速度却嫌稍慢。一般只应该用listiterator对一个arraylist进行向前和向后遍历,不要用它删除和插入元素;与linkedlist相比,它的效率要低许多
linkedlist 提供优化的顺序访问性能,同时可以高效率地在列表中部进行插入和删除操作。但在进行随机访问时,速度却相当慢,此时应换用arraylist。也提供了addfirst(),addlast(),getfirst(),getlast(),removefirst()以及removelast()(未在任何接口或基础类中定义),以便将其作为一个规格、队列以及一个双向队列使用
8、使用sets
set(接口) 添加到set的每个元素都必须是独一无二的;否则set就不会添加重复的元素。添加到set里的对象必须定义equals(),从而建立对象的唯一性。set拥有与collection完全相同的接口。一个set不能保证自己可按任何特定的顺序维持自己的元素
hashset* 用于除非常小的以外的所有set。对象也必须定义hashcode()
arrayset 由一个数组后推得到的set。面向非常小的set设计,特别是那些需要频繁创建和删除的。对于小set,与hashset相比,arrayset创建和反复所需付出的代价都要小得多。但随着set的增大,它的性能也会大打折扣。不需要hashcode()
treeset 由一个“红黑树”后推得到的顺序set。这样一来,我们就可以从一个set里提到一个顺序集合
9、 使用maps
map(接口) 维持“键-值”对应关系(对),以便通过一个键查找相应的值
hashmap* 基于一个散列表实现(用它代替hashtable)。针对“键-值”对的插入和检索,这种形式具有最稳定的性能。可通过构建器对这一性能进行调整,以便设置散列表的“能力”和“装载因子”
arraymap 由一个arraylist后推得到的map。对反复的顺序提供了精确的控制。面向非常小的map设计,特别是那些需要经常创建和删除的。对于非常小的map,创建和反复所付出的代价要比hashmap低得多。但在map变大以后,性能也会相应地大幅度降低
treemap 在一个“红-黑”树的基础上实现。查看键或者“键-值”对时,它们会按固定的顺序排列(取决于comparable或comparator,稍后即会讲到)。treemap最大的好处就是我们得到的是已排好序的结果。treemap是含有submap()方法的唯一一种map,利用它可以返回树的一部分
总结
以上就是本文关于java编程思想对象的容纳实例详解的全部内容,希望能对大家有所帮助,也希望大家对本站多多支持!