1、hashMap继承及实现接口。
Map<K,V>,Cloneable , Serializable,Map.Entry,Iterator
2、特点分析(根据其继承类及实现的接口)
(1)Map<K,V>
- collection(存储单值)和Map(存储 双值)是集合框架库的两个顶级接口。
- 以key->value的形式存储数据,key是不重复的,元素位置由key决定。可以通过key去寻找key-value的位置,从而得到value的值,适合做查找工作。
(2)Cloneable
- 可以使用clone方法
(3)Serializable
- 可以被序列化
(4)Map.Entry
- 可以存储key-value具体数值。
(5)Iterator
- 迭代器,只能从前往后进行遍历。
3、插入顺序:hashmap中元素的顺序,不是插入顺序。因为key-value的存储位置是由key决定的。
4、使用场景:由于hashmap查询效率较高,并且可以通过key去查询对应的value。
5、源码分析
(1)底层数据结构
- 数组+链表(V1.7)
- 数组+链表或者数组+红黑树(V1.8)
(2)构造函数: 数组默认初始容量为0,当添加第一个元素时,容量变为16。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final Entry<?,?>[] EMPTY_TABLE = {};
MyHashMap(){
table = EMPTY_TABLE;
threshold = 0.75;
}
(3)hash算法:通过key值,调用hashcode()函数计算key的hashcode。
注意的点:
- 一进入函数需要对key是否为空进行判断,若为空,置其hashcode为0。
- 对通过hashcode()计算出的hash值进行扰动处理,目的是保证其为正数和减低hashcode的重复率,降低hash冲突。
public int hash(K key){
if(key==null){
return 0;
}
//key.hashCode()是一个负数
int h = key.hashCode();
//扰动处理:hashcode会重复的,目的是降低hashcode重复率
h ^=(h>>>20) ^ (h>>>12);// >>>无符号的右移
h = h^(h>>>7)^(h>>>4);
return h;
//h % table.length 原来的,因为位运算符的运算效率高于算数运算符
//前提条件是table.length是2的幂
}
(4)indexOf()方法:计算下标。
参数:h(对key进行计算后的hashcode)
按位与操作:提高计算效率。
public int indexOf(int h){
return h & (table.length - 1);
}
(5)resize()扩容方法:对hashMap进行扩容。
- 初始化容量为16。
- 扩容条件:当前元素个数 >= 容量*加载因子,此时需要扩容。
- 扩容方式:2倍扩容,保证table.length是2的幂,以保证indexOf方法中按位与操作的准确性。
- 扩容后由于数组长度发生改变,需要重新计算index值并且更新节点(采用单链表的头插法)。
/*
* 扩容函数:resize()
* 2倍扩容始终能够保证table.length是2的幂
* 扩容后需要重新计算hash值,重新更新节点。
*/
public void resize(){
int old_length = table.length;
int new_length = 2 * old_length;
Entry<K,V>[] old_table = table;
table = new Entry[new_length];
//将老数组的元素放到新数组中,需要重新计算每一个元素的下标
for(Entry<K,V> e:old_table){
while(e != null){
Entry<K,V> next = e.next;
int index = indexOf(hash(e.key));
//头插法
e.next = table[index];
table[index] = e;//将头结点更新到e
e=next;//e继续往下走
}
}
}
(6)put()方法
- 函数名:put(K key,V value)
- 函数作用:给HashMap中添加元素
- 参数:key,value
- 返回值类型:void
需要注意的点
对key为空的特殊处理,体现在两个地方。
- hash()算法。(上文已说明)
- if 语句判断key是否相等时,首先判断hash值是否相同,如果hash值相同,不能说明他俩就是同一个key,若不同,则一定不是同一个key。其次判断引用是否相同,引用相同说明他俩相同;引用不同的话判断equals里面的属性值是否相同。
if (e.hash == h && (e.key == key || (key != null && key.equals(e.key)))) {...}
以下为put()方法实现及思路。
/*
* 添加函数:(思路如下)
* 1、如果是第一次添加,将数组容量设置为默认容量
* 2、根据当前数组大小,计算哈希后的index值
* 3、如果要添加的key值与已有key值有重复,则替换value值
* 4、如果不重复,先判断是否需要扩容。(size>table.length*0.75时需要扩容)
* 5、头插法插入链表。
*/
public void put(K key,V value){
if (table == EMPTY_TABLE) { // 是否时第一次添加元素
table = new Entry[DEFAULT_CAPCITY];
}
int h = hash(key);
int index = indexOf(h);//计算key值对应的下标
//e.key.equals(key) 比较速率较慢
for (Entry<K, V> e = table[index]; e != null; e = e.next) {//遍历下标下的链表
if (e.hash == h && (e.key == key || (key != null && key.equals(e.key)))) {//如果key值相同,则值替换
e.value = value;
return; //如果key有重复 值替换掉之后方法就退出
}
}
//代码走到着说明key 没有重复 头插法
if (size >= table.length * threshold) {
resize(); //如何扩容:2倍扩容 始终能够保证table.length是2的幂
index = indexOf(hash(key));
}
//添加数据,之前出错时因为没有传入h的值,导致每个插入的节点没有hash值。
table[index] = new Entry<K,V>(key, value, table[index],h);//不相同则头插
size++;
}
(6)remove()删除方法
- 函数名:remove(K key)
- 函数作用:通过指定key删除元素
- 参数:key
- 返回值类型:布尔类型
思路:
- 计算key的hash值和对应的index下标。
- 循环找出与传入key相同的节点。
- 找到后根据所删节点是否为头结点分情况处理。
- 移动pre(指向当前节点前驱)和e(指向当前节点)两个指针的位置。(pre和e初始位置都指向table[index])。
public boolean remove(K key){
if (size <= 0) {
return false;
}
int h = hash(key);
int index = indexOf(h);
Entry<K, V> pre = table[index];
Entry<K, V> e = table[index];
while (e != null) {
Entry<K, V> next = e.next;
//第一个循环进来如果判断相等就成立 删除的的就是头节点 pre == e
if (e.hash == h && (e.key == key || (key != null && key.equals(e.key)))) {
size--;
//如果删除的节点是头节点
if (pre == e) { //判断删除的节点就是头节点
table[index] = next;
} else {
pre.next = next;
}
e.next = null;
e.key = null;
e.value = null; //只有指向空之后才能进行垃圾回收
return true;
}
pre = e;
e = next; //pre 变成了e的前驱
}
return false;
}
6、解决哈希冲突的方法:链地址法。
7、常见问题
(1)为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?
答:保证index一定时小于数组长度。防止数组下标访问越界。
(2)为什么HashMap需要扩容?
答:不是因为没有足够存储空间。扩容之后要重新计算每一个节点对应的index,哈希冲突概率降低。
(3)如果自己指定HashMap的初始容量和加载因子,那么容量的大小,和加载因子的大小对HashMap有什么影响?
- 初始容量:如果我们指定不是2的幂,源码中有函数能够保证将最终数组容量改变到2的幂。向上取整找最近的2的幂。
- 初始容量越小,越容易发生哈希冲突。
- 加载因子 :越大 。对扩容时机有影响。
- 加载因子越大扩容的时机越晚,哈希冲突的几率越大,空间利用率越大。
- 加载因子越小,越大扩容的时机越早,哈希冲突的几率越小,空间利用率越小。
(4)为什么用按位与代替取余?
答:因为位运算的计算效率高于算数计算。
(5)为什么hashmap的容量要保持2的幂?
答:为了用 & (length-1) 代替 % length 。
(6)HashMap中如果添加重复key会怎样?
答:新添加进来的key对应的newvalue代替之前老的value。
8、应用场景
(1)hashMaori查询效率较高,可以通过key值去快速查找value值。
(2)计数。
9、HashTable与HashMap异同
相同点:
- 两者都会根据key重新计算所有元素的存储位置,算法相似,时间空间效率相同。
不同点:
- hashtable默认容量为11,hashmap为16。
- hashtable是在构造函数中初始化并且new对象;hashmap是在put方法中第一次添加数据时。
- hashtable中计算hash方法,hash(),hashSeed^k.hashCode(),没有扰动处理,index重复率会高。
- 计算index下标的方法:(hash & 0x7FFFFFFF)%table.length;保证hash值不为负数,保证index下标不会大于table.length。
- 扩容方式:hashTable是2倍加1,保证结果都是奇数,因为取余之后的结果相比于偶数更分散。tableMap是2倍扩容。
- 数据遍历方式:hashTable可以使用Enumerate和Iterator进行遍历;使用iterator进行遍历时支持快速失败机制。
- 是否是线程安全:hashTable是线程安全的。但并不是绝对的线程安全。由于hashTable效率低已被淘汰,索引多线程环境下会选择ConcurrentHashMap。
- 是否接受值为null的Key 或Value:hashtable中值和value都不能为空。hashmap中key和value都可以为空。