开发人员:J2EE
了解最常用的集合类型之一 Map 的基础知识以及如何针对您应用程序特有的数据优化 Map。
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 对象的等价性。
Map 构建 Map 定义了几个用于插入和删除元素的变换方法(表 2)。
尽管您可能注意到,纵然假设忽略构建一个需要传递给 putAll() 的 Map 的开销,使用 putAll() 通常也并不比使用大量的 put() 调用更有效率,但 putAll() 的存在一点也不稀奇。这是因为,putAll() 除了迭代 put() 所执行的将每个键值对添加到 Map 的算法以外,还需要迭代所传递的 Map 的元素。但应注意,putAll() 在添加所有元素之前可以正确调整 Map 的大小,因此如果您未亲自调整 Map 的大小(我们将对此进行简单介绍),则 putAll() 可能比预期的更有效。 查看 Map 迭代 Map 中的元素不存在直接了当的方法。如果要查询某个 Map 以了解其哪些元素满足特定查询,或如果要迭代其所有元素(无论原因如何),则您首先需要获取该 Map 的“视图”。有三种可能的视图(参见表 3)
前两个视图均返回 Set 对象,第三个视图返回 Collection 对象。就这两种情况而言,问题到这里并没有结束,这是因为您无法直接迭代 Collection 对象或 Set 对象。要进行迭代,您必须获得一个 Iterator 对象。因此,要迭代 Map 的元素,必须进行比较烦琐的编码 Iterator keyValuePairs = aMap.entrySet().iterator(); 值得注意的是,这些对象(Set、Collection 和 Iterator)实际上是基础 Map 的视图,而不是包含所有元素的副本。这使它们的使用效率很高。另一方面,Collection 或 Set 对象的 toArray() 方法却创建包含 Map 所有元素的数组对象,因此除了确实需要使用数组中元素的情形外,其效率并不高。 我运行了一个小测试(随附文件中的),该测试使用了 HashMap,并使用以下两种方法对迭代 Map 元素的开销进行了比较: int mapsize = aMap.size();
Object value = entry.getValue(); ... } 此测试使用了两种测量方法: 一种是测量迭代元素的时间,另一种测量使用 toArray 调用创建数组的其他开销。第一种方法(忽略创建数组所需的时间)表明,使用已从 toArray 调用中创建的数组迭代元素的速度要比使用 Iterator 的速度大约快 30%-60%。但如果将使用 toArray 方法创建数组的开销包含在内,则使用 Iterator 实际上要快 10%-20%。因此,如果由于某种原因要创建一个集合元素的数组而非迭代这些元素,则应使用该数组迭代元素。但如果您不需要此中间数组,则不要创建它,而是使用 Iterator 迭代元素。 表 3:返回视图的 Map 方法: 使用这些方法返回的对象,您可以遍历 Map 的元素,还可以删除 Map 中的元素。
访问元素 表 4 中列出了 Map 访问方法。Map 通常适合按键(而非按值)进行访问。Map 定义中没有规定这肯定是真的,但通常您可以期望这是真的。例如,您可以期望 containsKey() 方法与 get() 方法一样快。另一方面,containsValue() 方法很可能需要扫描 Map 中的值,因此它的速度可能比较慢。 表 4:Map 访问和测试方法: 这些方法检索有关 Map 内容的信息但不更改 Map 内容。
对使用 containsKey() 和 containsValue() 遍历 HashMap 中所有元素所需时间的测试表明,containsValue() 所需的时间要长很多。实际上要长几个数量级!(参见图 1和图 2,以及随附文件中的 。因此,如果 containsValue() 是应用程序中的性能问题,它将很快显现出来,并可以通过监测您的应用程序轻松地将其识别。这种情况下,我相信您能够想出一个有效的替换方法来实现 containsValue() 提供的等效功能。但如果想不出办法,则一个可行的解决方案是再创建一个 Map,并将第一个 Map 的所有值作为键。这样,第一个 Map 上的 containsValue() 将成为第二个 Map 上更有效的 containsKey()。
核心 Map Java 自带了各种 Map 类。这些 Map 类可归为三种类型:
内部哈希: 哈希映射技术 几乎所有通用 Map 都使用哈希映射。这是一种将元素映射到数组的非常简单的机制,您应了解哈希映射的工作原理,以便充分利用 Map。 哈希映射结构由一个存储元素的内部数组组成。由于内部采用数组存储,因此必然存在一个用于确定任意键访问数组的索引机制。实际上,该机制需要提供一个小于数组大小的整数索引值。该机制称作哈希函数。在 Java 基于哈希的 Map 中,哈希函数将对象转换为一个适合内部数组的整数。您不必为寻找一个易于使用的哈希函数而大伤脑筋: 每个对象都包含一个返回整数值的 hashCode() 方法。要将该值映射到数组,只需将其转换为一个正值,然后在将该值除以数组大小后取余数即可。以下是一个简单的、适用于任何对象的 Java 哈希函数 int hashvalue = Maths.abs(key.hashCode()) % table.length; (% 二进制运算符(称作模)将左侧的值除以右侧的值,然后返回整数形式的余数。) 实际上,在 1.4 版发布之前,这就是各种基于哈希的 Map 类所使用的哈希函数。但如果您查看一下代码,您将看到 int hashvalue = (key.hashCode() & 0x7FFFFFFF) % table.length; 它实际上是使用更快机制获取正值的同一函数。在 1.4 版中,HashMap 类实现使用一个不同且更复杂的哈希函数,该函数基于 Doug Lea 的 util.concurrent 程序包(稍后我将更详细地再次介绍 Doug Lea 的类)。
该图介绍了哈希映射的基本原理,但我们还没有对其进行详细介绍。我们的哈希函数将任意对象映射到一个数组位置,但如果两个不同的键映射到相同的位置,情况将会如何? 这是一种必然发生的情况。在哈希映射的术语中,这称作冲突。Map 处理这些冲突的方法是在索引位置处插入一个链接列表,并简单地将元素添加到此链接列表。因此,一个基于哈希的 Map 的基本 put() 方法可能如下所示 public Object put(Object key, Object value) { 如果看一下各种基于哈希的 Map 的源代码,您将发现这基本上就是它们的工作原理。此外,还有一些需要进一步考虑的事项,如处理空键和值以及调整内部数组。此处定义的 put() 方法还包含相应 get() 的算法,这是因为插入包括搜索映射索引处的项以查明该键是否已经存在。(即 get() 方法与 put() 方法具有相同的算法,但 get() 不包含插入和覆盖代码。) 使用链接列表并不是解决冲突的唯一方法,某些哈希映射使用另一种“开放式寻址”方案,本文对其不予介绍。 优化 Hasmap 如果哈希映射的内部数组只包含一个元素,则所有项将映射到此数组位置,从而构成一个较长的链接列表。由于我们的更新和访问使用了对链接列表的线性搜索,而这要比 Map 中的每个数组索引只包含一个对象的情形要慢得多,因此这样做的效率很低。访问或更新链接列表的时间与列表的大小线性相关,而使用哈希函数问或更新数组中的单个元素则与数组大小无关 — 就渐进性质(Big-O 表示法)而言,前者为 O(n),而后者为 O(1)。因此,使用一个较大的数组而不是让太多的项聚集在太少的数组位置中是有意义的。 调整 Map 实现的大小 在哈希术语中,内部数组中的每个位置称作“存储桶”(bucket),而可用的存储桶数(即内部数组的大小)称作容量 (capacity)。为使 Map 对象有效地处理任意数目的项,Map 实现可以调整自身的大小。但调整大小的开销很大。调整大小需要将所有元素重新插入到新数组中,这是因为不同的数组大小意味着对象现在映射到不同的索引值。先前冲突的键可能不再冲突,而先前不冲突的其他键现在可能冲突。这显然表明,如果将 Map 调整得足够大,则可以减少甚至不再需要重新调整大小,这很有可能显著提高速度。 使用 1.4.2 JVM 运行一个简单的测试,即用大量的项(数目超过一百万)填充 HashMap。表 5 显示了结果,并将所有时间标准化为已预先设置大小的服务器模式(关联文件中的 。对于已预先设置大小的 JVM,客户端和服务器模式 JVM 运行时间几乎相同(在放弃 JIT 编译阶段后)。但使用 Map 的默认大小将引发多次调整大小操作,开销很大,在服务器模式下要多用 50% 的时间,而在客户端模式下几乎要多用两倍的时间! 表 5:填充已预先设置大小的 HashMap 与填充默认大小的 HashMap 所需时间的比较
使用负载因子 为确定何时调整大小,而不是对每个存储桶中的链接列表的深度进行记数,基于哈希的 Map 使用一个额外参数并粗略计算存储桶的密度。Map 在调整大小之前,使用名为“负载因子”的参数指示 Map 将承担的“负载”量,即它的负载程度。负载因子、项数(Map 大小)与容量之间的关系简单明了:
例如,如果默认负载因子为 0.75,默认容量为 11,则 11 x 0.75 = 8.25,该值向下取整为 8 个元素。因此,如果将第 8 个项添加到此 Map,则该 Map 将自身的大小调整为一个更大的值。相反,要计算避免调整大小所需的初始容量,用将要添加的项数除以负载因子,并向上取整,例如,
奇数个存储桶使 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 性能的简单方法
Map 选择 也许您曾期望更复杂的考量,而这实际上是否显得太容易? 好的,让我们慢慢来。首先,您应使用哪种 Map?答案很简单: 不要为您的设计选择任何特定的 Map,除非实际的设计需要指定一个特殊类型的 Map。设计时通常不需要选择具体的 Map 实现。您可能知道自己需要一个 Map,但不知道使用哪种。而这恰恰就是使用 Map 接口的意义所在。直到需要时再选择 Map 实现 — 如果随处使用“Map”声明的变量,则更改应用程序中任何特殊 Map 的 Map 实现只需要更改一行,这是一种开销很少的调整选择。是否要使用默认的 Map 实现? 我很快将谈到这个问题。 同步 Map 同步与否有何差别? (对于同步,您既可以使用同步的 Map,也可以使用 Collections.synchronizedMap() 将未同步的 Map 转换为同步的 Map。后者使用“同步的包装器”)这是一个异常复杂的选择,完全取决于您如何根据多线程并发访问和更新使用 Map,同时还需要进行维护方面的考虑。例如,如果您开始时未并发更新特定 Map,但它后来更改为并发更新,情况将如何? 在这种情况下,很容易在开始时使用一个未同步的 Map,并在后来向应用程序中添加并发更新线程时忘记将此未同步的 Map 更改为同步的 Map。这将使您的应用程序容易崩溃(一种要确定和跟踪的最糟糕的错误)。但如果默认为同步,则将因随之而来的可怕性能而序列化执行多线程应用程序。看起来,我们需要某种决策树来帮助我们正确选择。 Doug Lea 是纽约州立大学奥斯威戈分校计算机科学系的教授。他创建了一组公共领域的程序包(统称 util.concurrent),该程序包包含许多可以简化高性能并行编程的实用程序类。这些类中包含两个 Map,即 ConcurrentReaderHashMap 和 ConcurrentHashMap。这些 Map 实现是线程安全的,并且不需要对并发访问或更新进行同步,同时还适用于大多数需要 Map 的情况。它们还远比同步的 Map(如 Hashtable)或使用同步的包装器更具伸缩性,并且与 HashMap 相比,它们对性能的破坏很小。util.concurrent 程序包构成了 JSR166 的基础;JSR166 已经开发了一个包含在 Java 1.5 版中的并发实用程序,而 Java 1.5 版将把这些 Map 包含在一个新的 java.util.concurrent 程序包中。
结束语 通过 Oracle JDeveloper 可以非常轻松地创建一个用于比较各种 Map 性能的测试类。更重要的是,集成良好的监测器可以在开发过程中快速、轻松地识别性能瓶颈 - 集成到 IDE 中的监测器通常被较频繁地使用,以便帮助构建一个成功的工程。现在,您已经拥有了一个监测器并了解了有关通用 Map 及其性能的基础知识,可以开始运行您自己的测试,以查明您的应用程序是否因 Map 而存在瓶颈以及在何处需要更改所使用的 Map。 以上内容介绍了通用 Map 及其性能的基础知识。当然,有关特定 Map 实现以及如何根据不同的需求使用它们还存在更多复杂和值得关注的事项,这些将在本文第 2 部分中介绍。 Jack Shirazi 是 O'Reilly 的“Java 性能调整”的作者,以及受欢迎的 JavaPerformanceTuning.com 网站(提供 Java 性能信息的全球知名站点)的总监。Jack 在 Java 性能领域提供咨询并著书立说。他还监督 JavaPerformanceTuning.com 提供的信息,其中包括每年大约发布 1000 条性能技巧以及许多有关性能工具、讨论组等内容的文章。Jack 早年还曾发布有关蛋白质结构预测以及黑洞热力学方面的文章,而且在其空闲时还对某些 Perl5 核心模块作出了贡献。 |
2,Java Map遍历方式的选择 http://www.cnblogs.com/fczjuever/archive/2013/04/07/3005997.html
1. 阐述
对于Java中Map的遍历方式,很多文章都推荐使用entrySet,认为其比keySet的效率高很多。理由是:entrySet方法一次拿到所有key和value的集合;而keySet拿到的只是key的集合,针对每个key,都要去Map中额外查找一次value,从而降低了总体效率。那么实际情况如何呢?
为了解遍历性能的真实差距,包括在遍历key+value、遍历key、遍历value等不同场景下的差异,我试着进行了一些对比测试。
2. 对比测试
一开始只进行了简单的测试,但结果却表明keySet的性能更好,这一点让我很是费解,不都说entrySet明显好于keySet吗?为了进一步地进行验证,于是采用了不同的测试数据进行更详细的对比测试。
2.1 测试数据
2.1.1 HashMap测试数据
- HashMap-1,大小为100万,key和value均为String,key的值为1、2、3……1000000:
Map<String, String> map = new HashMap<String, String>(); String key, value; for (i = 1; i <= num; i++) { key = "" + i; value = "value"; map.put(key, value); } |
- HashMap-2,大小为100万,key和value均为String,key的值为50、100、150、200、……、50000000:
Map<String, String> map = new HashMap<String, String>(); String key, value; for (i = 1; i <= num; i++) { key = "" + (i * 50); value = "value"; map.put(key, value); } |
2.1.2 TreeMap测试数据
- TreeMap-1,大小为100万,key和value均为String,key的值为1、2、3……1000000:
Map<String, String> map = new TreeMap<String, String>(); String key, value; for (i = 1; i <= num; i++) { key = "" + i; value = "value"; map.put(key, value); } |
- TreeMap-2,大小为100万,key和value均为String,key的值为50、100、150、200、……、50000000,更离散:
Map<String, String> map = new TreeMap<String, String>(); String key, value; for (i = 1; i <= num; i++) { key = "" + (i * 50); value = "value"; map.put(key, value); } |
2.2 测试场景
分别使用keySet、entrySet和values的多种写法测试三种场景:遍历key+value、遍历key、遍历value的场景。
2.2.1 遍历key+value
- keySet遍历key+value(写法1):
Iterator<String> iter = map.keySet().iterator(); while (iter.hasNext()) { key = iter.next(); value = map.get(key); } |
- keySet遍历key+value(写法2):
for (String key : map.keySet()) { value = map.get(key); } |
- entrySet遍历key+value(写法1):
Iterator<Entry<String, String>> iter = map.entrySet().iterator(); Entry<String, String> entry; while (iter.hasNext()) { entry = iter.next(); key = entry.getKey(); value = entry.getValue(); } |
- entrySet遍历key+value(写法2):
for (Entry<String, String> entry: map.entrySet()) { key = entry.getKey(); value = entry.getValue(); } |
2.2.2 遍历key
- keySet遍历key(写法1):
Iterator<String> iter = map.keySet().iterator(); while (iter.hasNext()) { key = iter.next(); } |
- keySet遍历key(写法2):
for (String key : map.keySet()) { } |
- entrySet遍历key(写法1):
Iterator<Entry<String, String>> iter = map.entrySet().iterator(); while (iter.hasNext()) { key = iter.next().getKey(); } |
- entrySet遍历key(写法2):
for (Entry<String, String> entry: map.entrySet()) { key = entry.getKey(); } |
2.2.3 遍历value
- keySet遍历value(写法1):
Iterator<String> iter = map.keySet().iterator(); while (iter.hasNext()) { value = map.get(iter.next()); } |
- keySet遍历value(写法2):
for (String key : map.keySet()) { value = map.get(key); } |
- entrySet遍历value(写法1):
Iterator<Entry<String, String>> iter = map.entrySet().iterator(); while (iter.hasNext()) { value = iter.next().getValue(); } |
- entrySet遍历value(写法2):
for (Entry<String, String> entry: map.entrySet()) { value = entry.getValue(); } |
- values遍历value(写法1):
Iterator<String> iter = map.values().iterator(); while (iter.hasNext()) { value = iter.next(); } |
- values遍历value(写法2):
for (String value : map.values()) { } |
2.3 测试结果
2.3.1 HashMap测试结果
单位:毫秒 |
HashMap-1 |
HashMap-2 |
keySet遍历key+value(写法1) |
39 |
93 |
keySet遍历key+value(写法2) |
38 |
87 |
entrySet遍历key+value(写法1) |
43 |
86 |
entrySet遍历key+value(写法2) |
43 |
85 |
单位:毫秒 |
HashMap-1 |
HashMap-2 |
keySet遍历key(写法1) |
27 |
65 |
keySet遍历key(写法2) |
26 |
64 |
entrySet遍历key(写法1) |
35 |
75 |
entrySet遍历key(写法2) |
34 |
74 |
单位:毫秒 |
HashMap-1 |
HashMap-2 |
keySet遍历value(写法1) |
38 |
87 |
keySet遍历value(写法2) |
37 |
87 |
entrySet遍历value(写法1) |
34 |
61 |
entrySet遍历value(写法2) |
32 |
62 |
values遍历value(写法1) |
26 |
48 |
values遍历value(写法2) |
26 |
48 |
2.3.2 TreeMap测试结果
单位:毫秒 |
TreeMap-1 |
TreeMap-2 |
keySet遍历key+value(写法1) |
430 |
451 |
keySet遍历key+value(写法2) |
429 |
450 |
entrySet遍历key+value(写法1) |
77 |
84 |
entrySet遍历key+value(写法2) |
70 |
68 |
单位:毫秒 |
TreeMap-1 |
TreeMap-2 |
keySet遍历key(写法1) |
50 |
49 |
keySet遍历key(写法2) |
49 |
48 |
entrySet遍历key(写法1) |
66 |
64 |
entrySet遍历key(写法2) |
65 |
63 |
单位:毫秒 |
TreeMap-1 |
TreeMap-2 |
keySet遍历value(写法1) |
432 |
448 |
keySet遍历value(写法2) |
430 |
448 |
entrySet遍历value(写法1) |
62 |
61 |
entrySet遍历value(写法2) |
62 |
61 |
values遍历value(写法1) |
46 |
46 |
values遍历value(写法2) |
45 |
46 |
3. 结论
3.1 如果你使用HashMap
- 同时遍历key和value时,keySet与entrySet方法的性能差异取决于key的具体情况,如复杂度(复杂对象)、离散度、冲突率等。换言之,取决于HashMap查找value的开销。entrySet一次性取出所有key和value的操作是有性能开销的,当这个损失小于HashMap查找value的开销时,entrySet的性能优势就会体现出来。例如上述对比测试中,当key是最简单的数值字符串时,keySet可能反而会更高效,耗时比entrySet少10%。总体来说还是推荐使用entrySet。因为当key很简单时,其性能或许会略低于keySet,但却是可控的;而随着key的复杂化,entrySet的优势将会明显体现出来。当然,我们可以根据实际情况进行选择
- 只遍历key时,keySet方法更为合适,因为entrySet将无用的value也给取出来了,浪费了性能和空间。在上述测试结果中,keySet比entrySet方法耗时少23%。
- 只遍历value时,使用vlaues方法是最佳选择,entrySet会略好于keySet方法。
- 在不同的遍历写法中,推荐使用如下写法,其效率略高一些:
for (String key : map.keySet()) { value = map.get(key); } |
for (Entry<String, String> entry: map.entrySet()) { key = entry.getKey(); value = entry.getValue(); } |
for (String value : map.values()) { } |
3.2 如果你使用TreeMap
- 同时遍历key和value时,与HashMap不同,entrySet的性能远远高于keySet。这是由TreeMap的查询效率决定的,也就是说,TreeMap查找value的开销较大,明显高于entrySet一次性取出所有key和value的开销。因此,遍历TreeMap时强烈推荐使用entrySet方法。
- 只遍历key时,keySet方法更为合适,因为entrySet将无用的value也给取出来了,浪费了性能和空间。在上述测试结果中,keySet比entrySet方法耗时少24%。
- 只遍历value时,使用vlaues方法是最佳选择,entrySet也明显优于keySet方法。
- 在不同的遍历写法中,推荐使用如下写法,其效率略高一些:
for (String key : map.keySet()) { value = map.get(key); } |
for (Entry<String, String> entry: map.entrySet()) { key = entry.getKey(); value = entry.getValue(); } |
for (String value : map.values()) { } |