zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    spring cloud 和 阿里微服务spring cloud Alibaba
    为WPF中的ContentControl设置背景色
    java RSA 解密
    java OA系统 自定义表单 流程审批 电子印章 手写文字识别 电子签名 即时通讯
    Hystrix 配置参数全解析
    spring cloud 2020 gateway 报错503
    Spring Boot 配置 Quartz 定时任务
    Mybatis 整合 ehcache缓存
    Springboot 整合阿里数据库连接池 druid
    java OA系统 自定义表单 流程审批 电子印章 手写文字识别 电子签名 即时通讯
  • 原文地址:https://www.cnblogs.com/Eason-S/p/5692943.html
Copyright © 2011-2022 走看看