映射(Map)是将键映射到值的对象。映射不能包含重复的键:每个键最多可以映射到一个值。它为数学函数抽象建模。Map接口包括用于基本操作的方法(如put,get,remove, containsKey,containsValue,size,和empty),批量操作(如putAll和clear),以及集合视图(如keySet,entrySet,和values)。
Java平台包含三个泛型Map实现: HashMap, TreeMap,和 LinkedHashMap。他们的行为和表现,正是类似HashSet,TreeSet和LinkedHashSet,如在Set接口部分介绍。
该页面的其余部分详细讨论了Map接口。但是首先,这里有一些Map使用JDK 8聚合操作收集到的Map更多示例。对现实世界的对象进行建模是面向对象编程中的常见任务,因此可以合理地认为某些程序可能例如按部门将员工分组:
// Group employees by departmentMap<Department, List<Employee>> byDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment));
或按部门计算所有工资的总和:
// Compute sum of salaries by departmentMap<Department, Integer> totalByDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment,Collectors.summingInt(Employee::getSalary)));
或者通过打分或不打分将学生分组:
// Partition students into passing and failingMap<Boolean, List<Student>> passingFailing = students.stream().collect(Collectors.partitioningBy(s -> s.getGrade()>= PASS_THRESHOLD));
您还可以按城市对人员进行分组:
// Classify Person objects by cityMap<String, List<Person>> peopleByCity= personStream.collect(Collectors.groupingBy(Person::getCity));
甚至串连两个Collectors,按州和城市对人进行分类:
// Cascade CollectorsMap<String, Map<String, List<Person>>> peopleByStateAndCity= personStream.collect(Collectors.groupingBy(Person::getState,Collectors.groupingBy(Person::getCity)))
同样,这些只是如何使用新的JDK 8 API的几个示例。有关lambda表达式和聚合操作的深入介绍,请参阅名为“ 聚合操作”的课程 。
Map接口基本操作
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 linefor (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语句的第二个参数。该自变量是一个条件表达式,其作用是:如果从未出现过该单词,则将频率设置为一个,如果已经看到该单词,则将其频率设置为当前值的一倍以上。尝试使用以下命令运行此程序:
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}
同样,只需将映射的实现类型更改为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的实现类型无关。 例如,假设您有一个名为m的Map。下面的单行代码创建一个新的HashMap,最初包含与m相同的所有键-值映射。
Map<K, V> copy = new HashMap<K, V>(m);
Map接口批量操作
clear操作完全按照您的预期进行操作:从Map中删除所有映射。putAll操作是Collection接口的addAll操作的Map类似物。除了明显地将一个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中元素的类型。
集合视图是唯一迭代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实例。放轻松:没有理由让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方法提供了相同的功能。
集合视图的高效用法:Map代数
当应用于集合视图时,批量操作(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共同的所有键值对。
m1.entrySet().removeAll(m2.entrySet());
假设您要从一个Map中删除所有在另一个中具有映射的键。
m1.keySet().removeAll(m2.keySet());
当您在同一批量操作中开始混合键和值时会发生什么?假设您有一个映射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的(管理者)值不再是雇员的所有条目。因此,临时副本中的键恰好代表了我们正在寻找的员工。
还有更多的习语,如本节中所包含的,但是将它们全部列出将是不切实际且乏味的。一旦掌握了要点,就可以在需要时提出正确的建议。
多映射
多重映射就像一个映射,但是它可以将每个键映射到多个值。 Java Collections Framework不包含用于多重映的接口,因为它们并不常用。使用值为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]);// Read words from file and put into a simulated multimapMap<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 thresholdfor (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);}}
在一个最小的anagram(相同字母异序词)组大小为173,000字的字典文件上运行此程序将产生以下输出。
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]
这些词中的许多似乎有些虚假,但这不是程序的错;它们在字典文件中。这是我们使用的字典文件。它源自“公共领域ENABLE”基准参考单词列表。
