β

[集合框架] Map 接口

Aptusource.orgAptusource.org 544 阅读

Map 是一个映射键值的对象。Map 中不能包含重复的键:每个键最多只能映射一个值。Map 接口中的方法包括基本操作(例如:put、get、remove、containsKey、containsValue、size、和 empty),批量操作(例如:putAll 和 clear),集合视图(例如:keySet、entrySet 和 values)。

Java 平台中包含了三个 Map 的实现,HashMap、TreeMap、和 LinkedHashMap。这三个 Map 的行为和性能与 HashSet、TreeSet 和 LinkedHashSet 类似,具体可参考 《Set 接口》 一文。

接下来,我们来研究 Map 接口的细节。首先,看看在 Java 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)))

这里的例子都是使用 Java 8 的简单例子。更多详细的 Java 8 集合处理可参考后续文章。

Map 接口的基本操作

Map 的基本操作包括 put、get、containsKey、containsValue、size 和 isEmpty 方法。下面的程序将会计算命令行参数中的单词出现的频率:

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);
    }
}

使用下面的命令行运行这个程序:

java Freq if it is to be it is up to me to delegate

运行后的输出:

 distinct words:
{to=3, delegate=1, be=1, it=2, up=1, if=1, me=1, is=2}

如果你希望看到单词按照字典顺序输出,那么需要将 HashMap 替换为 TreeMap。替换后再次运行的输出如下:

 distinct words:
{be=1, delegate=1, if=1, is=2, it=2, me=1, to=3, up=1}

同理,如果你希望按照你输入的顺序输出单词,那么可以使用 LinkedHashMap 作为 Map 实现。替换后再次运行输出如下:

 distinct words:
{if=1, it=2, is=2, to=3, be=1, up=1, me=1, delegate=1}

上面的例子充分体现了基于接口编码带来的好处。

Map 改进了 equals 和 hashCode 方法,因此不管 Map 的具体实现是什么,只要它们中包含的键值对全部相同,那么两个 Map 就相等(equal)。

按照惯例,Map 的实现中需要有一个构造方法用于接收 Map 类型的参数,新构建的 Map 使用参数中的 Map 来初始化数据。例如,如果已有一个 Map m,那么可以使用 m 的数据来创建一个新的 Map:

Map<K, V> copy = new HashMap<K, V>(m);

Map 接口的批量操作

clear 方法将删除 Map 中的所有键值对。putAll 方法用于将参数中 map 的数据导入到当前 map。下面的静态方法先使用 defaults 创建一个新 map,然后将 overrides 的值导入到新 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;
}

集合视图

Map 一共有下面三种集合视图:

集合视图可用于遍历 Map。下面演示使用 for 循环遍历 Map 中的键:

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

使用迭代器,根据条件过滤 Key 值:

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 对象是否会影响效率。其实不用担心,因为每次获取集合视图返回的对象都是相同的,并不会创建新的 Collection。java.util 包中的所有 Map 实现都遵循这个原则。

使用这三个集合视图,可以调用 Iterator.remove 方法从 Map 中删除关联的元素,前面的那个过滤的例子就是这么做的。

使用 entrySet,可以调用 Map.Entry 的 setValue 方法在遍历期间改变元素的值。这是在遍历期间改变 Map 值的唯一安全的方法,在遍历期间使用任何其它的方式改变 Map 的值的行为是不规范的。

集合视图本身支持多种删除元素的方法,包括remove、removeAll、retainAll 和 clear 方法。

集合视图不支持新增元素,因为单独在 keySet 和 values 上新增元素没有意义,而在 entrySet 上新增元素却没有必要,因为可以使用 Map 的 put 和 putAll 方法达到同样的效果。

集合视图的高级用法

当使用集合视图的批量操作的时候(containsAll、removeAll 和 retainAll),可以发现一些有趣的用法。首先,如果你想知道一个 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 中相同的键,可以使用下面代码:

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,它保存了员工和直属经理的键值对。假设员工和经理使用相同的对象,现在假设我们想要获取那些员工没有直属经理,可以使用下面的代码:

Set<Employee> individualContributors = new HashSet<Employee>(managers.keySet());
individualContributors.removeAll(managers.values());

假设你要删除直属经理是 Simon 的所有员工,可以使用下面的代码:

Employee simon = ... ;
managers.values().removeAll(Collections.singleton(simon));

上面的代码中使用到了 Collections.singleton 方法,这个静态的工厂方法将会返回一个只有一个元素的不可变 Set。

如果运行了上面的代码,那么删除的员工中有可能自身也是经理,下面的代码将会得到这些被删除的经理下的所有员工:

Map<Employee, Employee> m = new HashMap<Employee, Employee>(managers);
m.values().removeAll(managers.keySet());
Set<Employee> slackers = m.keySet();

好了,这样的例子可以列举出很多。不过为了不让这篇文章过于冗长和枯燥,我们就此结束。一旦你能理解其中的窍门,那么你也能编写出巧妙的代码。

多重映射

多重映射相当于一个 Map,不过同一个键映射了多个值。Java 集合框架中没有包含多重映射的接口,因为它们很不常用。如果要实现多重映射,可以将 Map 的值设置为 List 类型来实现。下面的例子将演示这个用法,它将读取文件中的单词(假设每行一个单词,并且都是小写)。如果单词中包含的字母一样,字母顺序可能不同,那么我们就将这些单词分为一组,并打印出所有的分组。这个程序接受两个命令行参数:1)读取文件的目录;2)打印出的分组中最少要多少个单词;如果分组中的单词数少于指定的最少单词数,将不会打印出来。

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);
    }
}

运行这个程序后,设置最小单词数为 8,输出结果可能是:

: [estrin, inerts, insert, inters, niters, nitres, sinter,
     triens, trines]
: [lapse, leaps, pales, peals, pleas, salep, sepal, spale]
: [aspers, parses, passer, prases, repass, spares, sparse,
     spears]
: [least, setal, slate, stale, steal, stela, taels, tales,
      teals, tesla]
: [enters, nester, renest, rentes, resent, tenser, ternes,
     treens]
: [arles, earls, lares, laser, lears, rales, reals, seral]
: [earings, erasing, gainers, reagins, regains, reginas,
     searing, seringa]
: [peris, piers, pries, prise, ripes, speir, spier, spire]
: [apers, apres, asper, pares, parse, pears, prase, presa,
      rapes, reaps, spare, spear]
: [alerts, alters, artels, estral, laster, ratels, salter,
      slater, staler, stelar, talers]
: [capers, crapes, escarp, pacers, parsec, recaps, scrape,
     secpar, spacer]
: [palest, palets, pastel, petals, plates, pleats, septal,
     staple, tepals]
: [anestri, antsier, nastier, ratines, retains, retinas,
     retsina, stainer, stearin]
: [ates, east, eats, etas, sate, seat, seta, teas]
: [carets, cartes, caster, caters, crates, reacts, recast,
     traces]
作者:Aptusource.orgAptusource.org
最好的 Java 技术博客
原文地址:[集合框架] Map 接口, 感谢原作者分享。

发表评论