java 集合系列目录:
Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例
Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例
Java 集合系列 05 Vector详细介绍(源码解析)和使用示例
Java 集合系列 06 Stack详细介绍(源码解析)和使用示例
Java 集合系列 07 List总结(LinkedList, ArrayList等使用场景和性能分析)
Java 集合系列 09 HashMap详细介绍(源码解析)和使用示例
Java 集合系列 10 Hashtable详细介绍(源码解析)和使用示例
Java 集合系列 11 hashmap 和 hashtable 的区别
前一章,我们学习了HashMap。这一章,我们对Hashtable进行学习。
我们先对Hashtable有个整体认识,然后再学习它的源码,
第1部分 Hashtable介绍
第2部分 Hashtable数据结构
第3部分 Hashtable主要方法
3.5 contains() 和 containsValue()
第5部分 Hashtable实现的Serializable接口
第1部分 Hashtable介绍
Hashtable 简介
和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。
此类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null
对象都可以用作键或值。
为了成功地在哈希表中存储和获取对象,用作键的对象必须实现 hashCode
方法和 equals
方法。
Hashtable 的实例有两个参数影响其性能:初始容量 和 加载因子。容量 是哈希表中桶 的数量,初始容量 就是哈希表创建时的容量。注意,哈希表的状态为 open:在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。加载因子 是对哈希表在其容量自动增加之前可以达到多满的一个尺度。初始容量和加载因子这两个参数只是对该实现的提示。关于何时以及是否调用 rehash 方法的具体细节则依赖于该实现。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间(在大多数 Hashtable 操作中,包括 get 和 put 操作,都反映了这一点)。
Hashtable的构造函数
1.默认构造函数,容量为11,加载因子为0.75。
public Hashtable() { this(11, 0.75f); }
2.用指定初始容量和默认的加载因子 (0.75) 构造一个新的空哈希表。
public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); }
3.用指定初始容量和指定加载因子构造一个新的空哈希表。
public Hashtable(int initialCapacity, float loadFactor) { //验证初始容量 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); //验证加载因子 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load: "+loadFactor); if (initialCapacity==0) initialCapacity = 1; this.loadFactor = loadFactor; //初始化table,获得大小为initialCapacity的table数组 table = new Entry[initialCapacity]; //计算阀值 threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); //初始化HashSeed值 initHashSeedAsNeeded(initialCapacity); }
其中initHashSeedAsNeeded方法用于初始化hashSeed参数,其中hashSeed用于计算key的hash值,它与key的hashCode进行按位异或运算。这个hashSeed是一个与实例相关的随机值,主要用于解决hash冲突。
private int hash(Object k) { // hashSeed will be zero if alternative hashing is disabled. return hashSeed ^ k.hashCode(); }
4.构造一个与给定的 Map 具有相同映射关系的新哈希表。
public Hashtable(Map<? extends K, ? extends V> t) { //设置table容器大小,其值==t.size * 2 + 1 this(Math.max(2*t.size(), 11), 0.75f); putAll(t); }
Hashtable的API
void clear() 将此哈希表清空,使其不包含任何键。 Object clone() 创建此哈希表的浅表副本。 boolean contains(Object value) 测试此映射表中是否存在与指定值关联的键。 boolean containsKey(Object key) 测试指定对象是否为此哈希表中的键。 boolean containsValue(Object value) 如果此 Hashtable 将一个或多个键映射到此值,则返回 true。 Enumeration<V> elements() 返回此哈希表中的值的枚举。 Set<Map.Entry<K,V>> entrySet() 返回此映射中包含的键的 Set 视图。 boolean equals(Object o) 按照 Map 接口的定义,比较指定 Object 与此 Map 是否相等。 V get(Object key) 返回指定键所映射到的值,如果此映射不包含此键的映射,则返回 null. 更确切地讲,如果此映射包含满足 (key.equals(k)) 的从键 k 到值 v 的映射,则此方法返回 v;否则,返回 null。 int hashCode() 按照 Map 接口的定义,返回此 Map 的哈希码值。 boolean isEmpty() 测试此哈希表是否没有键映射到值。 Enumeration<K> keys() 返回此哈希表中的键的枚举。 Set<K> keySet() 返回此映射中包含的键的 Set 视图。 V put(K key, V value) 将指定 key 映射到此哈希表中的指定 value。 void putAll(Map<? extends K,? extends V> t) 将指定映射的所有映射关系复制到此哈希表中,这些映射关系将替换此哈希表拥有的、针对当前指定映射中所有键的所有映射关系。 protected void rehash() 增加此哈希表的容量并在内部对其进行重组,以便更有效地容纳和访问其元素。 V remove(Object key) 从哈希表中移除该键及其相应的值。 int size() 返回此哈希表中的键的数量。 String toString() 返回此 Hashtable 对象的字符串表示形式,其形式为 ASCII 字符 ", " (逗号加空格)分隔开的、括在括号中的一组条目。 Collection<V> values() 返回此映射中包含的键的 Collection 视图。
第2部分 Hashtable数据结构
Hashtable的继承关系
java.lang.Object ↳ java.util.Dictionary<K, V> ↳ java.util.Hashtable<K, V> public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { }
Hashtable与Map关系如下图:
从图中可以看出:
(01) Hashtable继承于Dictionary类,实现了Map接口。Map是"key-value键值对"接口,Dictionary是声明了操作"键值对"函数接口的抽象类。
(02) Hashtable是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, count, threshold, loadFactor, modCount。
table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
count是Hashtable的大小,它是Hashtable保存的键值对的数量。
threshold是Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。
loadFactor就是加载因子。
modCount是用来实现fail-fast机制的
第3部分 Hashtable主要方法
HashTable的API对外提供了许多方法,这些方法能够很好帮助我们操作HashTable,
首先我们先看put方法:将指定 key
映射到此哈希表中的指定 value
。注意这里键key和值value都不可为空。
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } /* * 确保key在table[]是不重复的 * 处理过程: * 1、计算key的hash值,确认在table[]中的索引位置 * 2、迭代index索引位置,如果该位置处的链表中存在一个一样的key,则替换其value,返回旧值 */ Entry tab[] = table; //计算key的hash值 int hash = hash(key); //确认该key的索引位置 int index = (hash & 0x7FFFFFFF) % tab.length; //迭代,寻找该key,替换 for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { V old = e.value; e.value = value; return old; } } modCount++; //如果容器中的元素数量已经达到阀值,则进行扩容操作 if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); tab = table; hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. Entry<K,V> e = tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; return null; }
put方法的整个处理流程是:计算key的hash值,根据hash值获得key在table数组中的索引位置,然后迭代该key处的Entry链表(我们暂且理解为链表),若该链表中存在一个这个的key对象,那么就直接替换其value值即可,否则在将改key-value节点插入该index索引位置处。如下:
首先我们假设一个容量为5的table,存在8、10、13、16、17、21。他们在table中位置如下:
然后我们插入一个数:put(16,22),key=16在table的索引位置为1,同时在1索引位置有两个数,程序对该“链表”进行迭代,发现存在一个key=16,这时要做的工作就是用newValue=22替换oldValue16,并将oldValue=16返回。
在put(31,31),key=31所在的索引位置为3,并且在该链表中也没有存在某个key=31的节点,所以就将该节点插入该链表的第一个位置。
在HashTabled的put方法中有两个地方需要注意:
1、HashTable的扩容操作,在put方法中,如果需要向table[]中添加Entry元素,会首先进行容量校验,如果容量已经达到了阀值,HashTable就会进行扩容处理rehash(),如下:
1 protected void rehash() { 2 int oldCapacity = table.length; 3 Entry<K,V>[] oldMap = table; 4 5 // overflow-conscious code 6 int newCapacity = (oldCapacity << 1) + 1; 7 if (newCapacity - MAX_ARRAY_SIZE > 0) { 8 if (oldCapacity == MAX_ARRAY_SIZE) 9 // Keep running with MAX_ARRAY_SIZE buckets 10 return; 11 newCapacity = MAX_ARRAY_SIZE; 12 } 13 //新建一个size = newCapacity 的HashTable 14 Entry<K,V>[] newMap = new Entry[newCapacity]; 15 16 modCount++; 17 //重新计算阀值 18 threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); 19 //重新计算hashSeed 20 boolean rehash = initHashSeedAsNeeded(newCapacity); 21 22 table = newMap; 23 //将原来的元素拷贝到新的HashTable中 24 for (int i = oldCapacity ; i-- > 0 ;) { 25 for (Entry<K,V> old = oldMap[i] ; old != null ; ) { 26 Entry<K,V> e = old; 27 old = old.next; 28 29 if (rehash) { 30 e.hash = hash(e.key); 31 } 32 int index = (e.hash & 0x7FFFFFFF) % newCapacity; 33 e.next = newMap[index]; 34 newMap[index] = e; 35 } 36 } 37 }
在这个rehash()方法中我们可以看到容量扩大两倍+1,同时需要将原来HashTable中的元素一一复制到新的HashTable中,这个过程是比较消耗时间的,同时还需要重新计算hashSeed的,毕竟容量已经变了。这里对阀值啰嗦一下:比如初始值11、加载因子默认0.75,那么这个时候阀值threshold=8,当容器中的元素达到8时,HashTable进行一次扩容操作,容量 = 8 * 2 + 1 =17,而阀值threshold=17*0.75 = 13,当容器元素再一次达到阀值时,HashTable还会进行扩容操作,以此类推。
2、其实这里是我的一个疑问,在计算索引位置index时,HashTable进行了一个与运算过程(hash & 0x7FFFFFFF),至于为什么要与 0x7FFFFFFF, 那是hashtable 提供的hash算法, hashMap提供了不同的算法, 用户如果要定义自己的算法也是可以的.
下面是计算key的hash值,这里hashSeed发挥了作用。
private int hash(Object k) { return hashSeed ^ k.hashCode(); }
相对于put方法,get方法就会比较简单,处理过程就是计算key的hash值,判断在table数组中的索引位置,然后迭代链表,匹配直到找到相对应key的value,若没有找到返回null。
1 public synchronized V get(Object key) { 2 Entry tab[] = table; 3 int hash = hash(key); 4 int index = (hash & 0x7FFFFFFF) % tab.length; 5 for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { 6 if ((e.hash == hash) && e.key.equals(key)) { 7 return e.value; 8 } 9 } 10 return null; 11 }
putAll() 的作用是将“Map(t)”的中全部元素逐一添加到Hashtable中
public synchronized void putAll(Map<? extends K, ? extends V> t) { for (Map.Entry<? extends K, ? extends V> e : t.entrySet()) put(e.getKey(), e.getValue()); }
clear() 的作用是清空Hashtable。它是将Hashtable的table数组的值全部设为null
public synchronized void clear() { Entry tab[] = table; modCount++; for (int index = tab.length; --index >= 0; ) tab[index] = null; count = 0; }
3.5 contains() 和 containsValue()
contains() 和 containsValue() 的作用都是判断Hashtable是否包含“值(value)”
public synchronized boolean contains(Object value) { if (value == null) { throw new NullPointerException(); } Entry tab[] = table; for (int i = tab.length ; i-- > 0 ;) { for (Entry<K,V> e = tab[i] ; e != null ; e = e.next) { if (e.value.equals(value)) { return true; } } } return false; }
public boolean containsValue(Object value) { return contains(value); }
containsKey() 的作用是判断Hashtable是否包含key
1 public synchronized boolean containsKey(Object key) { 2 Entry tab[] = table; 3 int hash = hash(key); 4 // 计算索引值,% tab.length 的目的是防止数据越界 5 int index = (hash & 0x7FFFFFFF) % tab.length; 6 // 找到“key对应的Entry(链表)”,然后在链表中找出“哈希值”和“键值”与key都相等的元素 7 for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { 8 if ((e.hash == hash) && e.key.equals(key)) { 9 return true; 10 } 11 } 12 return false; 13 }
elements() 的作用是返回“所有value”的枚举对象
1 public synchronized Enumeration<V> elements() { 2 return this.<V>getEnumeration(VALUES); 3 } 4 5 // 获取Hashtable的枚举类对象 6 private <T> Enumeration<T> getEnumeration(int type) { 7 if (count == 0) { 8 return Collections.emptyEnumeration(); 9 } else { 10 return new Enumerator<>(type, false); 11 } 12 }
从中,我们可以看出:
(01) 若Hashtable的实际大小为0,则返回“空枚举类”对象emptyEnumerator;
(02) 否则,返回正常的Enumerator的对象。(Enumerator实现了迭代器和枚举两个接口)
先看看emptyEnumerator对象是如何实现的:
1 public static <T> Enumeration<T> emptyEnumeration() { 2 return (Enumeration<T>) EmptyEnumeration.EMPTY_ENUMERATION; 3 } 4 5 private static class EmptyEnumeration<E> implements Enumeration<E> { 6 static final EmptyEnumeration<Object> EMPTY_ENUMERATION 7 = new EmptyEnumeration<>(); 8 9 // 空枚举类的hasMoreElements() 始终返回false 10 public boolean hasMoreElements() { return false; } 11 // 空枚举类的nextElement() 抛出异常 12 public E nextElement() { throw new NoSuchElementException(); } 13 }
我们在来看看Enumeration类
Enumerator的作用是提供了“通过elements()遍历Hashtable的接口” 和 “通过entrySet()遍历Hashtable的接口”。因为,它同时实现了 “Enumerator接口”和“Iterator接口”。
1 private class Enumerator<T> implements Enumeration<T>, Iterator<T> { 2 Entry[] table = Hashtable.this.table; 3 int index = table.length; 4 Entry<K,V> entry = null; 5 Entry<K,V> lastReturned = null; 6 int type; 7 8 /** 9 * Indicates whether this Enumerator is serving as an Iterator 10 * or an Enumeration. (true -> Iterator). 11 */ 12 boolean iterator; 13 14 // 在将Enumerator当作迭代器使用时会用到,用来实现fail-fast机制。 15 protected int expectedModCount = modCount; 16 17 Enumerator(int type, boolean iterator) { 18 this.type = type; 19 this.iterator = iterator; 20 } 21 22 // 从遍历table的数组的末尾向前查找,直到找到不为null的Entry。 23 public boolean hasMoreElements() { 24 Entry<K,V> e = entry; 25 int i = index; 26 Entry[] t = table; 27 /* Use locals for faster loop iteration */ 28 while (e == null && i > 0) { 29 e = t[--i]; 30 } 31 entry = e; 32 index = i; 33 return e != null; 34 } 35 36 37 //从hasMoreElements() 和nextElement() 可以看出“Hashtable的elements()遍历方式” 38 // 首先,从后向前的遍历table数组。table数组的每个节点都是一个单向链表(Entry)。 39 // 然后,依次向后遍历单向链表Entry。 40 public T nextElement() { 41 Entry<K,V> et = entry; 42 int i = index; 43 Entry[] t = table; 44 /* Use locals for faster loop iteration */ 45 while (et == null && i > 0) { 46 et = t[--i]; 47 } 48 entry = et; 49 index = i; 50 if (et != null) { 51 Entry<K,V> e = lastReturned = entry; 52 entry = e.next; 53 return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e); 54 } 55 throw new NoSuchElementException("Hashtable Enumerator"); 56 } 57 58 // Iterator methods 59 // 迭代器Iterator的判断是否存在下一个元素,实际上,它是调用的hasMoreElements() 60 public boolean hasNext() { 61 return hasMoreElements(); 62 } 63 64 public T next() { 65 if (modCount != expectedModCount) 66 throw new ConcurrentModificationException(); 67 return nextElement(); 68 } 69 70 //首先,它在table数组中找出要删除元素所在的Entry,然后,删除单向链表Entry中的元素。 71 public void remove() { 72 if (!iterator) 73 throw new UnsupportedOperationException(); 74 if (lastReturned == null) 75 throw new IllegalStateException("Hashtable Enumerator"); 76 if (modCount != expectedModCount) 77 throw new ConcurrentModificationException(); 78 79 synchronized(Hashtable.this) { 80 Entry[] tab = Hashtable.this.table; 81 int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length; 82 83 for (Entry<K,V> e = tab[index], prev = null; e != null; 84 prev = e, e = e.next) { 85 if (e == lastReturned) { 86 modCount++; 87 expectedModCount++; 88 if (prev == null) 89 tab[index] = e.next; 90 else 91 prev.next = e.next; 92 count--; 93 lastReturned = null; 94 return; 95 } 96 } 97 throw new ConcurrentModificationException(); 98 } 99 } 100 } 101 102 103
remove() 的作用就是删除Hashtable中键为key的元素
1 public synchronized V remove(Object key) { 2 Entry tab[] = table; 3 int hash = hash(key); 4 int index = (hash & 0x7FFFFFFF) % tab.length; 5 // 找到“key对应的Entry(链表)”,然后在链表中找出要删除的节点,并删除该节点。 6 for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) { 7 if ((e.hash == hash) && e.key.equals(key)) { 8 modCount++; 9 if (prev != null) { 10 prev.next = e.next; 11 } else { 12 tab[index] = e.next; 13 } 14 count--; 15 V oldValue = e.value; 16 e.value = null; 17 return oldValue; 18 } 19 } 20 return null; 21 }
第4部分 Hashtable实现的Cloneable接口
Hashtable实现了Cloneable接口,即实现了clone()方法。
clone()方法的作用很简单,就是克隆一个Hashtable对象并返回。
1 public synchronized Object clone() { 2 try { 3 Hashtable<K,V> t = (Hashtable<K,V>) super.clone(); 4 t.table = new Entry[table.length]; 5 for (int i = table.length ; i-- > 0 ; ) { 6 t.table[i] = (table[i] != null) 7 ? (Entry<K,V>) table[i].clone() : null; 8 } 9 t.keySet = null; 10 t.entrySet = null; 11 t.values = null; 12 t.modCount = 0; 13 return t; 14 } catch (CloneNotSupportedException e) { 15 // this shouldn't happen, since we are Cloneable 16 throw new InternalError(); 17 } 18 }
第5部分 Hashtable实现的Serializable接口
Hashtable实现java.io.Serializable,分别实现了串行读取、写入功能。
串行写入函数就是将Hashtable的“总的容量,实际容量,所有的Entry”都写入到输出流中
串行读取函数:根据写入方式读出将Hashtable的“总的容量,实际容量,所有的Entry”依次读出
1 /** 2 * Save the state of the Hashtable to a stream (i.e., serialize it). 3 * 4 * @serialData The <i>capacity</i> of the Hashtable (the length of the 5 * bucket array) is emitted (int), followed by the 6 * <i>size</i> of the Hashtable (the number of key-value 7 * mappings), followed by the key (Object) and value (Object) 8 * for each key-value mapping represented by the Hashtable 9 * The key-value mappings are emitted in no particular order. 10 */ 11 private void writeObject(java.io.ObjectOutputStream s) 12 throws IOException { 13 Entry<K, V> entryStack = null; 14 15 synchronized (this) { 16 // Write out the length, threshold, loadfactor 17 s.defaultWriteObject(); 18 19 // Write out length, count of elements 20 s.writeInt(table.length); 21 s.writeInt(count); 22 23 // Stack copies of the entries in the table 24 for (int index = 0; index < table.length; index++) { 25 Entry<K,V> entry = table[index]; 26 27 while (entry != null) { 28 entryStack = 29 new Entry<>(0, entry.key, entry.value, entryStack); 30 entry = entry.next; 31 } 32 } 33 } 34 35 // Write out the key/value objects from the stacked entries 36 while (entryStack != null) { 37 s.writeObject(entryStack.key); 38 s.writeObject(entryStack.value); 39 entryStack = entryStack.next; 40 } 41 } 42 43 /** 44 * Reconstitute the Hashtable from a stream (i.e., deserialize it). 45 */ 46 private void readObject(java.io.ObjectInputStream s) 47 throws IOException, ClassNotFoundException 48 { 49 // Read in the length, threshold, and loadfactor 50 s.defaultReadObject(); 51 52 // Read the original length of the array and number of elements 53 int origlength = s.readInt(); 54 int elements = s.readInt(); 55 56 // Compute new size with a bit of room 5% to grow but 57 // no larger than the original size. Make the length 58 // odd if it's large enough, this helps distribute the entries. 59 // Guard against the length ending up zero, that's not valid. 60 int length = (int)(elements * loadFactor) + (elements / 20) + 3; 61 if (length > elements && (length & 1) == 0) 62 length--; 63 if (origlength > 0 && length > origlength) 64 length = origlength; 65 66 Entry<K,V>[] newTable = new Entry[length]; 67 threshold = (int) Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1); 68 count = 0; 69 initHashSeedAsNeeded(length); 70 71 // Read the number of elements and then all the key/value objects 72 for (; elements > 0; elements--) { 73 K key = (K)s.readObject(); 74 V value = (V)s.readObject(); 75 // synch could be eliminated for performance 76 reconstitutionPut(newTable, key, value); 77 } 78 this.table = newTable; 79 }
hashtable 的序列化和反序列化例子:
1 /** 2 * hashtale 序列化和反序列化 3 * 4 * @ClassName: hashtable_test 5 * @author Xingle 6 * @date 2014-6-30 上午9:33:04 7 */ 8 public class hashtable_test { 9 10 public static void main(String[] args) { 11 12 Hashtable<String, String> ht = new Hashtable<>(); 13 ht.put("1", "测试hashtable序列化"); 14 ht.put("2", "天天见"); 15 System.out.println("序列化前hashtable:"+ht); 16 new hashtable_test().serializable(ht); 17 18 } 19 20 private void serializable(Hashtable<String, String> ht_int) { 21 22 try { 23 ObjectOutputStream out = new ObjectOutputStream( 24 new FileOutputStream("test")); 25 out.writeObject(ht_int); 26 out.close(); 27 } catch (FileNotFoundException e) { 28 e.printStackTrace(); 29 } catch (IOException e) { 30 e.printStackTrace(); 31 } 32 33 try { 34 ObjectInputStream in = new ObjectInputStream(new FileInputStream( 35 "test")); 36 Hashtable<String, String> ht_out = (Hashtable<String, String>) in.readObject(); 37 System.out.println("反序列化后hashtable:"+ht_out); 38 } catch (FileNotFoundException e) { 39 e.printStackTrace(); 40 } catch (IOException e) { 41 e.printStackTrace(); 42 } catch (ClassNotFoundException e) { 43 e.printStackTrace(); 44 } 45 46 } 47 48 }
执行结果:
序列化前hashtable:{2=天天见, 1=测试hashtable序列化}
反序列化后hashtable:{2=天天见, 1=测试hashtable序列化}