Map接口
一个Map
是一个键值对对象。
一个map不能包含重复的键:每个键最多可以映射到一个值,它对数学函数抽象进行了建模。
Map接口基础操作
put
get
remove
containsKey
containsValue
size
empty
批量操作
putAll
clear
集合视图
keySet
entrySet
values
Java平台包含三种Map实现:HashMap(无序)、TreeMap(按键的字母顺序顺序)和LinkedHashMap(原顺序),它们的行为和性能类似于HashSet、TreeSet和LinkedHashSet,如 The Set Interface一节所述。
JDK8聚合操作收集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));
按城市进行分组:
// Cascade Collectors
Map<String, Map<String, List<Person>>> peopleByStateAndCity
= personStream.collect(Collectors.groupingBy(Person::getState,
Collectors.groupingBy(Person::getCity)))
Map接口基础操作
Map的基本操作(put、get、containsKey、containsValue、size和isEmpty)与Hashtable中的对应操作完全相同。
一个单词频次表的例子:
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);
}
}
与set和Listinterfaces类似,Map加强了对equals和hashCode方法,以便比较两个Map对象的逻辑相等性,而不必考虑它们的实现类型。两个Map的键值都相等,则两者相等。
通常所有通用Map实现的构造函数接受Map对象并初始化新Map,相当于复制一个相同的Map。这个标准Map转换构造函数完全类似于标准Collection 构造函数。
Map<K, V> copy = new HashMap<K, V>(m);
Map接口批量接口
clear 删除所有元素
putAll 类似于Collection的addAll,与构造函数一起使用可实现按默认值创建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 方法可以把Map视为Collection:
- keySet — 返回Map的key的Set。
- values — 返回Map的值的Collection。
- entrySet — 返回Map键值对构成的Set,类型为Map.Entry。
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总是返回相同的对象,这正是java中所有Map实现在
java.util
中做的。
三种Collection 视图中,Iterator的 remove 和 entrySet视图的Map.Entry's setValue 是Map迭代过程中的安全操作,其他操作行为可能是不明确的。
Collection视图支持移除操作, remove, removeAll, retainAll和 clear 。
在任何情况下,Collection视图都不支持添加元素,因为Map的put和putAll方法已经提供了相同的功能。
Collection视图的奇妙用法:映射代数
判断一个Map是否为另一个Map的子集,即包含所有键值对:
if (m1.entrySet().containsAll(m2.entrySet())) {
...
}
判断两个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公共的key:
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,映射员工和管理者的关系,找出没有管理者的员工:
Set<Employee> individualContributors = new HashSet<Employee>(managers.keySet());
individualContributors.removeAll(managers.values());
开除Simon的所有下属:
Employee simon = ... ;
managers.values().removeAll(Collections.singleton(simon));
所属管理者不再为公司工作的员工:
Map<Employee, Employee> m = new HashMap<Employee, Employee>(managers);
m.values().removeAll(managers.keySet());
Set<Employee> slackers = m.keySet();
Multimaps多重映射
multimap类似于Map,但它可以将每个键映射到多个值。
multimap类似于Map,但它可以将每个键映射到多个值。Java Collections框架没有包含用于multimap的接口,因为它们并不经常使用。把值类型为List的Map用作multimap,是一件相当简单的事情。下一个代码示例将演示这种技术,该代码示例读取每行包含一个单词的单词列表(全部小写),并打印出满足大小标准的所有字谜组。字谜组是一堆单词,它们都包含完全相同的字母,但顺序不同。
该程序在命令行上接受两个参数:(1)字典文件的名称和(2)要打印的字谜组的最小大小。包含少于指定最小字数的字谜组不会被打印。
有一个查找字谜组的标准技巧:对于字典中的每个单词,将单词中的字母按字母顺序排列(即将单词的字母按字母顺序重新排列),并将一个条目放入multimap中,将按字母顺序排列的单词映射到原始单词。例如,bad这个词会导致将abd映射为bad的条目被放入multimap中。稍作思考,你就会发现,所有的字词,任何给定的关键图都可以组成一个字词组合。遍历multimap中的键,打印出满足大小约束的每个字谜组,是一件很简单的事情。
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);
}
}