java的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,一个用于容纳键,另一个用来容纳值:
//: AssocArray.java // Simple version of a Dictionary 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); } } ///:~
#p#分页标题#e#
在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封装器对办理我们的问题毫无意义,所以不得不建设一个新类,用它来满意本身的要求。
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)); } } ///:~
#p#分页标题#e#
每个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要害字,第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库的用户文档中,往往可以找到更多、更具体的说明。