HashMap和HashTable的区别

一、HashMap简介

1.HashMap由数组和链表组合构成的数据结构。基于哈希表实现,HashMap每一个元素是一个key-value对,数组每个地方都存了Key-Value这样的实例,在Java7叫Entry在Java8中叫Node
因为他本身所有的位置都为null,在put插入的时候会根据key的hash去计算一个index值。
其内部通过单链表解决冲突问题,超过阀值时,会自动增长。
HashMap常量源码:

//默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表 转成 红黑树 的阈值
static final int TREEIFY_THRESHOLD = 8;
//红黑树 转为 链表 的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//存储方式由链表转成红黑树的容量的最小阈值
static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap中存储的键值对的数量
transient int size;
//扩容阈值,当size>=threshold时,就会扩容
int threshold;
//HashMap的加载因子
final float loadFactor;

<<运算符,表示移位操作。每次向左移动一位(相对于二进制来说),表示乘以2,此处1<<4表示00001中的1向左移动了4位,变成了10000,换算成十进制就是2^4=16,也就是HashMap的默认容量就是16。
默认容量16的原因:
在创建HashMap的时候,阿里巴巴规范插件会提醒我们最好赋初值,而且最好是2的幂。
这样是为了位运算的方便,位与运算比算数计算的效率高了很多。
之所以选择16,是为了服务将Key映射到index的算法。16只是一个经验值,因为作者认为16这个初始容量是能符合常用而已。
在使用2的幂的数字的时候,Length-1的值是所有二进制位全为1,
这种情况下,index的结果等同于HashCode后几位的值。
只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。这是为了实现均匀分布。

2.加载因子(load_factor)默认为0.75,当HashMap中存储的元素的数量大于(容量×加载因子),也就是默认大于16*0.75=12时,HashMap会进行扩容的操作。
HashMap的扩容机制
数组容量是有限的,数据多次插入的,到达一定的数量就会进行扩容,也就是resize。
有两个因素:Capacity:HashMap当前长度。
LoadFactor:负载因子,默认值0.75f。

就比如当前的容量大小为100,当你存进第76个的时候,判断发现需要进行resize了,那就进行扩容。
分为两步扩容创建一个新的Entry空数组,长度是原数组的2倍。
ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。
是因为长度扩大以后,Hash的规则也随之改变。

Hash的公式---> index = HashCode(Key) & (Length - 1)

原来长度(Length)是8你位运算出来的值是2 ,新的长度是16,位运算出来的值明显不一样了。

3.HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。
我们一般都会使用HashTable或者ConcurrentHashMap,但是因为前者的并
发度的原因基本上没啥使用场景了,所以存在线程不安全的场景我们都使用的是ConcurrentHashMap。

4.HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。
为啥我们重写equals方法的时候需要重写hashCode方法呢?你能用HashMap给我举个例子么?
因为在java中,所有的对象都是继承于Object类。Ojbect类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的。
在未重写equals方法我们是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不同。
HashMap是通过key的hashCode去寻找index的,那index一样就形成链表了,也就是说在同一个链表上,”张三“和”三张“的index都可能是4。
我们去get的时候,他就是根据key去hash然后计算出index,找到了4,那我怎么找到具体的”张三“还是”三张“呢?
equals!是的,所以如果我们对equals方法进行了重写,建议一定要对hashCode方法重写,以保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。
不然一个链表的对象,到时候发现hashCode都一样,就无法找出正确的数据了。

5.HashMap存数据的过程:
HashMap内部维护了一个存储数据的Entry数组,HashMap采用链表解决冲突,每一个Entry本质上是一个单向链表
当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后通过indexFor(hash,length)求该key-value对的存储位置,计算方法是先用hash&0x7FFFFFFF后,再对length取模,这就保证每一个key-value对都能存入HashMap中,当计算出的位置相同时,由于存入位置是一个链表,则把这个key-value对插入链表头。
java8之前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去,因为写这个代码的作者认为后来的值被查找的可能性更大点,提升查找的效率。
但是,在java8之后,都是尾插法了。

先举个例子吧,我们现在往一个容量大小为2的put两个值,负载因子是0.75。当我们在put第2个的时候就会进行resize

2*0.75 = 1 

所以插入第2个就要resize了!
现在我们要在容量为2的容器里面不同线程插入A,B,C,假如我们在resize之前打个断点,那意味着数据都插入了但是还没resize那扩容前可能是这样的。
我们可以看到链表的指向A->B->C
Tip:A的下一个指针是指向B的
在这里插入图片描述
因为resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。
可能出现B的下一个指针指向了A
在这里插入图片描述
一旦几个线程都调整完成,就可能出现环形链表!
在这里插入图片描述
因为java8之后链表有红黑树的部分,可以看到代码已经多了很多if else的逻辑判断了,红黑树的引入巧妙的将原本O(n)的时间复杂度降低到了O(logn)。
使用头插会改变链表的上的顺序,但是如果使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

就是说原本是A->B,在扩容后那个链表还是A->B
在这里插入图片描述
Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。

Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系。
即使不会出现死循环,但是通过源码看到put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。

6.HashMap中key和value都允许为null。HashMap做了特殊处理,key为null的键值对永远都放在以table[0]为头结点的链表中。当key=null时,则hash值为0。

//如果原来存在相同的key-value,原来的value会被替换掉
public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
  int h;
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap重新计算Hash值源码:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

7.HashMap中的链表个数超过8个时会自动转化为红黑树,当删除小于六时重新变为链表,为啥呢?
根据泊松分布,在负载因⼦默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。
8.HashMap在多线程环境下存在线程安全问题,那你一般都是怎么处理这种情况的?
一般在多线程的场景,我都会使用好几种不同的方式去代替:
(1)使用Collections.synchronizedMap(Map)创建线程安全的map集合;
(2)Hashtable
(3)ConcurrentHashMap
不过出于线程并发度的原因,我都会舍弃前两者使用最后的ConcurrentHashMap,他的性能和效率明显高于前两者。
9.Collections.synchronizedMap是怎么实现线程安全的你有了解过么?
在SynchronizedMap内部维护了一个普通对象Map,还有排斥锁mutex,如图
在这里插入图片描述

Collections.synchronizedMap(new HashMap<>(16));

我们在调用这个方法的时候就需要传入一个Map,可以看到有两个构造器,如果你传入了mutex参数,则将对象排斥锁赋值为传入的对象。
如果没有,则将对象排斥锁赋值为this,即调用synchronizedMap的对象,就是上面的Map。
创建出synchronizedMap之后,再操作map的时候,就会对方法上锁,如图全是锁
在这里插入图片描述
10.跟HashMap相比Hashtable是线程安全的,适合在多线程的情况下使用,但是效率可不太乐观,效率低的原因?
我看过他的源码,他在对数据操作的时候都会上锁,所以效率比较低下
在这里插入图片描述

二、Hashtable简介

1.Hashtable是基于哈希表实现的,每个元素是一个key-value对,其内部是通过单链表解决冲突问题,容量不足时,会自动增长。

2.Hashtable是JDK1.0引入的类,线程安全的,可用于多线程环境。
HashTable的源码,很简单粗暴,直接在方法上加锁,并发度很低,最多同时允许一个线程访问,ConcurrentHashMap就好很多了,1.7和1.8有较大的不同,不过并发度都比前者好太多了。
在这里插入图片描述
3.Hashtable同样实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆。
在这里插入图片描述
4.Hashtable 是不允许键或值为 null 的原因
Hashtable使用的是快速失败机制(fail—fast)。如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。

快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。

集合在被遍历期间如果内容发生变化,就会改变modCount的值。
每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

5.Hashtable 初始容量为:11,负载因子默认都是:0.75。当现有容量大于总容量负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。

参考文章
https://www.cnblogs.com/williamjie/p/9099141.html
https://blog.csdn.net/wangxing233/article/details/79452946

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMapHashtable 都是用于存储键值对的数据结构,它们在功能上非常相似,但也存在一些区别。 1. 线程安全性:Hashtable 是线程安全的,即多个线程可以同时访问一个 Hashtable 实例而不需要额外的同步措施。而 HashMap 不是线程安全的,如果多个线程同时访问一个 HashMap 实例,可能会导致数据不一致的问题。如果需要在多线程环境下使用,可以考虑使用 ConcurrentHashMap。 2. null 键和 null 值:Hashtable 不允许键或值为 null,如果尝试将 null 键或 null 值放入 Hashtable 中,会抛出 NullPointerException。而 HashMap 允许键和值为 null,可以正常存储和获取 null 值。 3. 继承关系:Hashtable 是 Dictionary 类的子类,而 HashMap 是 AbstractMap 类的子类。由于继承关系的不同,导致它们在实现上有一些差异。 4. 迭代顺序:HashMap 不保证迭代顺序,即遍历 HashMap 的键值对时,不一定按照插入顺序或者其他顺序进行遍历。而 Hashtable 的迭代顺序是按照插入顺序进行的。 5. 性能:由于 Hashtable 是线程安全的,它在多线程环境下的性能可能会受到一定影响。而 HashMap 在单线程环境下的性能通常会更好。 总的来说,如果在单线程环境下使用,并且需要允许键或值为 null,可以优先选择使用 HashMap。如果在多线程环境下使用,或者需要保证迭代顺序,可以考虑使用 Hashtable

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值