1、hashMap继承及实现接口。
Map<K,V>,Cloneable , Serializable,Map.Entry,Iterator
2、特点分析(根据其继承类及实现的接口)
(1)Map
(2)Cloneable
(3)Serializable
(4)Map.Entry
(5)Iterator
3、插入顺序:hashmap中元素的顺序,不是插入顺序。因为key-value的存储位置是由key决定的。
4、使用场景:由于hashmap查询效率较高,并且可以通过key去查询对应的value。
5、源码分析
(1)底层数据结构
(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。
注意的点:
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进行扩容。
/*
* 扩容函数: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()方法
需要注意的点
对key为空的特殊处理,体现在两个地方。
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()删除方法
思路:
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有什么影响?
(4)为什么用按位与代替取余?
答:因为位运算的计算效率高于算数计算。
(5)为什么hashmap的容量要保持2的幂?
答:为了用 & (length-1) 代替 % length 。
(6)HashMap中如果添加重复key会怎样?
答:新添加进来的key对应的newvalue代替之前老的value。
8、应用场景
(1)hashMaori查询效率较高,可以通过key值去快速查找value值。
(2)计数。
9、HashTable与HashMap异同
相同点:
不同点: