• Java集合(8):Hashtable


    一.Hashtable介绍

      和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射,它在很大程度上和HashMap的实现差不多。

      Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。

    1.Hashtable的继承关系

    public class Hashtable<K,V>
        extends Dictionary<K,V>
        implements Map<K,V>, Cloneable, java.io.Serializable
    

    2.Hashtable的类图关系

      HashTable继承Dictionary类,实现Map接口。其中Dictionary类是任何可将键映射到相应值的类的抽象父类。每个键和每个值都是一个对象。在任何一个 Dictionary 对象中,每个键至多与一个值相关联。Map是”key-value键值对”接口。

    二.Hashtable源码解析

    1.Hashtable的私有属性

    1 private transient Entry<?,?>[] table;//table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。
    2 private transient int count;//count是Hashtable的大小,它是Hashtable保存的键值对的数量。 
    3 private int threshold;//threshold是Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。
    4 private float loadFactor;//loadFactor就是加载因子。 
    5 private transient int modCount = 0;//modCount是用来实现fail-fast机制的

      所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并且立即抛出ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你(你已经出错了)。

    2.Hashtable的构造方法

     1 // 默认构造函数。
     2 public Hashtable() {
     3     this(11, 0.75f);
     4 }
     5 
     6 // 指定“容量大小”的构造函数
     7 public Hashtable(int initialCapacity) {
     8     this(initialCapacity, 0.75f);
     9 }
    10 
    11 // 指定“容量大小”和“加载因子”的构造函数
    12 public Hashtable(int initialCapacity, float loadFactor) {
    13     if (initialCapacity < 0)//验证初始容量
    14         throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    15     if (loadFactor <= 0 || Float.isNaN(loadFactor))//验证加载因子
    16         throw new IllegalArgumentException("Illegal Load: "+loadFactor);
    17     if (initialCapacity==0)
    18         initialCapacity = 1;
    19     this.loadFactor = loadFactor;
    20     table = new Entry[initialCapacity];//初始化table,获得大小为initialCapacity的table数组
    21     threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//计算阀值
    22     initHashSeedAsNeeded(initialCapacity);//初始化HashSeed值
    23 }
    24 
    25 // 包含“子Map”的构造函数
    26 public Hashtable(Map<? extends K, ? extends V> t) {
    27     this(Math.max(2*t.size(), 11), 0.75f);//设置table容器大小,其值==t.size * 2 + 1
    28     putAll(t);
    29 }

    3.存储数据put

    将指定 key 映射到此哈希表中的指定 value。注意这里键key和值value都不可为空

     1 public synchronized V put(K key, V value) {
     2     if (value == null) {// 确保value不为null
     3         throw new NullPointerException();
     4     }
     5 
     6     /*
     7      * 确保key在table[]是不重复的
     8      * 处理过程:
     9      * 1、计算key的hash值,确认在table[]中的索引位置
    10      * 2、迭代index索引位置,如果该位置处的链表中存在一个一样的key,则替换其value,返回旧值
    11      */
    12     Entry tab[] = table;
    13     int hash = hash(key);    //计算key的hash值
    14     int index = (hash & 0x7FFFFFFF) % tab.length;     //确认该key的索引位置
    15     for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { //迭代,寻找该key,替换
    16         if ((e.hash == hash) && e.key.equals(key)) {
    17             V old = e.value;
    18             e.value = value;
    19             return old;
    20         }
    21     }
    22 
    23     modCount++;
    24     if (count >= threshold) {  //如果容器中的元素数量已经达到阀值,则进行扩容操作
    25         rehash();
    26         tab = table;
    27         hash = hash(key);
    28         index = (hash & 0x7FFFFFFF) % tab.length;
    29     }
    30 
    31     Entry<K,V> e = tab[index];// 在索引位置处插入一个新的节点
    32     tab[index] = new Entry<>(hash, key, value, e);
    33     count++;//容器中元素+1
    34     return null;
    35 }

      put方法的整个处理流程是:计算key的hash值,根据hash值获得key在table数组中的索引位置,然后迭代该key处的Entry链表(我们暂且理解为链表),若该链表中存在一个这个的key对象,那么就直接替换其value值即可,否则在将改key-value节点插入该index索引位置处。

    过程演示如下:

      首先我们假设一个容量为5的table,存在如下的键值对:

      然后我们插入一个数:put(16,22),key=16在table的索引位置为1,同时在1索引位置有两个数,程序对该“链表”进行迭代,发现存在一个key=16,这时要做的工作就是用newValue=22替换oldValue16,并将oldValue=16返回。

      在put(33,33),key=33所在的索引位置为3,并且在该链表中也没有存在某个key=33的节点,所以就将该节点插入该链表的第一个位置。

      扩容操作:在put方法中,如果需要向table[]中添加Entry元素,会首先进行容量校验,如果容量已经达到了阀值,HashTable就会进行rehash()扩容处理

     1 protected void rehash() {
     2     int oldCapacity = table.length;
     3     Entry<K,V>[] oldMap = table;
     4 
     5     int newCapacity = (oldCapacity << 1) + 1;//新容量=旧容量 * 2 + 1
     6     if (newCapacity - MAX_ARRAY_SIZE > 0) {
     7         if (oldCapacity == MAX_ARRAY_SIZE)
     8             return;
     9         newCapacity = MAX_ARRAY_SIZE;
    10     }
    11     Entry<K,V>[] newMap = new Entry[];//新建一个size = newCapacity 的HashTable
    12     modCount++;
    13     threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//重新计算阀值
    14     boolean rehash = initHashSeedAsNeeded(newCapacity);//重新计算hashSeed
    15     table = newMap;
    16     for (int i = oldCapacity ; i-- > 0 ;) {//将原来的元素拷贝到新的HashTable中
    17         for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
    18             Entry<K,V> e = old;
    19             old = old.next;
    20             if (rehash) {
    21                 e.hash = hash(e.key);
    22             }
    23             int index = (e.hash & 0x7FFFFFFF) % newCapacity;
    24             e.next = newMap[index];
    25             newMap[index] = e;
    26         }
    27     }
    28 }

      通过上面rehash代码我们可以看到容量扩大两倍+1,同时需要将原来HashTable中的元素一一复制到新的HashTable中,这个过程是比较消耗时间的,同时还需要重新计算hashSeed的,毕竟容量已经变了。

      关于阀值:比如初始值11、加载因子默认0.75,那么这个时候阀值threshold=8,当容器中的元素达到8时,HashTable进行一次扩容操作,容量 = 8 * 2 + 1 =17,而阀值threshold=17*0.75 = 13,当容器元素再一次达到阀值时,HashTable还会进行扩容操作,一次类推。

    4.数据读取get()

      相对于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 }

    5.其他方法

    三.Hashtable的遍历

    1.遍历Hashtable的键值对(效率较高)

    第一步:根据entrySet()获取Hashtable的“键值对”的Set集合。
    第二步:通过Iterator迭代器遍历“第一步”得到的集合。

     1 // 假设table是Hashtable对象
     2 // table中的key是String类型,value是Integer类型
     3 Integer integ = null;
     4 Iterator iter = table.entrySet().iterator();
     5 while(iter.hasNext()) {
     6     Map.Entry entry = (Map.Entry)iter.next();
     7     // 获取key
     8     key = (String)entry.getKey();
     9         // 获取value
    10     integ = (Integer)entry.getValue();
    11 }

    2.通过Iterator遍历Hashtable的键(效率较低)

    第一步:根据keySet()获取Hashtable的“键”的Set集合。
    第二步:通过Iterator迭代器遍历“第一步”得到的集合。

     1 // 假设table是Hashtable对象
     2 // table中的key是String类型,value是Integer类型
     3 String key = null;
     4 Integer integ = null;
     5 Iterator iter = table.keySet().iterator();
     6 while (iter.hasNext()) {
     7         // 获取key
     8     key = (String)iter.next();
     9         // 根据key,获取value
    10     integ = (Integer)table.get(key);
    11 }

    3.通过Iterator遍历Hashtable的值

    第一步:根据value()获取Hashtable的“值”的集合。
    第二步:通过Iterator迭代器遍历“第一步”得到的集合。

    1 // 假设table是Hashtable对象
    2 // table中的key是String类型,value是Integer类型
    3 Integer value = null;
    4 Collection c = table.values();
    5 Iterator iter= c.iterator();
    6 while (iter.hasNext()) {
    7     value = (Integer)iter.next();
    8 }

    4.通过Enumeration遍历Hashtable的键(效率较高)

    第一步:根据keys()获取Hashtable的集合。
    第二步:通过Enumeration遍历“第一步”得到的集合。

    Enumeration enu = table.keys();
    while(enu.hasMoreElements()) {
        System.out.println(enu.nextElement());
    }  
    

    5.通过Enumeration遍历Hashtable的值(效率较高)

    第一步:根据elements()获取Hashtable的集合。
    第二步:通过Enumeration遍历“第一步”得到的集合。

    Enumeration enu = table.elements();
    while(enu.hasMoreElements()) {
        System.out.println(enu.nextElement());
    }
    

    四.HashTable和HashMap的比较

      HashTable的应用非常广泛,HashMap是新框架中用来代替HashTable的类,也就是说建议使用HashMap

    下面着重比较一下二者的区别:

    1.继承不同

    Hashtable是基于陈旧的Dictionary类的,HashMap是java 1.2引进的Map接口的一个实现。

    2.同步

    Hashtable 中的方法是同步的,保证了Hashtable中的对象是线程安全的。

    HashMap中的方法在缺省情况下是非同步的,HashMap中的对象并不是线程安全的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。

    3.效率

    单线程中, HashMap的效率大于Hashtable。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合,HashMap是Hashtable的轻量级实现,这样可以避免由于同步带来的不必要的性能开销,从而提高效率。

    4.null值

    Hashtable中,key和value都不允许出现null值,否则出现NullPointerException。

    在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。

    5.遍历方式

    Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式。

    6.容量

    Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。

    HashTable中hash数组默认大小是11,增加的方式是 old*2+1。

    HashMap中hash数组的默认大小是16,而且一定是2的指数。 

    小结:

      无论什么时候有多个线程访问相同实例的可能时,就应该使用Hashtable,反之使用HashMap。非线程安全的数据结构能带来更好的性能。

      如果在将来有一种可能—你需要按顺序获得键值对的方案时,HashMap是一个很好的选择,因为有HashMap的一个子类 LinkedHashMap。所以如果你想可预测的按顺序迭代(默认按插入的顺序),你可以很方便用LinkedHashMap替换HashMap。反观要是使用的Hashtable就没那么简单了。同时如果有多个线程访问HashMap,Collections.synchronizedMap()可以代替,总的来说HashMap更灵活

    参考:http://cmsblogs.com/?p=618

    http://blog.csdn.net/zheng0518/article/details/42199477

    http://www.cnblogs.com/devinzhang/archive/2012/01/13/2321481.html

  • 相关阅读:
    C语言写数据库(三)
    C语言写数据库(一)
    KMP 串的模式匹配 (25 分)
    11-散列4 Hashing
    11-散列3 QQ帐户的申请与登陆 (25 分)
    11-散列1 电话聊天狂人 (25 分)
    11-散列2 Hashing (25 分)
    am335x system upgrade uboot nand boot(三)
    am335x system upgrade uboot ethernet(二)
    am335x system upgrade uboot sd boot(一)
  • 原文地址:https://www.cnblogs.com/Eason-S/p/5692943.html
Copyright © 2020-2023  润新知