第 1 部分 TreeMap 介绍
TreeMap 简介
TreeMap 是一个有序的 key-value 集合, 它是通过红黑树实现的.
TreeMap 继承于 AbstractMap, 所以它是一个 Map, 即一个 key-value 集合.
TreeMap 实现了 NavigableMap 接口, 意味着它支持一系列的导航方法. 比如返回有序的 key 集合.
TreeMap 实现了 Cloneable 接口, 意味着它能被克隆.
TreeMap 实现了 java.io.Serializable 接口, 意味着它支持序列化.
TreeMap 基于红黑树 (Red-Black tree) 实现. 该映射根据其键的自然顺序进行排序, 或者根据创建映射时提供的 Comparator 进行排序, 具体取决于使用的构造方法.
TreeMap 的基本操作 containsKey,get,put 和 remove 的时间复杂度是 log(n) .
另外, TreeMap 是非同步的. 它的 iterator 方法返回的迭代器是 fail-fastl 的.
TreeMap 的构造函数
- // 默认构造函数. 使用该构造函数, TreeMap 中的元素按照自然排序进行排列.
- TreeMap()
- // 创建的 TreeMap 包含 Map
- TreeMap(Map<? extends K, ? extends V> copyFrom)
- // 指定 Tree 的比较器
- TreeMap(Comparator<? super K> comparator)
- // 创建的 TreeSet 包含 copyFrom
- TreeMap(SortedMap<K, ? extends V> copyFrom)
TreeMap 的 API
- Entry<K, V> ceilingEntry(K key)
- K ceilingKey(K key)
- void clear()
- Object clone()
- Comparator<? super K> comparator()
- boolean containsKey(Object key)
- NavigableSet<K> descendingKeySet()
- NavigableMap<K, V> descendingMap()
- Set<Entry<K, V>> entrySet()
- Entry<K, V> firstEntry()
- K firstKey()
- Entry<K, V> floorEntry(K key)
- K floorKey(K key)
- V get(Object key)
- NavigableMap<K, V> headMap(K to, boolean inclusive)
- SortedMap<K, V> headMap(K toExclusive)
- Entry<K, V> higherEntry(K key)
- K higherKey(K key)
- boolean isEmpty()
- Set<K> keySet()
- Entry<K, V> lastEntry()
- K lastKey()
- Entry<K, V> lowerEntry(K key)
- K lowerKey(K key)
- NavigableSet<K> navigableKeySet()
- Entry<K, V> pollFirstEntry()
- Entry<K, V> pollLastEntry()
- V put(K key, V value)
- V remove(Object key)
- int size()
- SortedMap<K, V> subMap(K fromInclusive, K toExclusive)
- NavigableMap<K, V> subMap(K from, boolean fromInclusive, K to, boolean toInclusive)
- NavigableMap<K, V> tailMap(K from, boolean inclusive)
- SortedMap<K, V> tailMap(K fromInclusive)
第 2 部分 TreeMap 数据结构
TreeMap 的继承关系
- java.lang.Object
- ? java.util.AbstractMap<K, V>
- ? java.util.TreeMap<K, V>
- public class TreeMap<K,V>
- extends AbstractMap<K,V>
- implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
- }
TreeMap 与 Map 关系如下图:
从图中可以看出:
(01) TreeMap 实现继承于 AbstractMap, 并且实现了 NavigableMap 接口.
(02) TreeMap 的本质是 R-B Tree(红黑树), 它包含几个重要的成员变量: root, size, comparator.
root 是红黑数的根节点. 它是 Entry 类型, Entry 是红黑数的节点, 它包含了红黑数的 6 个基本组成成分: key(键),value(值),left(左孩子),right(右孩子),parent(父节点),color(颜色).Entry 节点根据 key 进行排序, Entry 节点包含的内容为 value.
红黑数排序时, 根据 Entry 中的 key 进行排序; Entry 中的 key 比较大小是根据比较器 comparator 来进行判断的.
size 是红黑数中节点的个数.
关于红黑数的具体算法, 请参考 "红黑树(一) 原理和算法详细介绍".
第 3 部分 TreeMap 源码解析(基于 JDK1.6.0_45)
为了更了解 TreeMap 的原理, 下面对 TreeMap 源码代码作出分析. 我们先给出源码内容, 后面再对源码进行详细说明, 当然, 源码内容中也包含了详细的代码注释. 读者阅读的时候, 建议先看后面的说明, 先建立一个整体印象; 之后再阅读源码.
View Code
说明:
在详细介绍 TreeMap 的代码之前, 我们先建立一个整体概念.
TreeMap 是通过红黑树实现的, TreeMap 存储的是 key-value 键值对, TreeMap 的排序是基于对 key 的排序.
TreeMap 提供了操作 "key","key-value","value" 等方法, 也提供了对 TreeMap 这颗树进行整体操作的方法, 如获取子树, 反向树.
后面的解说内容分为几部分,
首先, 介绍 TreeMap 的核心, 即红黑树相关部分;
然后, 介绍 TreeMap 的主要函数;
再次, 介绍 TreeMap 实现的几个接口;
最后, 补充介绍 TreeMap 的其它内容.
TreeMap 本质上是一颗红黑树. 要彻底理解 TreeMap, 建议读者先理解红黑树. 关于红黑树的原理, 可以参考: 红黑树(一) 原理和算法详细介绍
第 3.1 部分 TreeMap 的红黑树相关内容
TreeMap 中于红黑树相关的主要函数有:
1 数据结构
1.1 红黑树的节点颜色 -- 红色
private static final boolean RED = false;
1.2 红黑树的节点颜色 -- 黑色
private static final boolean BLACK = true;
1.3 "红黑树的节点" 对应的类.
static final class Entry<K,V> implements Map.Entry<K,V> { ... }
Entry 包含了 6 个部分内容: key(键),value(值),left(左孩子),right(右孩子),parent(父节点),color(颜色)
Entry 节点根据 key 进行排序, Entry 节点包含的内容为 value.
2 相关操作
2.1 左旋
private void rotateLeft(Entry<K,V> p) { ... }
2.2 右旋
private void rotateRight(Entry<K,V> p) { ... }
2.3 插入操作
public V put(K key, V value) { ... }
2.4 插入修正操作
红黑树执行插入操作之后, 要执行 "插入修正操作".
目的是: 保红黑树在进行插入节点之后, 仍然是一颗红黑树
private void fixAfterInsertion(Entry<K,V> x) { ... }
2.5 删除操作
private void deleteEntry(Entry<K,V> p) { ... }
2.6 删除修正操作
红黑树执行删除之后, 要执行 "删除修正操作".
目的是保证: 红黑树删除节点之后, 仍然是一颗红黑树
private void fixAfterDeletion(Entry<K,V> x) { ... }
关于红黑树部分, 这里主要是指出了 TreeMap 中那些是红黑树的主要相关内容. 具体的红黑树相关操作 API, 这里没有详细说明, 因为它们仅仅只是将算法翻译成代码. 读者可以参考 "红黑树(一) 原理和算法详细介绍" 进行了解.
第 3.2 部分 TreeMap 的构造函数
1 默认构造函数
使用默认构造函数构造 TreeMap 时, 使用 java 的默认的比较器比较 Key 的大小, 从而对 TreeMap 进行排序.
- public TreeMap() {
- comparator = null;
- }
2 带比较器的构造函数
- public TreeMap(Comparator<? super K> comparator) {
- this.comparator = comparator;
- }
3 带 Map 的构造函数, Map 会成为 TreeMap 的子集
- public TreeMap(Map<? extends K, ? extends V> m) {
- comparator = null;
- putAll(m);
- }
该构造函数会调用 putAll()将 m 中的所有元素添加到 TreeMap 中. putAll()源码如下:
- public void putAll(Map<? extends K, ? extends V> m) {
- for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
- put(e.getKey(), e.getValue());
- }
从中, 我们可以看出 putAll()就是将 m 中的 key-value 逐个的添加到 TreeMap 中.
4 带 SortedMap 的构造函数, SortedMap 会成为 TreeMap 的子集
- public TreeMap(SortedMap<K, ? extends V> m) {
- comparator = m.comparator();
- try {
- buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
- } catch (java.io.IOException cannotHappen) {
- } catch (ClassNotFoundException cannotHappen) {
- }
- }
该构造函数不同于上一个构造函数, 在上一个构造函数中传入的参数是 Map,Map 不是有序的, 所以要逐个添加.
而该构造函数的参数是 SortedMap 是一个有序的 Map, 我们通过 buildFromSorted()来创建对应的 Map.
buildFromSorted 涉及到的代码如下:
View Code
要理解 buildFromSorted, 重点说明以下几点:
第一, buildFromSorted 是通过递归将 SortedMap 中的元素逐个关联.
第二, buildFromSorted 返回 middle 节点 (中间节点) 作为 root.
第三, buildFromSorted 添加到红黑树中时, 只将 level == redLevel 的节点设为红色. 第 level 级节点, 实际上是 buildFromSorted 转换成红黑树后的最底端 (假设根节点在最上方) 的节点; 只将红黑树最底端的阶段着色为红色, 其余都是黑色.
第 3.3 部分 TreeMap 的 Entry 相关函数
TreeMap 的 firstEntry(), lastEntry(), lowerEntry(), higherEntry(), floorEntry(), ceilingEntry(), pollFirstEntry() , pollLastEntry() 原理都是类似的; 下面以 firstEntry()来进行详细说明
我们先看看 firstEntry()和 getFirstEntry()的代码:
- public Map.Entry<K,V> firstEntry() {
- return exportEntry(getFirstEntry());
- }
- final Entry<K,V> getFirstEntry() {
- Entry<K,V> p = root;
- if (p != null)
- while (p.left != null)
- p = p.left;
- return p;
- }
从中, 我们可以看出 firstEntry() 和 getFirstEntry() 都是用于获取第一个节点.
但是, firstEntry() 是对外接口; getFirstEntry() 是内部接口. 而且, firstEntry() 是通过 getFirstEntry() 来实现的. 那为什么外界不能直接调用 getFirstEntry(), 而需要多此一举的调用 firstEntry() 呢?
先告诉大家原因, 再进行详细说明. 这么做的目的是: 防止用户修改返回的 Entry.getFirstEntry()返回的 Entry 是可以被修改的, 但是经过 firstEntry()返回的 Entry 不能被修改, 只可以读取 Entry 的 key 值和 value 值. 下面我们看看到底是如何实现的.
(01) getFirstEntry()返回的是 Entry 节点, 而 Entry 是红黑树的节点, 它的源码如下:
- // 返回 "红黑树的第一个节点"
- final Entry<K,V> getFirstEntry() {
- Entry<K,V> p = root;
- if (p != null)
- while (p.left != null)
- p = p.left;
- return p;
- }
从中, 我们可以调用 Entry 的 getKey(),getValue()来获取 key 和 value 值, 以及调用 setValue()来修改 value 的值.
(02) firstEntry()返回的是 exportEntry(getFirstEntry()). 下面我们看看 exportEntry()干了些什么?
- static <K,V> Map.Entry<K,V> exportEntry(TreeMap.Entry<K,V> e) {
- return e == null? null :
- new AbstractMap.SimpleImmutableEntry<K,V>(e);
- }
实际上, exportEntry() 是新建一个 AbstractMap.SimpleImmutableEntry 类型的对象, 并返回.
SimpleImmutableEntry 的实现在 AbstractMap.java 中, 下面我们看看 AbstractMap.SimpleImmutableEntry 是如何实现的, 代码如下:
View Code
从中, 我们可以看出 SimpleImmutableEntry 实际上是简化的 key-value 节点.
它只提供了 getKey(),getValue()方法类获取节点的值; 但不能修改 value 的值, 因为调用 setValue() 会抛出异常 UnsupportedOperationException();
再回到我们之前的问题: 那为什么外界不能直接调用 getFirstEntry(), 而需要多此一举的调用 firstEntry() 呢?
现在我们清晰的了解到:
(01) firstEntry()是对外接口, 而 getFirstEntry()是内部接口.
(02) 对 firstEntry()返回的 Entry 对象只能进行 getKey(),getValue()等读取操作; 而对 getFirstEntry()返回的对象除了可以进行读取操作之后, 还可以通过 setValue()修改值.
第 3.4 部分 TreeMap 的 key 相关函数
TreeMap 的 firstKey(),lastKey(),lowerKey(),higherKey(),floorKey(),ceilingKey()原理都是类似的; 下面以 ceilingKey()来进行详细说明
ceilingKey(K key)的作用是 "返回大于 / 等于 key 的最小的键值对所对应的 KEY, 没有的话返回 null", 它的代码如下:
- public K ceilingKey(K key) {
- return keyOrNull(getCeilingEntry(key));
- }
ceilingKey()是通过 getCeilingEntry()实现的. keyOrNull()的代码很简单, 它是获取节点的 key, 没有的话, 返回 null.
- static <K,V> K keyOrNull(TreeMap.Entry<K,V> e) {
- return e == null? null : e.key;
- }
getCeilingEntry(K key)的作用是 "获取 TreeMap 中大于 / 等于 key 的最小的节点, 若不存在(即 TreeMap 中所有节点的键都比 key 大), 就返回 null". 它的实现代码如下:
View Code
第 3.5 部分 TreeMap 的 values()函数
values() 返回 "TreeMap 中值的集合"
values()的实现代码如下:
- public Collection<V> values() {
- Collection<V> vs = values;
- return (vs != null) ? vs : (values = new Values());
- }
说明: 从中, 我们可以发现 values()是通过 new Values() 来实现 "返回 TreeMap 中值的集合".
那么 Values()是如何实现的呢? 没错! 由于返回的是值的集合, 那么 Values()肯定返回一个集合; 而 Values()正好是集合类 Value 的构造函数. Values 继承于 AbstractCollection, 它的代码如下:
View Code
说明: 从中, 我们可以知道 Values 类就是一个集合. 而 AbstractCollection 实现了除 size() 和 iterator() 之外的其它函数, 因此只需要在 Values 类中实现这两个函数即可.
size() 的实现非常简单, Values 集合中元素的个数 = 该 TreeMap 的元素个数.(TreeMap 每一个元素都有一个值嘛!)
iterator() 则返回一个迭代器, 用于遍历 Values. 下面, 我们一起可以看看 iterator()的实现:
- public Iterator<V> iterator() {
- return new ValueIterator(getFirstEntry());
- }
说明: iterator() 是通过 ValueIterator() 返回迭代器的, ValueIterator 是一个类. 代码如下:
- final class ValueIterator extends PrivateEntryIterator<V> {
- ValueIterator(Entry<K,V> first) {
- super(first);
- }
- public V next() {
- return nextEntry().value;
- }
- }
说明: ValueIterator 的代码很简单, 它的主要实现应该在它的父类 PrivateEntryIterator 中. 下面我们一起看看 PrivateEntryIterator 的代码:
View Code
说明: PrivateEntryIterator 是一个抽象类, 它的实现很简单, 只只实现了 Iterator 的 remove()和 hasNext()接口, 没有实现 next()接口.
而我们在 ValueIterator 中已经实现的 next()接口.
至此, 我们就了解了 iterator()的完整实现了.
第 3.6 部分 TreeMap 的 entrySet()函数
entrySet() 返回 "键值对集合". 顾名思义, 它返回的是一个集合, 集合的元素是 "键值对".
下面, 我们看看它是如何实现的? entrySet() 的实现代码如下:
- public Set<Map.Entry<K,V>> entrySet() {
- EntrySet es = entrySet;
- return (es != null) ? es : (entrySet = new EntrySet());
- }
说明: entrySet()返回的是一个 EntrySet 对象.
下面我们看看 EntrySet 的代码:
View Code
说明:
EntrySet 是 "TreeMap 的所有键值对组成的集合", 而且它单位是单个 "键值对".
EntrySet 是一个集合, 它继承于 AbstractSet. 而 AbstractSet 实现了除 size() 和 iterator() 之外的其它函数, 因此, 我们重点了解一下 EntrySet 的 size() 和 iterator() 函数
size() 的实现非常简单, AbstractSet 集合中元素的个数 = 该 TreeMap 的元素个数.
iterator() 则返回一个迭代器, 用于遍历 AbstractSet. 从上面的源码中, 我们可以发现 iterator() 是通过 EntryIterator 实现的; 下面我们看看 EntryIterator 的源码:
- final class EntryIterator extends PrivateEntryIterator<Map.Entry<K,V>> {
- EntryIterator(Entry<K,V> first) {
- super(first);
- }
- public Map.Entry<K,V> next() {
- return nextEntry();
- }
- }
说明: 和 Values 类一样, EntryIterator 也继承于 PrivateEntryIterator 类.
第 3.7 部分 TreeMap 实现的 Cloneable 接口
TreeMap 实现了 Cloneable 接口, 即实现了 clone()方法.
clone()方法的作用很简单, 就是克隆一个 TreeMap 对象并返回.
View Code
第 3.8 部分 TreeMap 实现的 Serializable 接口
TreeMap 实现 java.io.Serializable, 分别实现了串行读取, 写入功能.
串行写入函数是 writeObject(), 它的作用是将 TreeMap 的 "容量, 所有的 Entry" 都写入到输出流中.
而串行读取函数是 readObject(), 它的作用是将 TreeMap 的 "容量, 所有的 Entry" 依次读出.
readObject() 和 writeObject() 正好是一对, 通过它们, 我能实现 TreeMap 的串行传输.
View Code
说到这里, 就顺便说一下 "关键字 transient" 的作用
transient 是 Java 语言的关键字, 它被用来表示一个域不是该对象串行化的一部分.
Java 的 serialization 提供了一种持久化对象实例的机制. 当持久化对象时, 可能有一个特殊的对象数据成员, 我们不想用 serialization 机制来保存它. 为了在一个特定对象的一个域上关闭 serialization, 可以在这个域前加上关键字 transient.
当一个对象被串行化的时候, transient 型变量的值不包括在串行化的表示中, 然而非 transient 型的变量是被包括进去的.
第 3.9 部分 TreeMap 实现的 NavigableMap 接口
- firstKey(),lastKey(),lowerKey(),higherKey(),ceilingKey(),floorKey();
- firstEntry(), lastEntry(), lowerEntry(), higherEntry(), floorEntry(), ceilingEntry(), pollFirstEntry() , pollLastEntry();
上面已经讲解过这些 API 了, 下面对其它的 API 进行说明.
1 反向 TreeMap
descendingMap() 的作用是返回当前 TreeMap 的反向的 TreeMap. 所谓反向, 就是排序顺序和原始的顺序相反.
我们已经知道 TreeMap 是一颗红黑树, 而红黑树是有序的.
TreeMap 的排序方式是通过比较器, 在创建 TreeMap 的时候, 若指定了比较器, 则使用该比较器; 否则, 就使用 Java 的默认比较器.
而获取 TreeMap 的反向 TreeMap 的原理就是将比较器反向即可!
理解了 descendingMap()的反向原理之后, 再讲解一下 descendingMap()的代码.
- // 获取 TreeMap 的降序 Map
- public NavigableMap<K, V> descendingMap() {
- NavigableMap<K, V> km = descendingMap;
- return (km != null) ? km :
- (descendingMap = new DescendingSubMap(this,
- true, null, true,
- true, null, true));
- }
从中, 我们看出 descendingMap()实际上是返回 DescendingSubMap 类的对象. 下面, 看看 DescendingSubMap 的源码:
View Code
从中, 我们看出 DescendingSubMap 是降序的 SubMap, 它的实现机制是将 "SubMap 的比较器反转".
它继承于 NavigableSubMap. 而 NavigableSubMap 是一个继承于 AbstractMap 的抽象类; 它包括 2 个子类 --"(升序)AscendingSubMap" 和 "(降序)DescendingSubMap".NavigableSubMap 为它的两个子类实现了许多公共 API.
下面看看 NavigableSubMap 的源码.
View Code
NavigableSubMap 源码很多, 但不难理解; 读者可以通过源码和注释进行理解.
其实, 读完 NavigableSubMap 的源码后, 我们可以得出它的核心思想是: 它是一个抽象集合类, 为 2 个子类 --"(升序)AscendingSubMap" 和 "(降序)DescendingSubMap" 而服务; 因为 NavigableSubMap 实现了许多公共 API. 它的最终目的是实现下面的一系列函数:
- headMap(K toKey, boolean inclusive)
- headMap(K toKey)
- subMap(K fromKey, K toKey)
- subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive)
- tailMap(K fromKey)
- tailMap(K fromKey, boolean inclusive)
- navigableKeySet()
- descendingKeySet()
第 3.10 部分 TreeMap 其它函数
1 顺序遍历和逆序遍历
TreeMap 的顺序遍历和逆序遍历原理非常简单.
由于 TreeMap 中的元素是从小到大的顺序排列的. 因此, 顺序遍历, 就是从第一个元素开始, 逐个向后遍历; 而倒序遍历则恰恰相反, 它是从最后一个元素开始, 逐个往前遍历.
我们可以通过 keyIterator() 和 descendingKeyIterator()来说明!
keyIterator()的作用是返回顺序的 KEY 的集合,
descendingKeyIterator()的作用是返回逆序的 KEY 的集合.
keyIterator() 的代码如下:
- Iterator<K> keyIterator() {
- return new KeyIterator(getFirstEntry());
- }
说明: 从中我们可以看出 keyIterator() 是返回以 "第一个节点(getFirstEntry)" 为其实元素的迭代器.
KeyIterator 的代码如下:
- final class KeyIterator extends PrivateEntryIterator<K> {
- KeyIterator(Entry<K,V> first) {
- super(first);
- }
- public K next() {
- return nextEntry().key;
- }
- }
说明: KeyIterator 继承于 PrivateEntryIterator. 当我们通过 next()不断获取下一个元素的时候, 就是执行的顺序遍历了.
descendingKeyIterator()的代码如下:
- Iterator<K> descendingKeyIterator() {
- return new DescendingKeyIterator(getLastEntry());
- }
说明: 从中我们可以看出 descendingKeyIterator() 是返回以 "最后一个节点(getLastEntry)" 为其实元素的迭代器.
再看看 DescendingKeyIterator 的代码:
- final class DescendingKeyIterator extends PrivateEntryIterator<K> {
- DescendingKeyIterator(Entry<K,V> first) {
- super(first);
- }
- public K next() {
- return prevEntry().key;
- }
- }
说明: DescendingKeyIterator 继承于 PrivateEntryIterator. 当我们通过 next()不断获取下一个元素的时候, 实际上调用的是 prevEntry()获取的上一个节点, 这样它实际上执行的是逆序遍历了.
至此, TreeMap 的相关内容就全部介绍完毕了. 若有错误或纰漏的地方, 欢迎指正!
第 4 部分 TreeMap 遍历方式
4.1 遍历 TreeMap 的键值对
第一步: 根据 entrySet()获取 TreeMap 的 "键值对" 的 Set 集合.
第二步: 通过 Iterator 迭代器遍历 "第一步" 得到的集合.
- // 假设 map 是 TreeMap 对象
- // map 中的 key 是 String 类型, value 是 Integer 类型
- Integer integ = null;
- Iterator iter = map.entrySet().iterator();
- while(iter.hasNext()) {
- Map.Entry entry = (Map.Entry)iter.next();
- // 获取 key
- key = (String)entry.getKey();
- // 获取 value
- integ = (Integer)entry.getValue();
- }
4.2 遍历 TreeMap 的键
第一步: 根据 keySet()获取 TreeMap 的 "键" 的 Set 集合.
第二步: 通过 Iterator 迭代器遍历 "第一步" 得到的集合.
- // 假设 map 是 TreeMap 对象
- // map 中的 key 是 String 类型, value 是 Integer 类型
- String key = null;
- Integer integ = null;
- Iterator iter = map.keySet().iterator();
- while (iter.hasNext()) {
- // 获取 key
- key = (String)iter.next();
- // 根据 key, 获取 value
- integ = (Integer)map.get(key);
- }
4.3 遍历 TreeMap 的值
第一步: 根据 value()获取 TreeMap 的 "值" 的集合.
第二步: 通过 Iterator 迭代器遍历 "第一步" 得到的集合.
- // 假设 map 是 TreeMap 对象
- // map 中的 key 是 String 类型, value 是 Integer 类型
- Integer value = null;
- Collection c = map.values();
- Iterator iter= c.iterator();
- while (iter.hasNext()) {
- value = (Integer)iter.next();
- }
TreeMap 遍历测试程序如下:
View Code
第 5 部分 TreeMap 示例
下面通过实例来学习如何使用 TreeMap
View Code
运行结果:
- {
- one=8, three=4, two=2
- }
- next : one - 8
- next : three - 4
- next : two - 2
- size: 3
- contains key two : true
- contains key five : false
- contains value 0 : false
- tmap:{
- one=8, two=2
- }
- tmap is empty
来源: http://www.bubuko.com/infodetail-3056139.html