java中map集合的用法Word格式文档下载.docx
《java中map集合的用法Word格式文档下载.docx》由会员分享,可在线阅读,更多相关《java中map集合的用法Word格式文档下载.docx(24页珍藏版)》请在冰豆网上搜索。
map
HashMap()
;
Iterator
it
map.entrySet().iterator()
while
(it.hasNext())
Map.Entry
entry
(Map.Entry)
it.next()
Object
key
entry.getKey()
value
entry.getValue()
}
了解最常用的集合类型之一Map的基础知识以及如何针对您应用程序特有的数据优化Map。
本文相关下载:
·
Jack的HashMap测试
OracleJDeveloper10g
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)
将指定值与指定键相关联
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所有元素的数组对象,因此除了确实需要使用数组中元素的情形外,其效率并不高。
我运行了一个小测试(随附文件中的),该测试使用了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();
rem;
i++){
Map.Entryentry=(Map.Entry)keyValuePairs2[i];
ProfilersinOracleJDeveloper
OracleJDeveloper包含一嵌入的监测器,它测量内存和执行时间,使您能够快速识别代码中的瓶颈。
我曾使用Jdeveloper的执行监测器监测HashMap的containsKey()和containsValue()方法,并很快发现containsKey()方法的速度比containsValue()方法慢很多(实际上要慢几个数量级!
)。
(参见图1和图2,以及随附文件中的类)。
此测试使用了两种测量方法:
一种是测量迭代元素的时间,另一种测量使用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()所需的时间要长很多。
实际上要长几个数量级!
(参见图1和图2,以及随附文件中的。
因此,如果containsValue()是应用程序中的性能问题,它将很快显现出来,并可以通过监测您的应用程序轻松地将其识别。
这种情况下,我相信您能够想出一个有效的替换方法来实现containsValue()提供的等效功能。
但如果想不出办法,则一个可行的解决方案是再创建一个Map,并将第一个Map的所有值作为键。
这样,第一个Map上的containsValue()将成为第二个Map上更有效的containsKey()。
图1:
使用JDeveloper创建并运行Map测试类
图2:
在JDeveloper中使用执行监测器进行的性能监测查出应用程序中的瓶颈
核心Map
Java自带了各种Map类。
这些Map类可归为三种类型:
1.通用Map,用于在应用程序中管理映射,通常在java.util程序包中实现
oHashMap
oHashtable
oProperties
oLinkedHashMap
oIdentityHashMap
oTreeMap
oWeakHashMap
oConcurrentHashMap
2.专用Map,您通常不必亲自创建此类Map,而是通过某些其他类对其进行访问
ojava.util.jar.Attributes
ojavax.print.attribute.standard.PrinterStateReasons
ojava.security.Provider
ojava.awt.RenderingHints
ojavax.swing.UIDefaults
3.一个用于帮助实现您自己的Map类的抽象类
oAbstractMap
内部哈希:
哈希映射技术
几乎所有通用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的类)。
图3:
哈希工作原理
该图介绍了哈希映射的基本原理,但我们还没有对其进行详细介绍。
我们的哈希函数将任意对象映射到一个数组位置,但如果两个不同的键映射到相同的位置,情况将会如何?
这是一种必然发生的情况。
在哈希映射的术语中,这称作冲突。
Map处理这些冲突的方法是在索引位置处插入一个链接列表,并简单地将元素添加到此链接列表。
因此,一个基于哈希的Map的基本put()方法可能如下所示
publicObjectput(Objectkey,Objectvalue){
//我们的内部数组是一个Entry对象数组
//Entry[]table;
//获取哈希码,并映射到一个索引
inthash=key.hashCode();
intindex=(hash&
//循环遍历位于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显示了结果,并将所有时间标准化为已预先设置大小的服务器模式(关联文件中的。
对于已预先设置大小的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能够通过减少冲突数来提高执行效率。
虽然我所做的测试(关联文件中的并未表明质数可以始终获得更好的效率,但理想情形是容量取质数。
1.4版后的某些Map(如HashMap和LinkedHashMap,而非Hashtable或IdentityHashMap)使用需要2的幂容量的哈希函数,但下一个最高2的幂容量由这些Map计算,因此您不必亲自计算。
负载因子本身是空间和时间之间的调整折衷。
较小的负载因子将占用更多的空间,但将降低冲突的可能性,从而将加快访问和更新的速度。
使用大于0.75的负载因子可能是不明智的,而使用大于1.0的负载因子肯定是不明知的,这是因为这必定会引发一次冲突。
使用小于0.50的负载因子好处并不大,但只要您有效地调整Map的大小,应不会对小负载因子造成性能开销,而只会造成内存开销。
但较小的负载因子将意味着如果您未预先调整Map的大小,则导致更频繁的调整大小,从而降低性能,因此在调整负载因子时一定要注意这个问题。
选择适当的Map
应使用哪种Map?
它是否需要同步?
要获得应用程序的最佳性能,这可能是所面临的两个最重要的问题。
当使用通用Map时,调整Map大小和选择负载因子涵盖了Map调整选项。
以下是一个用于获得最佳Map性能的简单方法
1.将您的所有Map变量声明为Map,而不是任何具体实现,即不要声明为HashMap或Hashtable,或任何其他Map类实现。
MapcriticalMap=newHashMap();
//好
HashMapcriticalMap=newHashMap();
//差
这使您能够只更改一行代码即可非常轻松地替换任何特定的Map实例。
2.下载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包装在同步的包装器中,即使它将用于多个线程。
使用默认大小和负载因子。
3.监测您的应用程序。
如果发现某个Map造成瓶颈,则分析造成瓶颈的原因,并部分或全部更改该Map的以下内容:
Map类;
Map大小;
负载因子;
关键对象equals()方法实现。
专用的Map的基本上都需要特殊用途的定制Map实现,否则通用Map将实现您所需的性能目标。
Map选择
也许您曾期望更复杂的考量,而这实际上是否显得太容易?
好的,让我们慢慢来。
首先,您应使用哪种Map?
答案很简单:
不要为您的设计选择任何特定的Map,除非实际的设计需要指定一个特殊类型的Map。
设计时通常不需要选择具体的Map实现。
您可能知道自己需要一个Map,但不知道使用哪种。
而这恰恰就是使用Map接口的意义所在。
直到需要时再选择Map实现—如果随处使用“Map”声明的变量,则更改应用程序中任何特殊Map的Map实现只需要更改一行,这是一种开销很少的调整选择。
是否要使用默认的Map实现?
我很快将谈到这个问题。
同步Map
同步与否有何差别?
(对于同步,您既可以使用同步的Map,也可以使用Collections.sy