HashMap
HashMap是基于哈希表的map接口实现的,它存储的内容是键值对(key-value)映射。允许使用null值和null键(除了非同步和允许使用null职位,hashmap类与hashtable大致相同)。因为是基于hash表实现的所以不能保证映射顺序,特别是保证该顺序恒久不变。(因为每次扩容都会重新计算hash)。
迭代collection视图所需要的时间与 hashmap的实例的"容量"(桶的数量)及其大小(键值对映射关系数)成比例。所以,如果迭代性能很重要,就不能把初始容量设置的太高或者将加载因子设置的太低。
HashMap中有两个参数影响其性能:初始容量和加载因子
容量是哈希表中桶的数量,初始容量只是哈希表在创建时候的容量,加载因子是哈希表在其容量自动增加之前可以达到多少的一种尺度。当hash表中的条数超出了当前加载因子与当前容量的乘积时,则要对hash表进行rehash操作(就是重建内部数据结构),从而哈希表将具有大约两倍的桶数。
加载因子
加载因子也叫作扩容因子,用来判断什么时候进行扩容,假设加载因子为0.75,HashMap的初始容量为16,当HashMap中有16 * 0.75 = 12个容量时,HashMap就会进行扩容。如果加载因子越大,扩容发生的频率就会比较低,占用空间比较小,但是发生hash冲突的几率会提升,对元素操作时间会增加,运行效率降低;如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费;而且因为容量默认为2的次方,当加载因子为0.75时,容量和加载因子的乘积为整数。所以系统默认加载因子取了0.5 -1 之间的0.75。
哈希冲突
哈希冲突就是指hash值的冲突,指的是:多个key对象,在put的时候,进行计算hash值,得到的hash值是同一个的情况。
计算的时候就是用一个数取模运算,取余数就是要存储数组数据的下标,但是余数可能会存在一样的,这样就导致了一样余数的数据无法存储,所以造成了hash冲突。
解决hash冲突的三种方法
- 开放地址发:寻找下一个为空的数组下标,而后将冲突元素存储。
- 再散列法(二次哈希法):在此使用一个不同的hash算法重新计算一次。
- 链地址法(拉链法):hashmap使用的方法,将所有冲突的元素按照链表存储,冲突后时间复杂度为O(1+n)n为冲突元素的个数。
两个关键方法:put和get
HashMap是声明了 Map,Cloneable, Serializable 接口,和继承了AbstractMap类,里面的 Iterator 其实主要都是其内部类HashIterator 和其他几个 iterator 类实现,当然还有一个很重要的继承了Map.Entry 的 Entry 内部类。
public Object put(Object key, Object value) {
// 这个就是判断键值是够为空,如果为空就会返回一个static Object作为键值,这就是为什么hashmap允许空键空值的原因
Object k = maskNull(key);
int hash = hash(k);
int i = indexFor(hash, table.length);
HashMap底层原理
HashMap在JDK1.8之前的实现方式 数组+链表,
但是在JDK1.8后对HashMap进行了底层优化,改为了由 数组+链表或者数值+红黑树实现,主要的目的是提高查找效率
-
-
Jdk8数组+链表或者数组+红黑树实现,当链表中的元素超过了 8 个以后, 会将链表转换为红黑树,当红黑树节点 小于 等于6 时又会退化为链表。
-
当new HashMap():底层没有创建数组,首次调用put()方法示时,底层创建长度为16的数组,jdk8底层的数组是:Node[],而非Entry[],用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用rehash方法将数组容量增加到原来的两倍,专业术语叫做扩容,在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能.默认的负载因子大小为0.75,数组大小为16。也就是说,默认情况下,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍。
-
在我们Java中任何对象都有hashcode,hash算法就是通过hashcode与自己进行向右位移16的异或运算。这样做是为了计算出来的hash值足够随机,足够分散,还有产生的数组下标足够随机。
-
不同的对象算出来的数组下标是相同的这样就会产生hash冲突,当单线链表达到一定长度后效率会非常低。
-
在链表长度大于8的时候,将链表就会变成红黑树,提高查询的效率。
-
map.put(k,v)实现原理
(1)首先将k,v封装到Node对象当中(节点)。
(2)先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
(3)下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
map.get(k)实现原理
(1)、先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
(2)、在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。
Hashmap和hashtable ConcurrentHashMap区别
区别对比一(HashMap 和 HashTable 区别):
1、HashMap 是非线程安全的,HashTable 是线程安全的。
2、HashMap 的键和值都允许有 null 值存在,而 HashTable 则不行。
3、因为线程安全的问题,HashMap 效率比 HashTable 的要高。
4、Hashtable 是同步的,而 HashMap 不是。因此,HashMap 更适合于单线
程环境,而 Hashtable 适合于多线程环境。一般现在不建议用 HashTable, ①
是 HashTable 是遗留类,内部实现很多没优化和冗余。②即使在多线程环境下,
现在也有同步的 ConcurrentHashMap 替代,没有必要因为是多线程而用
HashTable。
区别对比二(HashTable 和 ConcurrentHashMap 区别):
HashTable 使用的是 Synchronized 关键字修饰,ConcurrentHashMap 是JDK1.7使用了锁分段技术来保证线程安全的。JDK1.8ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。