HashTable和HashMap的区别

散列表

概念:Hash table,也叫哈希表,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
HashMap/HashTable内部用Entry数组实现哈希表,而对于映射到同一个哈希桶(数组的同一个位置)的键值对,使用Entry链表来存储(解决hash冲突)。
在这里插入图片描述

Java中HashTable和HashMap(JDK8以前)的差异

HashTable自JDK1.0起就开始出现至今,而HashMap直到JDK1.2才出现。HashMap出现后,完善了HashTable过去的不足,但也来了新的不足之处。

相同点:
a.内部Entry

HashTable和HashMap里面有一个内部类Entry,用来存放真正的Key-Value键值对;

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         * 内部Entry类结构,存放真正的Key-Value键值对
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        .......
  }
b.默认负载因子

默认负载因子都为0.75
HashTable:

//指定初始容量大小的构造函数
public Hashtable(int initialCapacity) {
	this(initialCapacity, 0.75f);
}

HashMap:

/**
 * The load factor used when none specified in constructor.
 * 负载因子
 */
  static final float DEFAULT_LOAD_FACTOR = 0.75f;
不同点:
a.继承的类不同:

HashTable继承和实现:

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

HashMap继承和实现:

public class HashMap<K,V> extends AbstractMap<K,V>
   		implements Map<K,V>, Cloneable, Serializable
b.默认初始大小

HashTable中默认构造大小为11,负载因子为0.75f;

//默认无参构造函数
public Hashtable() {
    this(11, 0.75f);
}

HashMap中的初始大小为16(1 << 4 ),负载因子也是0.75f;

//默认无参构造函数
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
c.两者的Hash函数不同

HashTable会尽量使用素数、奇数,哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。代码中使用hashSeed值与对象的哈希值做异或运算得出所需存储的对象的哈希值。

/**
* 与此实例关联的随机值,使哈希冲突更难找到的密钥哈希码。
*/
transient int hashSeed;
private int hash(Object k) {
    // hashSeed will be zero if alternative hashing is disabled.
    return hashSeed ^ k.hashCode();
}

HashMap总是使用2的幂作为哈希表的大小,为了减小哈希冲突,在HashSeed与对象哈希值做异或运算之后,再做两次无符号右移位运算和异或运算,得出的哈希值。

//hashMap中给定的Hash算法
final int hash(Object k) {
    int h = hashSeed;
 	h ^= k.hashCode();
	h ^= (h >>> 20) ^ (h >>> 12);
	return h ^ (h >>> 7) ^ (h >>> 4);
}

HashMap和HashTable在计算hash时都用到了一个叫hashSeed的变量。这是因为映射到同一个hash桶内的Entry对象,是以链表的形式存在的,而链表的查询效率比较低,所以HashMap/HashTable的效率对哈希冲突非常敏感,所以可以额外开启一个可选hash(hashSeed),从而减少哈希冲突。

d.允许的最大容量

HashTable中最大容量为:2147483647 [0x7fffffff] - 8

//最大数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

HashMap中最大容量则为:1073741824

//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
e.针对触发扩容的时机不同

前面有说到,在两者在默认构造的时候,都有一个默认的负载因子值为0.75f。通过该负载因子计算得出阈值(threshold),这个阈值就是作为扩容的触发点。
HashTable在触发扩容操作时,是当前哈希表中的Entry总数>=阈值(threshold)时。

//HashTable
//如果容器中的元素数量已经达到阀值,则进行扩容操作  
if (count >= threshold) {
	// Rehash the table if the threshold is exceeded
	rehash();
	
	tab = table;
	hash = hash(key);
	index = (hash & 0x7FFFFFFF) % tab.length;
}

protected void rehash() {
	.......
	int newCapacity = (oldCapacity << 1) + 1;//原容量的2倍+1
	.......
  	Entry<K,V>[] newMap = new Entry[newCapacity];
  	.......   
    for (int i = oldCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;
            
            if (rehash) {
                e.hash = hash(e.key);
            }
            //& 0x7FFFFFFF目的为了去掉符号;% newCapacity减少hash冲突
            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = newMap[index];
            newMap[index] = e;
        }
    }
}

而HashMap触发扩容操作时,要同时判断阈值和哈希冲突,当两者同时满足时,才进行原基础的2倍扩容

//HashMap
//如果容器中的元素数量已经达到阀值且哈希桶不为空 
if ((size >= threshold) && (null != table[bucketIndex])) {
	// 每次扩充为原来的2n 
	resize(2 * table.length);
	hash = (null != key) ? hash(key) : 0;
	bucketIndex = indexFor(hash, table.length);
}
///获取桶索引
static int indexFor(int h, int length) {
	// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
	return h & (length-1);
}
f.允许空值的情况

HashMap是支持null键和null值的,而HashTable在遇到null时,会抛出NullPointerException异常。这并不是因为HashTable有什么特殊的实现层面的原因导致不能支持null键和null值,这仅仅是因为HashMap在实现时对null做了特殊处理,将null的hashCode值定为了0,从而将其存放在哈希表的第0个bucket中。

//HashTable中put操作
public synchronized V put(K key, V value) {
	//确保value不能为空判断
	if (value == null) {
	    throw new NullPointerException();
	}
        ......
}

HashMap在实现时对null做了特殊处理

//HashMap中put操作
public V put(K key, V value) {
    ......
    if (key == null)
        return putForNullKey(value);
    ......
    }
}
/**
     * Offloaded version of put for null keys
     */
private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}
h.线程安全

HashTable是线程安全的,HashMap不是线程安全的。
HashTable:在操作方法前,用synchronized修饰,保证线程的安全性,但效率极低。

//put操作
public synchronized V put(K key, V value)//remove操作
public synchronized V remove(Object key)//get操作
public synchronized V get(Object key)//clear操作
public synchronized void clear()......

HashMap:在以上操作中没有synchronized关键字修饰,并且它的设计就不是以线程安全来设计的。它线程不安全的地方,主要在于扩容后,旧的Entry在关联新的哈希桶时会出现线程不安全的情况。

HashMap和HashTable的总结

HashTable已经被淘汰了,简单来说就是,如果你不需要线程安全,那么使用HashMap,如果需要线程安全,那么使用ConcurrentHashMap。HashTable已经被淘汰了,不要在新的代码中再使用它。

虽然HashMap和HashTable的公开接口应该不会改变,或者说改变不频繁。但每一版本的JDK,都会对HashMap和HashTable的内部实现做优化,比如JDK 1.8的红黑树优化。所以,尽可能的使用新版本的JDK吧,除了那些炫酷的新功能,普通的API也会有性能上有提升。

为什么HashTable已经淘汰了,还要优化它?因为有老的代码还在使用它,所以优化了它之后,这些老的代码也能获得性能提升。

版权声明:Echo_IX https://blog.csdn.net/chenxiaoti/article/details/82900630

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值