Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
Map
是将键映射到值的对象。map 不能包含重复键:每个键最多可映射一个值。它模拟了数学 function (函数) 抽象。Map
接口包括基本操作的方法(例如 put
,get
,remove
,containsKey
,containsValue
,size
和 empty
),批量操作(例如 putAll
和 clear
)和集合视图(例如 keySet
,entrySet
和 values
)。
Java 平台包含三个通用 Map
实现:HashMap
,TreeMap
,和 LinkedHashMap
。它们的行为和性能完全类似于 HashSet
,TreeSet
和 LinkedHashSet
,如 The Set Interface 部分所述。
本页的其余部分详细讨论了 Map
接口。但首先,这里有一些使用 JDK 8 聚合操作收集到 Map
的示例。对现实世界对象进行建模是面向对象编程中的常见任务,因此可以合理地认为某些程序可能会按部门对员工进行分组:
// Group employees by department Map<Department, List<Employee>> byDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment));
或者按部门计算所有工资的总和:
// Compute sum of salaries by department Map<Department, Integer> totalByDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment, Collectors.summingInt(Employee::getSalary)));
或者通过及格或不及格来对学生进行分组:
// Partition students into passing and failing Map<Boolean, List<Student>> passingFailing = students.stream() .collect(Collectors.partitioningBy(s -> s.getGrade()>= PASS_THRESHOLD));
你还可以按城市分组:
// Classify Person objects by city Map<String, List<Person>> peopleByCity = personStream.collect(Collectors.groupingBy(Person::getCity));
或者将两个集合串联起来,按州和城市对人们进行分类:
// Cascade Collectors Map<String, Map<String, List<Person>>> peopleByStateAndCity = personStream.collect(Collectors.groupingBy(Person::getState, Collectors.groupingBy(Person::getCity)))
同样,这些只是如何使用新 JDK 8 API 的几个示例。有关 lambda 表达式和聚合操作的深入介绍,请参阅标题为 Aggregate Operations 的课程。
Map
的基本操作(put
,get
,containsKey
,containsValue
,size
和 isEmpty
)的行为与 Hashtable
中的对应行为完全相同。下面的程序
生成其参数列表中找到的单词的频率表。频率表将每个单词映射到它在参数列表中出现的次数。
import java.util.*; public class Freq { public static void main(String[] args) { Map<String, Integer> m = new HashMap<String, Integer>(); // Initialize frequency table from command line for (String a : args) { Integer freq = m.get(a); m.put(a, (freq == null) ? 1 : freq + 1); } System.out.println(m.size() + " distinct words:"); System.out.println(m); } }
关于这个程序唯一棘手的问题是 put
语句的第二个参数。该参数是一个条件表达式,如果该单词之前从未见过,则将频率设置为 1,如果已经见过该单词,则将其设置为当前值加 1。尝试使用以下命令运行此程序:
java Freq if it is to be it is up to me to delegate
该程序产生以下输出。
8 distinct words: {to=3, delegate=1, be=1, it=2, up=1, if=1, me=1, is=2}
假设你希望按字母顺序查看频率表。你所要做的就是将 Map
的实现类型从 HashMap
更改为 TreeMap
。进行这种四个字符的更改会导致程序从同一命令行生成以下输出。
8 distinct words: {be=1, delegate=1, if=1, is=2, it=2, me=1, to=3, up=1}
类似地,你可以通过将 map 的实现类型更改为 LinkedHashMap
,使程序按照单词首次出现在命令行上的顺序打印频率表。这样做会产生以下输出。
8 distinct words: {if=1, it=2, is=2, to=3, be=1, up=1, me=1, delegate=1}
这种灵活性有力地说明了基于接口的框架的强大功能。
与 Set
和 List
接口类似,Map
增强了对 equals
和 hashCode
方法的要求,以便可以比较两个 Map
对象的逻辑相等性,而不考虑它们的实现类型。如果两个 Map
实例表示相同的键值映射,则它们是相等的。
按照规范,所有通用 Map
实现都提供构造函数,这些构造函数接受 Map
对象并初始化新的 Map
以包含指定的 Map
中所有的键值映射。此标准 Map
转换构造函数完全类似于标准 Collection
构造函数:它允许调用者创建最初所需实现类型的 Map
包含另一个 Map
中的所有映射,无论其他 Map
的实现类型如何。例如,假设你有 Map
,名为 m
。以下单行创建一个新的 HashMap
,最初包含与 m
相同的所有键值映射。
Map<K, V> copy = new HashMap<K, V>(m);
clear
操作完全按照你的想法执行:它从 Map
中移除所有映射。putAll
操作是 Collection
接口的 addAll
操作的 Map
类似物。除了明显使用将一个 Map
转储到另一个之外,它还有第二个更微妙的用途。假设 Map
用于表示属性 - 值对的集合;putAll
操作与 Map
转换构造函数结合使用,提供了一种使用默认值实现属性映射创建的简洁方法。以下是演示此技术的静态工厂方法。
static <K, V> Map<K, V> newAttributeMap(Map<K, V>defaults, Map<K, V> overrides) { Map<K, V> result = new HashMap<K, V>(defaults); result.putAll(overrides); return result; }
Collection
视图方法允许以下列三种方式将 Map
视为 Collection
:
keySet
Map
中包含的键的 Set
。values
Map
中包含的值的 Collection
。此 Collection
不是 Set
,因为多个键可以映射到相同的值。entrySet
Map
中包含的键值对的 Set
。Map
接口提供了一个名为 Map.Entry
的小型嵌套接口,这是 Set
中元素的类型。Collection
视图提供 唯一 方法迭代 Map
。此示例说明了使用 for-each
构造迭代 Map
中的键的标准惯用法:
for (KeyType key : m.keySet()) System.out.println(key);
及使用 iterator
:
// Filter a map based on some // property of its keys. for (Iterator<Type> it = m.keySet().iterator(); it.hasNext(); ) if (it.next().isBogus()) it.remove();
迭代值的习惯用法类似。以下是迭代键值对的习惯用法。
for (Map.Entry<KeyType, ValType> e : m.entrySet()) System.out.println(e.getKey() + ": " + e.getValue());
起初,很多人担心这些习惯用法可能会很慢,因为每次调用 Collection
视图操作时,Map
都必须创建一个新的 Collection
实例。请不用担心:每次要求给定 Collection
视图时,Map
始终会返回相同的对象。这正是 java.util
中所有 Map
实现的功能。
使用所有三个 Collection
视图,调用 Iterator
的 remove
操作将从后备 Map
中移除关联的条目,假设后备 Map
支持元素移除。这由前面的过滤习惯用法说明。
使用 entrySet
视图,还可以通过在迭代期间调用 Map.Entry
的 setValue
方法来更改与键关联的值(再次,假设 Map
支持值修改)。请注意,这些是在迭代期间修改 Map
的 only (唯一) 安全方法;如果在迭代进行过程中以任何其他方式修改基础 Map
,则行为未指定。
Collection
视图支持以多种形式移除元素 remove
,removeAll
,retainAll
和 clear
操作,以及 Iterator.remove
操作。(再次,这假设后备 Map
支持元素移除。)
在任何情况下,Collection
视图 都不支持 元素添加。对于 keySet
和 values
视图没有任何意义,并且 entrySet
视图没有必要,因为后备 Map
的 put
和 putAll
方法提供相同的功能。
当应用于 Collection
视图时,批量操作(containsAll
,removeAll
和 retainAll
)是令人惊讶的有效工具。对于初学者,假设你想知道一个 Map
是否是另一个的子图 也就是说,第一个 Map
是否包含第二个中的所有键值映射。以下习惯用法可以解决这个问题。
if (m1.entrySet().containsAll(m2.entrySet())) { ... }
沿着类似的行,假设你想知道两个 Map
对象是否包含所有相同键的映射。
if (m1.keySet().equals(m2.keySet())) { ... }
假设你有一个 Map
表示一组属性 - 值对,两个 Set
表示必需的属性和允许的属性。(允许的属性包括必需的属性。)以下代码段确定属性映射是否符合这些约束,如果不符合则打印详细的错误消息。
static <K, V> boolean validate(Map<K, V> attrMap, Set<K> requiredAttrs, Set<K>permittedAttrs) { boolean valid = true; Set<K> attrs = attrMap.keySet(); if (! attrs.containsAll(requiredAttrs)) { Set<K> missing = new HashSet<K>(requiredAttrs); missing.removeAll(attrs); System.out.println("Missing attributes: " + missing); valid = false; } if (! permittedAttrs.containsAll(attrs)) { Set<K> illegal = new HashSet<K>(attrs); illegal.removeAll(permittedAttrs); System.out.println("Illegal attributes: " + illegal); valid = false; } return valid; }
假设你想知道两个 Map
对象共有的所有键。
Set<KeyType>commonKeys = new HashSet<KeyType>(m1.keySet()); commonKeys.retainAll(m2.keySet());
类似的习惯用法可以为你提供共同的值。
到目前为止提出的所有习惯用法都是非破坏性的;也就是说,他们不会修改后备 Map
。这里有一些会。假设你要移除一个 Map
与另一个键共有的所有键值对。
m1.entrySet().removeAll(m2.entrySet());
假设你要从一个 Map
中移除在另一个中具有映射的所有键。
m1.keySet().removeAll(m2.keySet());
在同一批量操作中开始混合键和值时会发生什么?假设你有一个 Map
,managers
,它将公司中的每个员工映射到员工的经理。我们会故意模糊键和值对象的类型。没关系,只要它们是相同的。现在假设你想知道所有“个人贡献者”(或非管理者)是谁。以下代码段将准确告诉你你想要了解的内容。
Set<Employee> individualContributors = new HashSet<Employee>(managers.keySet()); individualContributors.removeAll(managers.values());
假设你要解雇所有直接向某位经理 Simon 报告的员工。
Employee simon = ... ; managers.values().removeAll(Collections.singleton(simon));
请注意,此习惯用法使用 Collections.singleton
,这是一个静态工厂方法,它返回带有单个指定元素的不可变 Set
。
一旦你完成了这项工作,你可能会有一大堆员工,他们的经理不再为公司工作(如果任何西蒙的直接报告本身就是经理)。以下代码将告诉你哪些员工拥有不再为公司工作的经理。
Map<Employee, Employee> m = new HashMap<Employee, Employee>(managers); m.values().removeAll(managers.keySet()); Set<Employee> slackers = m.keySet();
这个例子有点棘手。首先,它创建 Map
的临时副本,并从临时副本中移除其(manager)值是原始 Map
中的键的所有条目。请记住,原始 Map
为每位员工都有一个条目。因此,临时 Map
中的其余条目包含原始 Map
中的所有条目,其(经理)值不再是雇员。因此,临时副本中的 key 恰好代表了我们正在寻找的员工。
还有很多像本节所包含的那样的习惯用法,但是将它们全部列出是不实际和繁琐的。一旦你掌握了它的窍门,当你需要它的时候,找到一个合适的并不难。
multimap 类似于 Map
,但它可以将每个键映射到多个值。Java 集合框架不包含多重映射的接口,因为它们不常用。将值 List
实例的 Map
用作 multimap,这是一件相当简单的事情。在下一个代码示例中演示了此技术,该示例读取每行包含一个单词(全部小写)的单词列表,并打印出符合大小标准的所有 anagram 组。anagram group (anagram 组) 是一堆单词,所有单词都包含完全相同的字母,但顺序不同。该程序在命令行上有两个参数:(1)字典文件的名称和(2)要打印的 anagram 组的最小大小。不打印包含少于指定最小值的单词组的 anagram 组。
找到 anagram 组有一个标准技巧:对于字典中的每个单词,按字母顺序排列单词中的字母(即将单词的字母重新排序为字母顺序)并将条目放入 multimap,将字母顺序排列的单词映射到原始单词字。例如,单词 bad 会产生 abd 映射 bad 的条目以放入 multimap。片刻的反射将显示任何给定键映射形成 anagram 组的所有单词。迭代 multimap 中的键,打印出满足大小约束的每个 anagram 组是一件简单的事情。
以下程序
是该技术的直接实现。
import java.util.*; import java.io.*; public class Anagrams { public static void main(String[] args) { int minGroupSize = Integer.parseInt(args[1]); // Read words from file and put into a simulated multimap Map<String, List<String>> m = new HashMap<String, List<String>>(); try { Scanner s = new Scanner(new File(args[0])); while (s.hasNext()) { String word = s.next(); String alpha = alphabetize(word); List<String> l = m.get(alpha); if (l == null) m.put(alpha, l=new ArrayList<String>()); l.add(word); } } catch (IOException e) { System.err.println(e); System.exit(1); } // Print all permutation groups above size threshold for (List<String> l : m.values()) if (l.size() >= minGroupSize) System.out.println(l.size() + ": " + l); } private static String alphabetize(String s) { char[] a = s.toCharArray(); Arrays.sort(a); return new String(a); } }
在 173,000 字的字典文件上运行此程序,最小 anagram 组大小为 8 会产生以下输出。
9: [estrin, inerts, insert, inters, niters, nitres, sinter, triens, trines] 8: [lapse, leaps, pales, peals, pleas, salep, sepal, spale] 8: [aspers, parses, passer, prases, repass, spares, sparse, spears] 10: [least, setal, slate, stale, steal, stela, taels, tales, teals, tesla] 8: [enters, nester, renest, rentes, resent, tenser, ternes, treens] 8: [arles, earls, lares, laser, lears, rales, reals, seral] 8: [earings, erasing, gainers, reagins, regains, reginas, searing, seringa] 8: [peris, piers, pries, prise, ripes, speir, spier, spire] 12: [apers, apres, asper, pares, parse, pears, prase, presa, rapes, reaps, spare, spear] 11: [alerts, alters, artels, estral, laster, ratels, salter, slater, staler, stelar, talers] 9: [capers, crapes, escarp, pacers, parsec, recaps, scrape, secpar, spacer] 9: [palest, palets, pastel, petals, plates, pleats, septal, staple, tepals] 9: [anestri, antsier, nastier, ratines, retains, retinas, retsina, stainer, stearin] 8: [ates, east, eats, etas, sate, seat, seta, teas] 8: [carets, cartes, caster, caters, crates, reacts, recast, traces]
许多这些词似乎有点虚伪,但这不是程序的错;他们在字典文件中。这是我们使用的 dictionary file
。它源自 Public Domain ENABLE 基准参考词列表。