HashMap与Has和Table的区别

HashMap和HashTable的区别和不同

       Hashtable是一个古老的类了,尽量少用;

       Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比Hashtable的性能高一点;但如果有多条线程访问同一个Map对象时,使用Hashtable实现类会更好。

       Hashtable不允许使用null作为key和value,如果试图把null值放进Hashtable中,将会引发NullPointerException异常;但HashMap里最多只有一项key-value的key和value的情形(即HashMap里的key值不能重复)。

记得刚毕业那会准备面试,看过不少面试题,里面有个说出HashMap和HashTable不同的题目,我那会面试的时候也遇到不少次这个问题,还隐约记得当时的回答是这样的:
HashTable是比较旧的版本;HashTable是线程安全的,而HashMap是非线程安全的;HashMap的key和value允许Null值,还有就是HashMap效率要高。好像就这些了吧,当时认识也比较肤浅。

前段时间有空就想起来了这个问题,于是就想深入的理解一下这两个比较常用的数据结构,然后看了一下这两个类的源码,有了比以前更深入的了解。大体上这两个类内部处理差别不是很大,当然还是有不少不同,下面我们来一一探讨一下他们之间的不同
引言:首先来说一下HashMap内部其实是一个数组+链表的结构,当你put一个元素的时候,HashMap会根据Key的hash值算出要存放的数组的位置,如果两个元素算出的数组值相同的话,那么他们会放在数组里的同一个位置,这个时候在获取该元素的时候,那么会根据Key的hash找到数组的位置,然后再从链表中找到该元素。那么我们可以很容易的想象到,如果每一个数组里只有一个元素的时候,效率是最高的,因为不需要再对链表进行操作。有了这点认识我们就可以进行接下来的分析了。

[list]
[1]数组大小 。
既然说了内部是数组+链表,那就设计到数组的大小,这一点,HashMap和HashTable是不同的
HashMap的默认大小

Java代码 
static   final   int  DEFAULT_INITIAL_CAPACITY =  16 ;  
static final int DEFAULT_INITIAL_CAPACITY = 16;

我们看到默认是16,而且HashMap的大小一定是2的幂数。这里你可能会问了,如果初始化HashMap的时候指定了一个不是2的幂数的长度呢?如果是这种情况,它也会找到一个最接近你指定值的一个2的幂数,下面是源码:

Java代码 
int  capacity =  1 ;  
        while  (capacity < initialCapacity)   
            capacity <<= 1 ;  
int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

就是他会找到一个比你指定值大且是2的幂数的一个值,那么为什么它会这么做呢,这个问题我留到下面具体说明。
HashMap长度最大值也是有设定的,他的最大值是

Java代码 
static   final   int  MAXIMUM_CAPACITY =  1  <<  30 ;  
static final int MAXIMUM_CAPACITY = 1 << 30;

如果你指定超过这个大小,它会抛弃你指定的值而采用这个默认值

Java代码 
if  (initialCapacity > MAXIMUM_CAPACITY)  
           initialCapacity = MAXIMUM_CAPACITY;  
 if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;

HashTable的默认大小

Java代码 
public  Hashtable() {  
    this ( 11 ,  0 .75f);  
    }  
public Hashtable() {
 this(11, 0.75f);
    }

默认是11,HashTable跟HashMap不同如果你指定了长度,它不会对你的指定值进行处理。HashTable的最大值,我没看有看到HashTable中有指定最大值的行为

[2]长度扩容
上面我们讲了两个类的初始大小,这里需要说明的事,在实际中真正利用的长度并不是这个值,而是有个加载因子,默认是0.75,比如长度是16,而真正使用的是16*0.75,当超过这个数,就会扩容
HashMap扩容
HashMap扩容会把之前长度*2,因为之前的长度肯定是2的幂数,所以自动扩容后也是2的幂数

HashTable扩容
HashTable扩容是把之前长度*2+1

扩容操作是比较消耗资源的,所以这里我们告诉我们在初始化HashMap和HashTable的时候要考虑到实际使用时的长度,应该尽可能的避免出现扩容的操作,从而提高效率

[3]put操作
HashMap的put

Java代码 
public  V put(K key, V value) {  
    K k = maskNull(key);  
        int  hash = hash(k);  
        int  i = indexFor(hash, table.length);  
  
        for  (Entry<K,V> e = table[i]; e !=  null ; e = e.next) {  
            if  (e.hash == hash && eq(k, e.key)) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this );  
                return  oldValue;  
            }  
        }  
  
        modCount++;  
        addEntry(hash, k, value, i);  
        return   null ;  
    }  
public V put(K key, V value) {
 K k = maskNull(key);
        int hash = hash(k);
        int i = indexFor(hash, table.length);

        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            if (e.hash == hash && eq(k, e.key)) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, k, value, i);
        return null;
    }

我们看到首先进行maskNull,这也是为什么HashMap允许有null键;
然后是计算hash值,下面是hash方法

Java代码 
static   int  hash(Object x) {  
        int  h = x.hashCode();  
  
        h += ~(h << 9 );  
        h ^=  (h >>> 14 );  
        h +=  (h << 4 );  
        h ^=  (h >>> 10 );  
        return  h;  
    }  
static int hash(Object x) {
        int h = x.hashCode();

        h += ~(h << 9);
        h ^=  (h >>> 14);
        h +=  (h << 4);
        h ^=  (h >>> 10);
        return h;
    }
这里HashMap对hash值进行处理,进行了一些列的操作,目的是为了让hash值的高位参与接下来的运算,至于为什么要用到 9,14,4,10这几个数,我也不明白,我曾经给JDK的作者之一道格发过邮件咨询这个问题,他没有给我正面的回答,而是建议我看一本书中的hash那一章节,在我查找该问题的时候,发现国外很多人也向道格咨询过此问题,得到的回到好像跟我得到的回答是一样的,嘿嘿。好了,不说题外话了,反正大家知道经过这么操作我们到了一个非负整数。接下来是计算数组下标indexFor

Java代码 
static   int  indexFor( int  h,  int  length) {  
        return  h & (length- 1 );  
    }  
static int indexFor(int h, int length) {
        return h & (length-1);
    }

这里的 h & (length-1)是比较出彩的地方,也是为什么前面我们介绍HashMap长度的时候,他总是2的幂数,现在我们知道为什么了吧,它实现了 h & (length-1)等价于h % length的操作,求与比求模效率要高,在道格给我的回信中也提到了这一点。也许有人会问了,这里求与的操作中hash的高位没有参与到运算,那么当 hash值高位不同时,算出的index却是相同的,那么也就是说增加了在数组中相同位置存放的对象的几率,sun其实考虑到这个问题了,这就是我前面提到的那个hash方法,这也是我向道格发邮件的原因。
put方法接下来的代码就比较简单了,大家可以自己看,提一句就是put也是有返回值的如果你存放相同的Key的话返回旧的value值,如果是不同的key返回null

HashTable的put

Java代码 
public   synchronized  V put(K key, V value) {  
    // Make sure the value is not null   
    if  (value ==  null ) {  
        throw   new  NullPointerException();  
    }  
  
    // Makes sure the key is not already in the hashtable.   
    Entry tab[] = table;  
    int  hash = key.hashCode();  
    int  index = (hash &  0x7FFFFFFF ) % tab.length;  
    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;  
            index = (hash & 0x7FFFFFFF ) % tab.length;  
    }   
  
    // Creates the new entry.   
    Entry<K,V> e = tab[index];  
    tab[index] = new  Entry<K,V>(hash, key, value, e);  
    count++;  
    return   null ;  
    }  
public synchronized V put(K key, V value) {
 // Make sure the value is not null
 if (value == null) {
     throw new NullPointerException();
 }

 // Makes sure the key is not already in the hashtable.
 Entry tab[] = table;
 int hash = key.hashCode();
 int index = (hash & 0x7FFFFFFF) % tab.length;
 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;
            index = (hash & 0x7FFFFFFF) % tab.length;
 }

 // Creates the new entry.
 Entry<K,V> e = tab[index];
 tab[index] = new Entry<K,V>(hash, key, value, e);
 count++;
 return null;
    }

我这里首先就是发现该put是同步的,这是和HashMap的不同。还有这里没有对hash进行处理,而是直接用hash值,在计算数组下标的时候也和HashMap不同,他这里就是采用了普通的求模操作,在求模之前对hash值进行取绝对值操作,保证计算出来的下标是正的;我们可以看出差别了,单从计算下标这一点,HashMap是比HashTable有优化的。

这里我们再来总结一下:HashMap对hash值进行了处理(处理的目的:1是为了得到一个正数,另外一个是为了在进行接下来indexFor 计算时尽可能的得到均匀的下标值);HashTable没有对hash值进行处理,只是简单的进行了一下hash & 0x7FFFFFFF操作(取绝对值);HashMap在进行index计算是使用了求与,而Hashtable采用了求模,HashMap之所以可以采用求与来替代求模前提是建立在数组长度总是2的幂数上,我们可以这么说HashMap采用了2的幂数的长度和求与的结合达到了HashTable中求模的效果,从而提高了效率。

 

 


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值