Java教程是针对JDK 8编写的。本页中描述的示例和实践不利用后续版本中引入的改进,并且可能使用不再可用的技术。
有关Java SE 9和后续版本中更新的语言功能的摘要,请参见Java语言变更。
有关所有JDK版本的新功能、增强功能以及已删除或已弃用选项的信息,请参见JDK发布说明。
Map
是将键映射到值的对象。Map不能包含重复的键:每个键最多只能映射到一个值。它模拟了数学上的函数抽象。Map接口包括基本操作的方法(如put
,get
,remove
,containsKey
,containsValue
,size
和empty
),批量操作(如putAll
和clear
)以及集合视图(如keySet
,entrySet
和values
)。
Java平台包含三种通用的Map
实现: HashMap
,TreeMap
和LinkedHashMap
。它们的行为和性能与HashSet
,TreeSet
和LinkedHashSet
完全相同,如集合接口部分所述。
本页面的剩余部分详细讨论了Map
接口。但首先,这里有一些使用JDK 8聚合操作收集到Map
的更多示例。对于面向对象编程来说,对现实世界对象进行建模是一项常见任务,因此合理地认为一些程序可能会按部门分组员工:
// 按部门分组员工 Map<Department, List<Employee>> byDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment));
或者计算每个部门的薪水总和:
// 按部门计算薪水总和 Map<Department, Integer> totalByDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment, Collectors.summingInt(Employee::getSalary)));
或者按及格和不及格分组学生:
// 将学生分组为及格和不及格 Map<Boolean, List<Student>> passingFailing = students.stream() .collect(Collectors.partitioningBy(s -> s.getGrade()>= PASS_THRESHOLD));
你还可以按城市分组人员:
// 按城市分类人员对象 Map<String, List<Person>> peopleByCity = personStream.collect(Collectors.groupingBy(Person::getCity));
甚至可以级联两个收集器,按州和城市对人员进行分类:
// 级联收集器 Map<String, Map<String, List<Person>>> peopleByStateAndCity = personStream.collect(Collectors.groupingBy(Person::getState, Collectors.groupingBy(Person::getCity)))
再次,这只是使用新的JDK 8 API的一些示例。有关lambda表达式和聚合操作的详细内容,请参阅名为聚合操作的课程。
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>(); // 从命令行初始化频率表 for (String a : args) { Integer freq = m.get(a); m.put(a, (freq == null) ? 1 : freq + 1); } System.out.println(m.size() + "个不同的单词:"); System.out.println(m); } }
这个程序的唯一棘手的地方在于put
语句的第二个参数。该参数是一个条件表达式,如果单词以前从未出现过,则将频率设置为1,如果单词已经出现,则将频率设置为当前值加1。尝试使用以下命令运行此程序:
java Freq if it is to be it is up to me to delegate
程序会产生以下输出。
8个不同的单词: {to=3, delegate=1, be=1, it=2, up=1, if=1, me=1, is=2}
假设你希望按字母顺序查看频率表。你只需要将Map
的实现类型从HashMap
更改为TreeMap
即可。通过进行这个四个字符的更改,该程序将生成相同命令行的以下输出。
8个不同的单词: {be=1, delegate=1, if=1, is=2, it=2, me=1, to=3, up=1}
同样,你可以通过将映射的实现类型更改为LinkedHashMap
,使程序按照单词在命令行中首次出现的顺序打印频率表。这样做将得到以下输出。
8个不同的单词: {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
的实现类型。例如,假设你有一个名为m
的Map
。以下一行代码创建一个新的HashMap
,最初包含与m
相同的所有键值映射。
Map<K, V> copy = new HashMap<K, V>(m);
clear
操作做的就是你想象的那样:它从Map
中移除所有的映射。putAll
操作是Map
接口的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);
并使用迭代器
:
// 基于键的某个属性过滤map。 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
实例。放心吧:一个Map
在被要求返回给定Collection
视图时,总是可以返回同一个对象。这正是java.util
中所有Map
实现的做法。
对于所有三个Collection
视图,调用Iterator
的remove
操作会从后端Map
中移除关联的条目,前提是后端Map
支持元素删除。这在前面的过滤习惯用法中有所说明。
对于entrySet
视图,还可以在迭代过程中通过调用Map.Entry
的setValue
方法更改与键关联的值(再次前提是Map
支持值修改)。请注意,这些是在迭代期间修改Map
的唯一
安全方式;如果在迭代过程中以任何其他方式修改了底层Map
,则行为是未指定的。
所有Collection
视图都支持各种形式的元素删除——remove
、removeAll
、retainAll
和clear
操作,以及Iterator.remove
操作。(同样,这假设后端Map
支持元素删除。)
Collection
视图在任何情况下都不支持元素添加。对于keySet
和values
视图是没有意义的,并且对于entrySet
视图是不必要的,因为后端Map
的put
和putAll
方法提供了相同的功能。
当应用于Collection
视图时,批量操作(containsAll
、removeAll
和retainAll
)是非常强大的工具。首先,假设您想要知道一个Map
是否是另一个Map
的子映射——也就是说,第一个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
。以下是一些会修改的方法。假设您想要删除一个Map
与另一个Map
具有相同键值对的所有键值对。
m1.entrySet().removeAll(m2.entrySet());
假设您想要从一个Map
中删除在另一个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
。
一旦你完成了这一步,你可能会有一堆员工,他们的经理已经不在公司工作了(如果Simon的直接下属中有人也是经理的话)。下面的代码将告诉你哪些员工的经理已经不在公司工作。
Map<Employee, Employee> m = new HashMap<Employee, Employee>(managers); m.values().removeAll(managers.keySet()); Set<Employee> slackers = m.keySet();
这个例子有点棘手。首先,它创建了一个临时副本的Map
,然后从临时副本中移除所有值为原始Map
的键的条目。请记住,原始Map
每个员工都有一个条目。因此,临时Map
中剩余的条目包括所有原始Map
中值(经理)不再是员工的条目。那么,临时副本中的键正好表示我们正在寻找的员工。
还有很多类似于本节中包含的习语,但是列举所有这些习语将是不切实际和乏味的。一旦你掌握了它,当你需要时找到正确的习语并不那么困难。
一个多重映射类似于一个Map
,但它可以将每个键映射到多个值。Java集合框架没有包含多重映射的接口,因为它们并不常用。使用值为List
实例的Map
作为多重映射是一个相当简单的方法。这个技巧在下面的代码示例中演示,它读取一个包含每行一个单词(全小写)的单词列表,并打印出满足大小条件的所有变位词组。一个变位词组是一组单词,它们所有的字母完全相同,但顺序不同。程序在命令行上接受两个参数:(1)字典文件的名称,(2)要打印出的最小变位词组的大小。不打印包含少于指定最小值的单词的变位词组。
有一个标准的技巧来找到变位词组:对于字典中的每个单词,对单词的字母进行字母排序(即,将单词的字母重新排序为字母顺序),并将一个条目放入多重映射中,将字母排序后的单词映射到原始单词。例如,单词bad会导致一个映射abd到bad的条目放入多重映射中。稍加思考就会发现,任何给定键映射到的所有单词形成一个变位词组。简单地遍历多重映射中的键,打印出满足大小限制的每个变位词组。
下面的程序
是对这个技巧的直接实现。
import java.util.*; import java.io.*; public class Anagrams { public static void main(String[] args) { int minGroupSize = Integer.parseInt(args[1]); // 从文件中读取单词并放入模拟的多重映射中 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); } // 打印所有大于等于最小组大小的排列组合 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个单词的字典文件上运行,最小的同字母异序词组大小为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]
其中许多单词似乎有些假的,但这不是程序的错;它们在字典文件中。这是我们使用的字典文件
,它是从Public Domain ENABLE基准参考词汇表中获取的。