Map interface
Map是将键 Map 到值的对象。Map 不能包含重复的键:每个键最多可以 Map 到一个值。它为 math 函数抽象建模。 Map
interface包括用于基本操作(例如put
,get
,remove
,containsKey
,containsValue
,size
和empty
),批量操作(例如putAll
和clear
)以及集合视图(例如keySet
,entrySet
和values
)的方法。
Java 平台包含三个通用的Map
实现:HashMap,TreeMap和LinkedHashMap。它们的行为和性能与HashSet
,TreeSet
和LinkedHashSet
精确相似,如设置interface部分中所述。
该页面的其余部分详细讨论了Map
interface。但是首先,这里有一些使用 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));
甚至级联两个收集器,按 State 和城市对人进行分类:
// 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 interface的基本操作
Map
(put
,get
,containsKey
,containsValue
,size
和isEmpty
)的基本操作与Hashtable
中的对应操作完全一样。 following program生成在其参数列表中找到的单词的频率表。频率表将每个单词 Map 到其在参数列表中出现的次数。
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
语句的第二个参数。该自变量是一个条件表达式,其作用是:如果从未出现过该单词,则将频率设置为一个,如果已经看到该单词,则将频率设置为当前值的一个。try使用以下命令运行此程序:
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}
假设您希望按字母 Sequences 查看频率表。您要做的就是将Map
的实现类型从HashMap
更改为TreeMap
。进行此四个字符的更改将导致程序从同一命令行生成以下输出。
8 distinct words:
{be=1, delegate=1, if=1, is=2, it=2, me=1, to=3, up=1}
同样,只需将 Map 的实现类型更改为LinkedHashMap
,就可以使程序按单词在命令行上首次出现的 Sequences 打印频率表。这样做将产生以下输出。
8 distinct words:
{if=1, it=2, is=2, to=3, be=1, up=1, me=1, delegate=1}
这种灵 Active 有效地说明了基于interface的框架的功能。
像Set和Listinterface一样,Map
加强了对equals
和hashCode
方法的要求,因此可以比较两个Map
对象的逻辑相等性,而不必考虑它们的实现类型。如果两个Map
实例表示相同的键值 Map,则它们相等。
按照约定,所有通用的Map
实现都提供带有Map
对象并初始化新的Map
以包含指定Map
中的所有键-值 Map 关系的构造函数。此标准Map
转换构造函数完全类似于标准Collection
构造函数:它允许调用者创建所需实现类型的Map
,该类型最初包含另一个Map
中的所有 Map,而与其他Map
的实现类型无关。例如,假设您有一个名为m
的Map
。以下单线代码创建一个新的HashMap
,最初包含与m
相同的所有键-值 Map。
Map<K, V> copy = new HashMap<K, V>(m);
Map interface批量操作
clear
操作完全按照您认为的方式执行:从Map
中删除所有 Map。 putAll
操作是Collection
interface的addAll
操作的Map
模拟。除了明显地将一个Map
倾倒到另一个中之外,它还有第二个更微妙的用法。假设使用Map
表示属性值对的集合; putAll
操作与Map
转换构造函数结合使用,提供了一种使用默认值实现属性 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 Views
Collection
视图方法允许通过以下三种方式将Map
视为Collection
:
-
keySet
—Map
中包含的键Set
。 -
values
—Map
中包含的值Collection
。Collection
不是Set
,因为多个键可以 Map 到相同的值。 -
entrySet
—Map
中包含的键值对Set
。Map
interface提供了一个称为Map.Entry
的小型嵌套interface,该interface是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
实例。放轻松:没有理由让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 代数
当应用于Collection
视图时,批量操作(containsAll
,removeAll
和retainAll
)是令人惊讶的强大工具。对于 Starters,假设您想知道一个Map
是否是另一个的子 Map—也就是说,第一个Map
是否包含第二个的所有键-值 Map。下面的惯用法可以解决问题。
if (m1.entrySet().containsAll(m2.entrySet())) {
...
}
同样,假设您想知道两个Map
对象是否包含所有相同键的 Map。
if (m1.keySet().equals(m2.keySet())) {
...
}
假设您有一个Map
代表一个属性值对的集合,两个Set
代表必需的属性和允许的属性。 (允许的属性包括必需的属性.)以下代码段确定属性 Map 是否符合这些约束,如果不符合,则打印详细的错误消息。
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
共有的所有键值对。
m1.entrySet().removeAll(m2.entrySet());
假设您要从一个Map
删除所有在另一个 Map 中的键。
m1.keySet().removeAll(m2.keySet());
当您在同一批量操作中开始混合键和值时会发生什么?假设您有一个Map
,managers
,它将公司中的每个员工 Map 到该员工的 Manager。我们将故意模糊键和值对象的类型。没关系,只要它们相同即可。现在,假设您想知道所有“个人贡献者”(或非 管理 者)是谁。以下代码段将准确告诉您您想知道的内容。
Set<Employee> individualContributors = new HashSet<Employee>(managers.keySet());
individualContributors.removeAll(managers.values());
假设您要解雇所有直接向某个 Manager 西蒙报告的员工。
Employee simon = ... ;
managers.values().removeAll(Collections.singleton(simon));
请注意,该惯用法使用了Collections.singleton
(一种静态工厂方法),该方法返回带有单个指定元素的不可变Set
。
完成此操作后,您可能会有一群员工的 Manager 不再在公司工作(如果 Simon 的直接报表中的任何人本身就是 Manager)。以下代码将告诉您哪些员工的 Manager 不再为公司工作。
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
的所有条目,其(Manager)值不再是雇员。因此,临时副本中的密钥恰好代表了我们正在寻找的员工。
还有更多的习语,如本节中所包含的,但将它们全部列出将是不切实际且乏味的。一旦掌握了要点,就可以在需要时提出正确的建议。
Multimaps
多重 Map 就像Map
,但是它可以将每个键 Map 到多个值。 Java Collections Framework 不包含用于多图的interface,因为它们并不常用。使用值为List
实例的Map
作为多图是一件相当简单的事情。下一个代码示例中演示了此技术,该示例读取一个单词列表,其中每行包含一个单词(全部为小写字母),并打印出所有符合大小标准的字谜组。字谜组是一堆单词,所有单词都包含完全相同的字母,但 Sequences 不同。该程序在命令行上接受两个参数:(1)词典文件的名称和(2)要打印的字谜组的最小大小。包含少于指定最小值的单词的组合词组不会打印。
查找字谜组的标准技巧是:对于字典中的每个单词,将单词中的字母按字母 Sequences 排序(即,将单词的字母重新排序为字母 Sequences),然后将条目放入多图,将按字母 Sequences 排列的单词 Map 到原始单词字。例如,单词* bad 导致 abd 到 bad *的条目 Map 被放入多图。片刻的反思将表明,任何给定键 Map 所针对的所有单词都组成一个字谜组。遍历多图中的键,打印出满足大小限制的每个字谜组,这很简单。
以下程序是此技术的直接实现。
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);
}
}
在一个最小的 anagram 组大小为 8 的 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]
这些词中有许多看起来有些虚假,但这不是程序的错;它们在字典文件中。这是我们使用的dictionary file。它源自“公共领域 ENABLE”基准参考单词列表。