9.md 6.8 KB
Newer Older
W
ch9  
wizardforcel 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
# 第九章 `Map`接口

> 原文:[Chapter 9  The Map interface](http://greenteapress.com/thinkdast/html/thinkdast010.html)

> 译者:[飞龙](https://github.com/wizardforcel)

> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)

> 自豪地采用[谷歌翻译](https://translate.google.cn/)

在接下来的几个练习中,我介绍了`Map`接口的几个实现。其中一个基于哈希表,这可以说是所发明的最神奇的数据结构。另一个是类似的`TreeMap`,不是很神奇,但它有附加功能,它可以按顺序迭代元素。

你将有机会实现这些数据结构,然后我们将分析其性能。

但是在我们可以解释哈希表之前,我们将从一个`Map`开始,它使用键值对的`List`来简单实现。

## 9.1 实现`MyLinearMap`

像往常一样,我提供启动代码,你将填写缺少的方法。这是`MyLinearMap`类定义的起始:

```java
public class MyLinearMap<K, V> implements Map<K, V> {

    private List<Entry> entries = new ArrayList<Entry>();
```

该类使用两个类型参数,`K`是键的类型,`V`是值的类型。`MyLinearMap`实现`Map`,这意味着它必须提供`Map`接口中的方法。

`MyLinearMap`对象具有单个实例变量,`entries`,这是一个`Entry``ArrayList`对象。每个`Entry`都包含一个键值对。这里是定义:

```java
    public class Entry implements Map.Entry<K, V> {
        private K key;
        private V value;
        
        public Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }
        
        @Override
        public K getKey() {
            return key;
        }
        @Override
        public V getValue() {
            return value;
        }
    }
```

`Entry`没有什么,只是一个键和一个值的容器。该定义内嵌在`MyLinearList`中,因此它使用相同类型的参数,`K``V`

这就是你做这个练习所需的所有东西,所以让我们开始吧。

## 9.2 练习 7

在本书的仓库中,你将找到此练习的源文件:

+   `MyLinearMap.java`包含练习的第一部分的起始代码。
+   `MyLinearMapTest.java`包含`MyLinearMap`的单元测试。

你还会找到 Ant 构建文件`build.xml`

运行`ant build`来编译源文件。然后运行`ant   MyLinearMapTest`;几个测试应该失败,因为你有一些任务要做。

首先,填写`findEntry`的主体。这是一个辅助方法,不是`Map`接口的一部分,但是一旦你让它工作,你可以在几种方法中使用它。给定一个目标键(Key),它应该搜索条目(Entry)并返回包含目标的条目(按照键,而不是值),或者如果不存在则返回`null`。请注意,我提供了`equals`,正确比较两个键并处理`null`


你可以再次运行`ant MyLinearMapTest`,但即使你的`findEntry`是正确的,测试也不会通过,因为`put`不完整。


填充`put`。你应该阅读`Map.put`的文档,<http://thinkdast.com/listput> ,以便你知道应该做什么。你可能希望从一个版本开始,其中`put`始终添加新条目,并且不会修改现有条目;这样你可以先测试简单的情况。或者如果你更加自信,你可以一次写出整个东西。

一旦你`put`正常工作,测试`containsKey`应该通过。

阅读`Map.get`的文档,<http://thinkdast.com/listget> ,然后填充方法。再次运行测试。

最后,阅读`Map.remove`的文档,<http://thinkdast.com/maprem> 并填充方法。

到了这里,所有的测试都应该通过。恭喜!

## 9.3 分析`MyLinearMap`

这一节中,我展示了上一个练习的答案,并分析核心方法的性能。这里是`findEntry``equals`

```java
private Entry findEntry(Object target) {
    for (Entry entry: entries) {
        if (equals(target, entry.getKey())) {
            return entry;
        }
    }
    return null;
}

private boolean equals(Object target, Object obj) {
    if (target == null) {
        return obj == null;
    }
    return target.equals(obj);
}
```

`equals`的运行时间可能取决于`target`键和键的大小 ,但通常不取决于条目的数量,`n`。那么`equals`是常数时间。

`findEntry`中,我们可能会很幸运,并在一开始就找到我们要找的键,但是我们不能指望它。一般来说,我们要搜索的条目数量与`n`成正比,所以`findEntry`是线性的。


大部分的`MyLinearMap`核心方法使用`findEntry`,包括`put``get`,和`remove`。这就是他们的样子:

```java
public V put(K key, V value) {
    Entry entry = findEntry(key);
    if (entry == null) {
        entries.add(new Entry(key, value));
        return null;
    } else {
        V oldValue = entry.getValue();
        entry.setValue(value);
        return oldValue;
    }
}
public V get(Object key) {
    Entry entry = findEntry(key);
    if (entry == null) {
        return null;
    }
    return entry.getValue();
}
public V remove(Object key) {
    Entry entry = findEntry(key);
    if (entry == null) {
        return null;
    } else {
        V value = entry.getValue();
        entries.remove(entry);
        return value;
    }
}
```

`put`调用`findEntry`之后,其他一切都是常数时间。记住这个`entries`是一个`ArrayList`,所以降魔为添加元素平均是常数时间。如果键已经在映射中,我们不需要添加条目,但我们必须调用`entry.getValue``entry.setValue`,而这些都是常数时间。把它们放在一起,`put`是线性的。

同样,`get`也是线性的。

`remove`稍微复杂一些,因为`entries.remove`可能需要从一开始或中间删除`ArrayList`的一个元素,并且需要线性时间。但是没关系:两个线性运算仍然是线性的。


总而言之,核心方法都是线性的,这就是为什么我们将这个实现称为`MyLinearMap`(嗒嗒!)。

如果我们知道输入的数量很少,这个实现可能会很好,但是我们可以做得更好。实际上,`Map`所有的核心方法都是常数时间的实现。当你第一次听到这个消息时,可能似乎觉得不可能。实际上我们所说的是,你可以在常数时间内大海捞针,不管海有多大。这是魔法。

我们不是将条目存储在一个大的`List`中,而是把它们分解成许多短的列表。对于每个键,我们将使用哈希码(在下一节中进行说明)来确定要使用的列表。
使用大量的简短列表比仅仅使用一个更快,但正如我将解释的,它不会改变增长级别;核心功能仍然是线性的。但还有一个技巧:如果我们增加列表的数量来限制每个列表的条目数,就会得到一个恒定时间的映射。你会在下一个练习中看到细节,但是首先要了解哈希!

在下一章中,我将介绍一种解决方案,分析`Map`核心方法的性能,并引入更有效的实现。