资讯专栏INFORMATION COLUMN

Java™ 教程(Map接口)

Kahn / 2738人阅读

Map接口

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

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

本页的其余部分详细讨论了Map接口,但首先,这里有一些使用JDK 8聚合操作收集到Map的示例,对现实世界对象进行建模是面向对象编程中的常见任务,因此可以合理地认为某些程序可能会按部门对员工进行分组:

// Group employees by department
Map> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));

或者按部门计算所有工资的总和:

// Compute sum of salaries by department
Map totalByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.summingInt(Employee::getSalary)));

或者通过成绩及格或成绩不及格分组学生:

// Partition students into passing and failing
Map> passingFailing = students.stream()
.collect(Collectors.partitioningBy(s -> s.getGrade()>= PASS_THRESHOLD));

你还可以按城市分组:

// Classify Person objects by city
Map> peopleByCity
         = personStream.collect(Collectors.groupingBy(Person::getCity));

或者甚至级联两个收集器按州和城市对人进行分类:

// Cascade Collectors 
Map>> 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 m = new HashMap();

        // 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 copy = new HashMap(m);
Map接口批量操作

clear的操作完全符合你的想法:它从Map中删除所有映射。putAll操作是Collection接口的addAll操作的Map模拟,除了明显使用将一个Map转储到另一个Map之外,它还有第二个更微妙的用途,假设Map用于表示属性—值对的集合,putAll操作与Map转换构造函数结合使用,提供了一种使用默认值实现属性映射创建的简洁方法。以下是演示此技术的静态工厂方法。

static  Map newAttributeMap(Mapdefaults, Map overrides) {
    Map result = new HashMap(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 it = m.keySet().iterator(); it.hasNext(); )
    if (it.next().isBogus())
        it.remove();

迭代值的语法是类似的,以下是迭代键值对的语法。

for (Map.Entry 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  boolean validate(Map attrMap, Set requiredAttrs, SetpermittedAttrs) {
    boolean valid = true;
    Set attrs = attrMap.keySet();

    if (! attrs.containsAll(requiredAttrs)) {
        Set missing = new HashSet(requiredAttrs);
        missing.removeAll(attrs);
        System.out.println("Missing attributes: " + missing);
        valid = false;
    }
    if (! permittedAttrs.containsAll(attrs)) {
        Set illegal = new HashSet(attrs);
        illegal.removeAll(permittedAttrs);
        System.out.println("Illegal attributes: " + illegal);
        valid = false;
    }
    return valid;
}

假设你想知道两个Map对象共有的所有键。

SetcommonKeys = new HashSet(m1.keySet());
commonKeys.retainAll(m2.keySet());

类似的语法可以为你提供共同的值。

到目前为止提出的所有语法都是非破坏性的,也就是说,它们不会修改支持Map,这里有一些,假设你要删除一个Map与另一个Map共有的所有键值对。

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

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

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

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

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

假设你要解雇所有直接向某位经理Simon报告的员工。

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

请注意,这个语法是使用Collections.singleton,这是一个静态工厂方法,它返回一个带有指定元素的不可变Set

一旦你完成了这项工作,你可能会有一群员工,他们的经理不再为公司工作(如果任何Simon的直接报告本身就是经理),以下代码将告诉你哪些员工拥有不再为公司工作的经理。

Map m = new HashMap(managers);
m.values().removeAll(managers.keySet());
Set 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> m = new HashMap>();

        try {
            Scanner s = new Scanner(new File(args[0]));
            while (s.hasNext()) {
                String word = s.next();
                String alpha = alphabetize(word);
                List l = m.get(alpha);
                if (l == null)
                    m.put(alpha, l=new ArrayList());
                l.add(word);
            }
        } catch (IOException e) {
            System.err.println(e);
            System.exit(1);
        }

        // Print all permutation groups above size threshold
        for (List 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基准参考词列表。

上一篇:Deque接口 下一篇:对象排序

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73946.html

相关文章

  • Java教程(集合接口

    集合接口 核心集合接口封装了不同类型的集合,如下图所示,这些接口允许独立于其表示的细节来操纵集合,核心集合接口是Java集合框架的基础,如下图所示,核心集合接口形成层次结构。 showImg(https://segmentfault.com/img/bVbntJW?w=402&h=146); Set是一种特殊的Collection,SortedSet是一种特殊的Set,依此类推,另请注意,层次结构...

    elisa.yang 评论0 收藏0
  • Java 8 并发教程:原子变量和 ConcurrentMa

    摘要:并发教程原子变量和原文译者飞龙协议欢迎阅读我的多线程编程系列教程的第三部分。如果你能够在多线程中同时且安全地执行某个操作,而不需要关键字或上一章中的锁,那么这个操作就是原子的。当多线程的更新比读取更频繁时,这个类通常比原子数值类性能更好。 Java 8 并发教程:原子变量和 ConcurrentMap 原文:Java 8 Concurrency Tutorial: Synchroni...

    bitkylin 评论0 收藏0
  • Java 8 简明教程

    摘要:简明教程原文译者黄小非来源简明教程并没有没落,人们很快就会发现这一点欢迎阅读我编写的介绍。编译器会自动地选择合适的构造函数来匹配函数的签名,并选择正确的构造函数形式。 Java 8 简明教程 原文:Java 8 Tutorial 译者:ImportNew.com - 黄小非 来源:Java 8简明教程 ‍ Java并没有没落,人们很快就会发现这一点 欢迎阅读我编写的Java ...

    testHs 评论0 收藏0
  • Java教程(SortedMap接口

    SortedMap接口 SortedMap是一个按升序维护其条目的Map,根据键的自然顺序或在创建SortedMap时提供的Comparator进行排序,SortedMap接口提供常规Map操作和以下操作的操作: 范围视图 — 对排序后的map执行任意范围操作 端点 — 返回已排序map中的第一个或最后一个键 比较器访问 — 返回用于排序map的Comparator(如果有的话) 下面的接口是...

    JessYanCoding 评论0 收藏0
  • Java教程(抽象方法和类)

    抽象方法和类 抽象类是一个声明为abstract的类 — 它可能包括也可能不包括抽象方法,抽象类无法实例化,但可以进行子类化。 抽象方法是在没有实现的情况下声明的方法(没有大括号,后跟分号),如下所示: abstract void moveTo(double deltaX, double deltaY); 如果一个类包含抽象方法,那么该类本身必须被声明为abstract,如: public abs...

    Amio 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<