java.util 中的集合类包含 Java 中某些最常用的类。 最常用的集合类是 List 和 Map。
List 的具体实现包括 ArrayList 和 Vector,它们是可变大小的列表,比较适合构建、存储和操作任何类型对象的元素列表。
List 适用于按数值索引访问元素的情形。
Map 提供了一个更通用的元素存储方法。 Map 集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值。
从概念上而言,您可以将 List 看作是具有数值键的 Map。
而实际上,除了 List 和 Map 都在定义 java.util 中外,两者并没有直接的联系。
Java 核心类中有很多预定义的 Map 类。 在介绍具体实现之前,我们先介绍一下 Map 接口本身,以便了解所有实现的共同点。
Map 接口定义了四种类型的方法,每个 Map 都包含这些方法:
表 1: 覆盖的方法。 我们将这 Object 的这两个方法覆盖,以正确比较 Map 对象的等价性。
Map 构建
Map 定义了几个用于插入和删除元素的变换方法(表 2)。
表 2: Map 更新方法: 可以更改 Map 内容。
注意:
使用 putAll() 通常并不比使用大量的 put() 调用更有效率,但 putAll() 的存在一点也不稀奇。 这是因为,putAll() 除了迭代 put() 所执行的将每个键值对添加到 Map 的算法以外,还需要迭代所传递的 Map 的元素。 但应注意,putAll() 在添加所有元素之前可以正确调整 Map 的大小,因此如果您未亲自调整 Map 的大小(我们将对此进行简单介绍),则 putAll() 可能比预期的更有效。
查看 Map
迭代 Map 中的元素不存在直接了当的方法。 如果要查询某个 Map 以了解其哪些元素满足特定查询,或如果要迭代其所有元素(无论原因如何),则您首先需要获取该 Map 的“视图”。
有三种可能的视图
- 所有键值对 — 参见 entrySet()
- 所有键 — 参见 keySet()
- 所有值 — 参见 values()
前两个视图均返回 Set 对象,第三个视图返回 Collection 对象。要进行迭代,您必须获得一个 Iterator 对象。
Iterator keyValuePairs = aMap.entrySet().iterator();
Iterator keys = aMap.keySet().iterator();
Iterator values = aMap.values().iterator();
注意:
这些对象(Set、Collection 和 Iterator)实际上是基础 Map 的视图,而不是包含所有元素的副本。 这使它们的使用效率很高。 另一方面,Collection 或 Set 对象的 toArray() 方法却创建包含 Map 所有元素的数组对象,因此除了确实需要使用数组中元素的情形外,其效率并不高。
忽略创建数组所需的时间,使用已从 toArray 调用中创建的数组迭代元素的速度要比使用 Iterator 的速度大约快 30%-60%。
但如果将使用 toArray 方法创建数组的开销包含在内,则使用 Iterator 实际上要快 10%-20%。
因此,如果由于某种原因要创建一个集合元素的数组而非迭代这些元素,则应使用该数组迭代元素。 但如果您不需要此中间数组,则不要创建它,而是使用 Iterator 迭代元素。
表 3: 返回视图的 Map 方法:
访问元素
表 4 中列出了 Map 访问方法。Map 通常适合按键(而非按值)进行访问。
containsValue() 方法很可能需要扫描 Map 中的值,因此它的速度可能比较慢
注意:
对使用 containsKey() 和 containsValue() 遍历 HashMap 中所有元素所需时间的测试表明,containsValue() 所需的时间要长很多。 实际上要长几个数量级!
一个可行的解决方案是再创建一个 Map,并将第一个 Map 的所有值作为键。 这样,第一个 Map 上的 containsValue() 将成为第二个 Map 上更有效的 containsKey()。
核心 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 都使用哈希映射。
在 Java 基于哈希的 Map 中,哈希函数将对象转换为一个适合内部数组的整数。
实际上,在 1.4 版发布之前,这就是各种基于哈希的 Map 类所使用的哈希函数。
int hashvalue = (key.hashCode() & 0x7FFFFFFF) % table.length;
我们的哈希函数将任意对象映射到一个数组位置,但如果两个不同的键映射到相同的位置,情况将会如何?
这是一种必然发生的情况。 在哈希映射的术语中,这称作冲突。
Map 处理这些冲突的方法是在索引位置处插入一个链接列表,并简单地将元素添加到此链接列表。
因此,一个基于哈希的 Map 的基本 put() 方法可能如下所示:
public Object put(Object key, Object value) {
//我们的内部数组是一个 Entry 对象数组
//Entry[] table;
//获取哈希码,并映射到一个索引
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % table.length;
//循环遍历位于 table[index] 处的链接列表,以查明
//我们是否拥有此键项 — 如果拥有,则覆盖它
for (Entry e = table[index] ; e != null ; e = e.next) {
//必须检查键是否相等,原因是不同的键对象
//可能拥有相同的哈希
if ((e.hash == hash) && e.key.equals(key)) {
//这是相同键,覆盖该值
//并从该方法返回 old 值
Object old = e.value;
e.value = value;
return old;
}
}
//仍然在此处,因此它是一个新键,只需添加一个新 Entry
//Entry 对象包含 key 对象、 value 对象、一个整型的 hash、
//和一个指向列表中的下一个 Entry 的 next Entry
//创建一个指向上一个列表开头的新 Entry,
//并将此新 Entry 插入表中
Entry e = new Entry(hash, key, value, table[index]);
table[index] = e;
return null;
}
此外,还有一些需要进一步考虑的事项,如处理空键和值以及调整内部数组。
此处定义的 put() 方法还包含相应 get() 的算法,这是因为插入包括搜索映射索引处的项以查明该键是否已经存在(即 get() 方法与 put() 方法具有相同的算法,但 get() 不包含插入和覆盖代码。)
优化 Hasmap
如果哈希映射的内部数组只包含一个元素,则所有项将映射到此数组位置,从而构成一个较长的链接列表。 由于我们的更新和访问使用了对链接列表的线性搜索,而这要比 Map 中的每个数组索引只包含一个对象的情形要慢得多,因此这样做的效率很低。
因此,使用一个较大的数组而不是让太多的项聚集在太少的数组位置中是有意义的。
调整 Map 实现的大小
在哈希术语中,内部数组中的每个位置称作“存储桶”(bucket),而可用的存储桶数(即内部数组的大小)称作容量 (capacity)。 为使 Map 对象有效地处理任意数目的项,Map 实现可以调整自身的大小。 但调整大小的开销很大。 调整大小需要将所有元素重新插入到新数组中,这是因为不同的数组大小意味着对象现在映射到不同的索引值。 先前冲突的键可能不再冲突,而先前不冲突的其他键现在可能冲突。 这显然表明,如果将 Map 调整得足够大,则可以减少甚至不再需要重新调整大小,这很有可能显著提高速度。
使用负载因子
为确定何时调整大小,而不是对每个存储桶中的链接列表的深度进行记数,基于哈希的 Map 使用一个额外参数并粗略计算存储桶的密度。 Map 在调整大小之前,使用名为“负载因子”的参数指示 Map 将承担的“负载”量,即它的负载程度。
如果(负载因子)x(容量)>(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,而不是任何具体实现,即不要声明为 HashMap 或 Hashtable,或任何其他 Map 类实现。
criticalMap = new HashMap(); //好
HashMap criticalMap = new HashMap(); //差