转载

Java™ 教程(Map接口)

Map接口

Map 是将键映射到值的对象,map不能包含重复的键:每个键最多可以映射一个值,它模拟数学函数抽象。 Map 接口包括基本操作的方法(如 putgetremovecontainsKeycontainsValuesizeempty ),批量操作(如 putAllclear )和集合视图(如 keySetentrySetvalues )。

Java平台包含三个通用 Map 实现: HashMap 、 TreeMap 和 LinkedHashMap ,它们的行为和性能完全类似于 HashSetTreeSetLinkedHashSet ,如Set接口部分所述。

本页的其余部分详细讨论了 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表达式和聚合操作的深入介绍,请参阅标题为聚合操作的课程。

Map接口基本操作

MapputgetcontainsKeycontainsValuesizeisEmpty )的基本操作与 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 强化了对 equalshashCode 方法的要求,因此可以比较两个 Map 对象的逻辑相等性,而不考虑它们的实现类型,如果两个 Map 实例表示相同的键值映射,则它们是相等的。

按照惯例,所有通用 Map 实现都提供构造函数,这些构造函数接受 Map 对象并初始化新 Map 以包含指定 Map 中的所有键值映射。这个标准的 Map 转换构造函数完全类似于标准的 Collection 构造函数:它允许调用者创建一个所需实现类型的 Map ,该 Map 最初包含另一个 Map 中的所有映射,而不管其他 Map 的实现类型如何。例如,假设你有一个名为 mMap ,以下单行创建一个新的 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

  • keySetMap 中包含键的 Set
  • valuesMap 中包含值的 Collection ,此 Collection 不是 Set ,因为多个键可以映射到相同的值。
  • entrySetMap 中包含的键值对的 SetMap 接口提供了一个名为 Map.Entry 的小型嵌套接口,该接口是此 Set 中元素的类型。

Collection 视图提供迭代 Map 的唯一方法,此示例说明了使用 for-each 构造迭代 Map 中的键的标准语法:

for (KeyType key : m.keySet())
    System.out.println(key);

使用迭代器:

// 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 视图,调用 Iteratorremove 操作将从支持 Map 中删除相关条目,假设支持 Map 一开始就支持元素删除,这由前面的过滤语法说明。

使用 entrySet 视图,还可以通过在迭代期间调用 Map.EntrysetValue 方法来更改与键关联的值(同样,假设 Map 一开始就支持值修改)。请注意,这些是在迭代期间修改 Map 的唯一安全方法,如果在迭代进行过程中以任何其他方式修改基础 Map ,则行为是未指定的。

Collection 视图支持以多种形式删除元素 — removeremoveAllretainAllclear 操作,以及 Iterator.remove 操作(同样,这假设支持 Map 支持元素删除)。

Collection 视图在任何情况下都不支持元素添加,对于 keySetvalues 视图没有任何意义,并且对于 entrySet 视图没有必要,因为支持 MapputputAll 方法提供相同的功能。

Collection视图的花哨用途:Map代数

应用于 Collection 视图时,批量操作( containsAllremoveAllretainAll )是令人惊讶的强大工具。对于初学者,假设你想知道一个 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 共有的所有键值对。

m1.entrySet().removeAll(m2.entrySet());

假设你要从一个 Map 中删除在另一个 Map 中具有映射的所有键。

m1.keySet().removeAll(m2.keySet());

在同一个批量操作中开始混合键和值时会发生什么?假设你有一个 Mapmanagers ,将公司中的每个员工映射到员工的经理,我们会故意模糊键和值对象的类型,没关系,只要它们是相同的,现在假设你想知道所有“个人贡献者”(或非管理者)是谁,以下代码段将准确告诉你你想要了解的内容。

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 的临时副本,并从临时副本中删除其( manager )值是原始 Map 中的键的所有条目,请记住,原始 Map 为每个员工都有一个条目。因此,临时 Map 中的其余条目包括来自原始 Map 的其(经理)值不再是雇员的所有条目,因此,临时副本中的键恰好代表了我们正在寻找的员工。

多重映射

多重映射就像 Map ,但它可以将每个键映射到多个值,Java集合框架不包含多重映射的接口,因为它们并不常用。使用 Map 值为 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 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字的字典文件上运行此程序,最小变位词组大小为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基准参考词列表。

原文  https://segmentfault.com/a/1190000018660055
正文到此结束
Loading...