|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有帐号?立即注册
x
先说优点,首先和C,C++这些语言比起来,java很简单,去掉指针的java,非常好理解,自动垃圾回收机制也很好,自从JDK1.5推出以后,性能上又有了很大提高。汇合java.util中的汇合类包括Java中某些最经常使用的类。最经常使用的汇合类是List和Map。List的详细完成包含ArrayList和Vector,它们是可变巨细的列表,对照合适构建、存储和操纵任何范例对象的元素列表。List合用于按数值索引会见元素的情况。
Map供应了一个更通用的元素存储办法。Map汇合类用于存储元素对(称作“键”和“值”),个中每一个键映照到一个值。从观点上而言,您能够将List看做是具无数值键的Map。而实践上,除List和Map都在界说java.util中外,二者并没有间接的接洽。本文将侧重先容中心Java刊行套件中附带的Map,同时还将先容怎样接纳或完成更合用于您使用程序特定命据的公用Map。
懂得Map接口和办法
Java中心类中有良多预界说的Map类。在先容详细完成之前,我们先先容一下Map接口自己,以便懂得一切完成的配合点。Map接口界说了四品种型的办法,每一个Map都包括这些办法。上面,我们从两个一般的办法(表1)入手下手对这些办法加以先容。
表1:掩盖的办法。我们将这Object的这两个办法掩盖,以准确对照Map对象的等价性。equals(Objecto)对照指定对象与此Map的等价性
hashCode()前往此Map的哈希码
Map构建
Map界说了几个用于拔出和删除元素的变更办法(表2)。
表2:Map更新办法:能够变动Map内容。clear()从Map中删除一切映照
remove(Objectkey)从Map中删除键和联系关系的值
put(Objectkey,Objectvalue)将指定值与指定键相干联
clear()从Map中删除一切映照
putAll(Mapt)将指定Map中的一切映照复制到此map
只管您大概注重到,即使假定疏忽构建一个必要传送给putAll()的Map的开支,利用putAll()一般也其实不比利用大批的put()挪用更无效率,但putAll()的存在一点也不希奇。这是由于,putAll()除迭代put()所实行的将每一个键值对增加到Map的算法之外,还必要迭代所传送的Map的元素。但应注重,putAll()在增加一切元素之前能够准确调剂Map的巨细,因而假如您未亲身调剂Map的巨细(我们将对此举行复杂先容),则putAll()大概比预期的更无效。
检察Map
迭代Map中的元素不存在直截了当的办法。假如要查询某个Map以懂得其哪些元素满意特定查询,或假如要迭代其一切元素(不管缘故原由怎样),则您起首必要猎取该Map的“视图”。有三种大概的视图(拜见表3)
一切键值对―拜见entrySet()
一切键―拜见keySet()
一切值―拜见values()
前两个视图均前往Set对象,第三个视图前往Collection对象。就这两种情形而言,成绩到这里并没有停止,这是由于您没法间接迭代Collection对象或Set对象。要举行迭代,您必需取得一个Iterator对象。因而,要迭代Map的元素,必需举行对照啰嗦的编码
IteratorkeyValuePairs=aMap.entrySet().iterator();
Iteratorkeys=aMap.keySet().iterator();
Iteratorvalues=aMap.values().iterator();
值得注重的是,这些对象(Set、Collection和Iterator)实践上是基本Map的视图,而不是包括一切元素的正本。这使它们的利用效力很高。另外一方面,Collection或Set对象的toArray()办法却创立包括Map一切元素的数组对象,因而除的确必要利用数组中元素的情况外,其效力其实不高。
我运转了一个小测试(随附文件中的Test1),该测试利用了HashMap,并利用以下两种办法对迭代Map元素的开支举行了对照:
intmapsize=aMap.size();
IteratorkeyValuePairs1=aMap.entrySet().iterator();
for(inti=0;i<mapsize;i++)
{
Map.Entryentry=(Map.Entry)keyValuePairs1.next();
Objectkey=entry.getKey();
Objectvalue=entry.getValue();
...
}
Object[]keyValuePairs2=aMap.entrySet().toArray();
for(inti=0;i<rem;i++){
{
Map.Entryentry=(Map.Entry)keyValuePairs2[i];
Objectkey=entry.getKey();
Objectvalue=entry.getValue();
...
}
此测试利用了两种丈量办法:一种是丈量迭代元素的工夫,另外一种丈量利用toArray挪用创立数组的其他开支。第一种办法(疏忽创立数组所需的工夫)标明,利用已从toArray挪用中创立的数组迭代元素的速率要比利用Iterator的速率约莫快30%-60%。但假如将利用toArray办法创立数组的开支包括在内,则利用Iterator实践上要快10%-20%。因而,假如因为某种缘故原由要创立一个汇合元素的数组而非迭代这些元素,则应利用该数组迭代元素。但假如您不必要其中间数组,则不要创立它,而是利用Iterator迭代元素。
表3:前往视图的Map办法:利用这些办法前往的对象,您能够遍历Map的元素,还能够删除Map中的元素。entrySet()前往Map中所包括映照的Set视图。Set中的每一个元素都是一个Map.Entry对象,可使用getKey()和getValue()办法(另有一个setValue()办法)会见后者的键元素和值元素
keySet()前往Map中所包括键的Set视图。删除Set中的元素还将删除Map中响应的映照(键和值)
values()前往map中所包括值的Collection视图。删除Collection中的元素还将删除Map中响应的映照(键和值)
会见元素
表4中列出了Map会见办法。Map一般合适按键(而非按值)举行会见。Map界说中没有划定这一定是真的,但一般您能够希冀这是真的。比方,您能够希冀containsKey()办法与get()办法一样快。另外一方面,containsValue()办法极可能必要扫描Map中的值,因而它的速率大概对照慢。
表4:Map会见和测试办法:这些办法检索有关Map内容的信息但不变动Map内容。get(Objectkey)前往与指定键联系关系的值
containsKey(Objectkey)假如Map包括指定键的映照,则前往true
containsValue(Objectvalue)假如此Map将一个或多个键映照到指定值,则前往true
isEmpty()假如Map不包括键-值映照,则前往true
size()前往Map中的键-值映照的数量
对利用containsKey()和containsValue()遍历HashMap中一切元素所需工夫的测试标明,containsValue()所需的工夫要长良多。实践上要长几个数目级!(拜见和,和随附文件中的Test2)。因而,假如containsValue()是使用程序中的功能成绩,它将很快展现出来,并能够经由过程监测您的使用程序轻松地将其辨认。这类情形下,我信任您可以想出一个无效的交换办法来完成containsValue()供应的等效功效。但假如想不出举措,则一个可行的办理计划是再创立一个Map,并将第一个Map的一切值作为键。如许,第一个Map上的containsValue()将成为第二个Map上更无效的containsKey()。
:利用JDeveloper创立并运转Map测试类
:在JDeveloper中利用实行监测器举行的功能监测查出使用程序中的瓶颈
中心Map
Java自带了各类Map类。这些Map类可回为三品种型:
通用Map,用于在使用程序中办理映照,一般在java.util程序包中完成
HashMap
Hashtable
Properties
LinkedHashMap
IdentityHashMap
TreeMap
WeakHashMap
ConcurrentHashMap
公用Map,您一般不用亲身创立此类Map,而是经由过程某些其他类对其举行会见
java.util.jar.Attributes
javax.print.attribute.standard.PrinterStateReasons
java.security.Provider
java.awt.RenderingHints
javax.swing.UIDefaults
一个用于匡助完成您本人的Map类的笼统类
AbstractMap
外部哈希:哈希映照手艺
几近一切通用Map都利用哈希映照。这是一种将元素映照到数组的十分复杂的机制,您应懂得哈希映照的事情道理,以便充实使用Map。
哈希映照布局由一个存储元素的外部数组构成。因为外部接纳数组存储,因而一定存在一个用于断定恣意键会见数组的索引机制。实践上,该机制必要供应一个小于数组巨细的整数索引值。该机制称作哈希函数。在Java基于哈希的Map中,哈希函数将对象转换为一个合适外部数组的整数。您不用为寻觅一个易于利用的哈希函数而年夜伤头脑:每一个对象都包括一个前往整数值的hashCode()办法。要将该值映照到数组,只需将其转换为一个正值,然后在将该值除以数组巨细后取余数便可。以下是一个复杂的、合用于任何对象的Java哈希函数
inthashvalue=Maths.abs(key.hashCode())%table.length;
(%二进制运算符(称作模)将左边的值除以右边的值,然后前往整数情势的余数。)
实践上,在1.4版公布之前,这就是各类基于哈希的Map类所利用的哈希函数。但假如您检察一下代码,您将看到
inthashvalue=(key.hashCode()&0x7FFFFFFF)%table.length;
它实践上是利用更快机制猎取正值的统一函数。在1.4版中,HashMap类完成利用一个分歧且更庞大的哈希函数,该函数基于DougLea的util.concurrent程序包(稍后我将更具体地再次先容DougLea的类)。
:哈希事情道理
该图先容了哈希映照的基础道理,但我们还没有对其举行具体先容。我们的哈希函数将恣意对象映照到一个数组地位,但假如两个分歧的键映照到不异的地位,情形将会怎样?这是一种一定产生的情形。在哈希映照的术语中,这称作抵触。Map处置这些抵触的办法是在索引地位处拔出一个链接列表,并复杂地将元素增加到此链接列表。因而,一个基于哈希的Map的基础put()办法大概以下所示
publicObjectput(Objectkey,Objectvalue){
//我们的外部数组是一个Entry对象数组
//Entry[]table;
//猎取哈希码,并映照到一个索引
inthash=key.hashCode();
intindex=(hash&0x7FFFFFFF)%table.length;
//轮回遍历位于table[index]处的链接列表,以查明
//我们是不是具有此键项―假如具有,则掩盖它
for(Entrye=table[index];e!=null;e=e.next){
//必需反省键是不是相称,缘故原由是分歧的键对象
//大概具有不异的哈希
if((e.hash==hash)&&e.key.equals(key)){
//这是不异键,掩盖该值
//并从该办法前往old值
Objectold=e.value;
e.value=value;
returnold;
}
}
//仍旧在此处,因而它是一个新键,只需增加一个新Entry
//Entry对象包括key对象、value对象、一个整型的hash、
//和一个指向列表中的下一个Entry的nextEntry
//创立一个指向上一个列表开首的新Entry,
//并将此新Entry拔出表中
Entrye=newEntry(hash,key,value,table[index]);
table[index]=e;
returnnull;
}
假如看一下各类基于哈希的Map的源代码,您将发明这基础上就是它们的事情道理。别的,另有一些必要进一步思索的事项,如处置空键和值和调剂外部数组。此处界说的put()办法还包括响应get()的算法,这是由于拔出包含搜刮映照索引处的项以查明该键是不是已存在。(即get()办法与put()办法具有不异的算法,但get()不包括拔出和掩盖代码。)利用链接列表并非办理抵触的独一办法,某些哈希映照利用另外一种“开放式寻址”计划,本文对其不予先容。
优化Hasmap
假如哈希映照的外部数组只包括一个元素,则一切项将映照到此数组地位,从而组成一个较长的链接列表。因为我们的更新和会见利用了对链接列表的线性搜刮,而这要比Map中的每一个数组索引只包括一个对象的情况要慢很多,因而如许做的效力很低。会见或更新链接列表的工夫与列表的巨细线性相干,而利用哈希函数会见或更新数组中的单个元素则与数组巨细有关―就渐进性子(Big-O暗示法)而言,前者为O(n),尔后者为O(1)。因而,利用一个较年夜的数组而不是让太多的项会萃在太少的数组地位中是成心义的。
调剂Map完成的巨细
在哈希术语中,外部数组中的每一个地位称作“存储桶”(bucket),而可用的存储桶数(即外部数组的巨细)称作容量(capacity)。为使Map对象无效地处置恣意数量的项,Map完成能够调剂本身的巨细。但调剂巨细的开支很年夜。调剂巨细必要将一切元素从头拔出到新数组中,这是由于分歧的数组巨细意味着对象如今映照到分歧的索引值。先前抵触的键大概不再抵触,而先前不抵触的其他键如今大概抵触。这明显标明,假如将Map调剂得充足年夜,则能够削减乃至不再必要从头调剂巨细,这很有大概明显进步速率。
利用1.4.2JVM运转一个复杂的测试,即用大批的项(数量凌驾一百万)添补HashMap。表5显现了却果,并将一切工夫尺度化为已事后设置巨细的服务器形式(联系关系文件中的Test3)。关于已事后设置巨细的JVM,客户端和服务器形式JVM运转工夫几近不异(在保持JIT编译阶段后)。但利用Map的默许巨细将激发屡次调剂巨细操纵,开支很年夜,在服务器形式下要多用50%的工夫,而在客户端形式下几近要多用两倍的工夫!
表5:添补已事后设置巨细的HashMap与添补默许巨细的HashMap所需工夫的对照客户端形式服务器形式
事后设置的巨细100%100%
默许巨细294%157%
利用负载因子
为断定什么时候调剂巨细,而不是对每一个存储桶中的链接列表的深度举行记数,基于哈希的Map利用一个分外参数并大略盘算存储桶的密度。Map在调剂巨细之前,利用名为“负载因子”的参数唆使Map将承当的“负载”量,即它的负载水平。负载因子、项数(Map巨细)与容量之间的干系复杂了然:
假如(负载因子)x(容量)>(Map巨细),则调剂Map巨细
比方,假如默许负载因子为0.75,默许容量为11,则11x0.75=8.25,该值向下取整为8个元素。因而,假如将第8个项增加到此Map,则该Map将本身的巨细调剂为一个更年夜的值。相反,要盘算制止调剂巨细所需的初始容量,用将要增加的项数除以负载因子,并向上取整,比方,
关于负载因子为0.75的100个项,应将容量设置为100/0.75=133.33,并将了局向上取整为134(或取整为135以利用奇数)
奇数个存储桶使map可以经由过程削减抵触数来进步实行效力。固然我所做的测试(联系关系文件中的Test4)并未标明质数能够一直取得更好的效力,但幻想情况是容量取质数。1.4版后的某些Map(如HashMap和LinkedHashMap,而非Hashtable或IdentityHashMap)利用必要2的幂容量的哈希函数,但下一个最高2的幂容量由这些Map盘算,因而您不用亲身盘算。
负载因子自己是空间和工夫之间的调剂折中。较小的负载因子将占用更多的空间,但将下降抵触的大概性,从而将加速会见和更新的速率。利用年夜于0.75的负载因子多是不明智的,而利用年夜于1.0的负载因子一定是不明知的,这是由于这一定会激发一次抵触。利用小于0.50的负载因子优点其实不年夜,但只需您无效地调剂Map的巨细,应不会对小负载因子形成功能开支,而只会形成内存开支。但较小的负载因子将意味着假如您未事后调剂Map的巨细,则招致更频仍的调剂巨细,从而下降功能,因而在调剂负载因子时必定要注重这个成绩。
选择得当的Map
应利用哪一种Map?它是不是必要同步?要取得使用程序的最好功能,这多是所面对的两个最主要的成绩。当利用通用Map时,调剂Map巨细和选择负载因子涵盖了Map调剂选项。
以下是一个用于取得最好Map功能的复杂办法
将您的一切Map变量声明为Map,而不是任何详细完成,即不要声明为HashMap或Hashtable,或任何其他Map类完成。
MapcriticalMap=newHashMap();//好
HashMapcriticalMap=newHashMap();//差
这使您可以只变动一行代码便可十分轻松地交换任何特定的Map实例。
下载DougLea的util.concurrent程序包(http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html)。将ConcurrentHashMap用作默许Map。当移植到1.5版时,将java.util.concurrent.ConcurrentHashMap用作您的默许Map。不要将ConcurrentHashMap包装在同步的包装器中,即便它将用于多个线程。利用默许巨细和负载因子。
监测您的使用程序。假如发明某个Map形成瓶颈,则剖析形成瓶颈的缘故原由,并部分或全体变动该Map的以下内容:Map类;Map巨细;负载因子;关头对象equals()办法完成。公用的Map的基础上都必要特别用处的定制Map完成,不然通用Map将完成您所需的功能方针。
Map选择
大概您曾希冀更庞大的考量,而这实践上是不是显得太简单?好的,让我们渐渐来。起首,您应利用哪一种Map?谜底很复杂:不要为您的计划选择任何特定的Map,除非实践的计划必要指定一个特别范例的Map。计划时一般不必要选择详细的Map完成。您大概晓得本人必要一个Map,但不晓得利用哪一种。而这恰好就是利用Map接口的意义地点。直到必要时再选择Map完成―假如到处利用“Map”声明的变量,则变动使用程序中任何特别Map的Map完成只必要变动一行,这是一种开支很少的调剂选择。是不是要利用默许的Map完成?我很快将谈到这个成绩。
同步Map
同步与否有何不同?(关于同步,您既可使用同步的Map,也能够利用Collections.synchronizedMap()将未同步的Map转换为同步的Map。后者利用“同步的包装器”)这是一个非常庞大的选择,完整取决于您怎样依据多线程并发会见和更新利用Map,同时还必要举行保护方面的思索。比方,假如您入手下手时未并发更新特定Map,但它厥后变动为并发更新,情形将怎样?在这类情形下,很简单在入手下手时利用一个未同步的Map,并在厥后向使用程序中增加并发更新线程时健忘将此未同步的Map变动为同步的Map。这将使您的使用程序简单溃散(一种要断定和跟踪的最糟的毛病)。但假如默许为同步,则将因随之而来的可骇功能而序列化实行多线程使用程序。看起来,我们必要某种决议树来匡助我们准确选择。
DougLea是纽约州立年夜学奥斯威戈分校盘算机迷信系的传授。他创立了一组大众范畴的程序包(统称util.concurrent),该程序包包括很多能够简化高功能并行编程的有用程序类。这些类中包括两个Map,即ConcurrentReaderHashMap和ConcurrentHashMap。这些Map完成是线程平安的,而且不必要对并发会见或更新举行同步,同时还合用于年夜多半必要Map的情形。它们还远比同步的Map(如Hashtable)或利用同步的包装器更具伸缩性,而且与HashMap比拟,它们对功能的损坏很小。util.concurrent程序包组成了JSR166的基本;JSR166已开辟了一个包括在Java1.5版中的并发有用程序,而Java1.5版将把这些Map包括在一个新的java.util.concurrent程序包中。
一切这统统意味着您不必要一个决议树来决意是利用同步的Map仍是利用非同步的Map,而只需利用ConcurrentHashMap。固然,在某些情形下,利用ConcurrentHashMap其实不符合。但这些情形很少见,而且应详细情形详细处置。这就是监测的用处。
停止语
经由过程OracleJDeveloper能够十分轻松地创立一个用于对照各类Map功能的测试类。更主要的是,集成优秀的监测器能够在开辟过程当中疾速、轻松地辨认功能瓶颈-集成到IDE中的监测器一般被较频仍地利用,以便匡助构建一个乐成的工程。如今,您已具有了一个监测器并懂得了有关通用Map及其功能的基本常识,能够入手下手运转您本人的测试,以查明您的使用程序是不是因Map而存在瓶颈和在那边必要变动所利用的Map。
以上内容先容了通用Map及其功能的基本常识。固然,有关特定Map完成和怎样依据分歧的需求利用它们还存在更多庞大和值得存眷的事项,这些将在本文第2部分中先容。
--------------------------------------------------------------------------------
JackShirazi是OReilly的“Java功能调剂”的作者,和受接待的JavaPerformanceTuning.com网站(供应Java功能信息的环球出名站点)的总监。Jack在Java功能范畴供应征询并著书立说。他还监视JavaPerformanceTuning.com供应的信息,个中包含每一年约莫公布1000条功能技能和很多有关功能工具、会商组等外容的文章。Jack从前还曾公布有关卵白质布局展望和黑洞热力学方面的文章,并且在其余暇时还对某些Perl5中心模块作出了奉献。
java主要分三块,j2se:java的基础核心语言。j2me:java的微型模块,专门针对内存小,没有持续电源等小型设备。j2ee:java的企业模块,专门针对企业数据库服务器的连接维护。 |
|