說在前面
本文包含手寫泛型HashMap<K,V>爲簡化版,僅爲理解 HashMap 的 get() 和put() 方法的工作過程,非Java源碼。
get(K key) 原理
- 先計算出key對應的hash值
int hash = key.hashCode(); //此處的 hashCode() 方法爲 Object對象 所屬方法,默認都有 //自定義的類需要覆寫該方法
- 對超出數組範圍的hash值進行處理
hash = (hash >>> 16)^hash;//java內部自做的優化,爲了使hash值更加均衡,減少衝突 int index = hash & (table.length - 1);//對下標進行合理化,以免下標越界 //這樣做可以使index在數組長度範圍內的原因或者一個前提是,這裏的數組的長度一定是2的n次方, //這樣table.length - 1 在二進制情況下,除最高位,其餘低位爲一定是1,用hash與這樣的一個數進行與操作 //即只保留了hash的二進制的低位,就會使hash的範圍一定小於數組長度
- 根據正確的hash值(下標值)找到所在的鏈表的頭結點
Entry<K,V> node = table[index];
- 遍歷鏈表,如果key值相等,返回對應的value值,否則返回null
while(node != null){
if(node.key.equals(key)){
return node.value;
}
node = node.next;
}
-
具體實現 get(K key)
@Override
public V get(K key) {
int hash = key.hashCode();
hash = (hash >>> 16)^hash;//java內部自做的優化,爲了使hash值更加均衡
int index = hash & (table.length - 1);
Entry<K,V> node = table[index];
while(node != null){
if(node.key.equals(key)){
return node.value;
}
node = node.next;
}
return null;
}
put(K key,V value) 原理
- 先計算出key對應的hash值
- 對超出數組範圍的hash值進行處理
- 根據正確的hash值(下標值)找到所在的鏈表的頭結點
- 如果頭結點==null,直接將新結點賦值給數組的該位置
Entry<K,V> newNode = new Entry<>(key,value); table[index] = newNode;
- 否則,遍歷鏈表,找到key相等的節點,並進行value值的替換,返回舊的value值
Entry<K,V> pre = null;//用來追蹤該段鏈表的最後一個結點,爲尾插做準備,如果採用頭插法,則不需要 while(node != null){ if(node.key.equals(key)){ V oldValue = node.value; node.value = value; return oldValue; } pre = node; node = node.next; }
- 如果沒有找到,採用尾插法(1.8)/頭插法(1.7)創建新結點並插入到鏈表中
pre.next = new Entry<>(key,value);
- 將存儲元素數量+1 !!!
- 校驗是否需要擴容(需要全部重新計算hash值,因爲數組長度改變了)
1)擴容原因:爲了減少hash衝突,降低衝突率(插入一個新的key時,會遇到衝突的概率)
2)負載因子=所有key的數量 / 數組的長度
3)衝突率和負載因子成正相關!因此爲了降低衝突率,可改變數組的長度!
4)具體操作:通過計算負載因子並與擴容因子(規定爲0.75)進行比較if((double) size / table.length >= 0.75 ){ resize(); }
private void resize(){
/**
* 1.創造新數組,長度爲原數組的2倍
* 2.遍歷原數組,找到每一條鏈表的頭節點
* 3.遍歷每一條鏈表,新建結點並將節點採用頭插法插入到新數組中
*
*/
Entry<K,V>[] newTable = new Entry[table.length * 2];
for(int i = 0; i < table.length; i++){
Entry<K,V> node = table[i];
while(node != null){
Entry<K,V> newNode = new Entry<>(node.key,node.value);
int hash = node.key.hashCode();
hash = (hash >>> 16) ^ hash;
int index = hash ^ (newTable.length - 1);
//使用頭插,尾插也可以
newNode.next = newTable[index];
newTable[index] = newNode;
node = node.next;
}
}
}
-
具體實現 put(K key,V value)
@Override
public V put(K key, V value) {
int hash = key.hashCode();
hash = (hash >>> 16)^hash;//java內部自做的優化語句,爲了使hash值更加均衡
int index = hash & (table.length - 1);
Entry<K,V> node = table[index];
if(node == null){
Entry<K,V> newNode = new Entry<>(key,value);
table[index] = newNode;
}else{
Entry<K,V> pre = null;
while(node != null){
if(node.key.equals(key)){
V oldValue = node.value;
node.value = value;
return oldValue;
}
pre = node;
node = node.next;
}
pre.next = new Entry<>(key,value);
}
size++;
if((double) size / table.length >= LOAD_FACTOR_THRESHOLD ){
resize();
}
return null;
}
具體實現HashMap<K,V>
package advance_ds.hashmap;
//接口
public interface Map<K,V> {
V get(K key);
V put(K key,V value);
}
/**
* @author Maria
* @program JavaDaily
* @date 2020/3/21 14:51
*/
public class HashMap<K,V> implements Map<K,V> {
//鏈表的節點類
private static class Entry<K,V>{
K key;
V value;
Entry<K,V> next;
public Entry(K key,V value){
this.key = key;
this.value = value;
}
}
//基本存儲方式:數組
private Entry<K,V>[] table = new Entry[16];
//存儲的元素的個數
private int size = 0;
//擴容因子
private static final double LOAD_FACTOR_THRESHOLD = 0.75;
@Override
public V get(K key) {
/**
* 1.先計算出key對應的hash值
* 2.對超出數組範圍的hash值進行處理
* 3.根據正確的hash值(下標值)找到所在的鏈表的頭結點
* 4.遍歷鏈表,如果key值相等,返回對應的value值,否則返回null
*/
int hash = key.hashCode();
hash = (hash >>> 16)^hash;//java內部自做的優化,爲了使hash值更加均衡
int index = hash & (table.length - 1);
Entry<K,V> node = table[index];
while(node != null){
if(node.key.equals(key)){
return node.value;
}
node = node.next;
}
return null;
}
@Override
public V put(K key, V value) {
/**
* 1.先計算出key對應的hash值
* 2.對超出數組範圍的hash值進行處理
* 3.根據正確的hash值(下標值)找到所在的鏈表的頭結點
* 4.如果頭結點==null,直接將新結點賦值給數組的該位置
* 5.否則,遍歷鏈表,找到key相等的節點,並進行value值的替換,返回舊的value值
* 6.如果沒有找到,採用尾插法(1.8)/頭插法(1.7)創建新結點並插入到鏈表中
* 7.將存儲元素數量+1
* 8.校驗是否需要擴容(需要全部重新計算hash值,因爲數組長度改變了)
* 擴容原因:爲了減少hash衝突,衝突率:插入一個新的key時,會遇到衝突的概率
* 負載因子=所有key的數量/數組的長度
* 衝突率和負載因子成正相關!因此爲了降低衝突率,可改變數組的長度!
* 具體操作:通過計算負載因子並與擴容因子進行比較
*/
int hash = key.hashCode();
hash = (hash >>> 16)^hash;//java內部自做的優化語句,爲了使hash值更加均衡
int index = hash & (table.length - 1);
Entry<K,V> node = table[index];
if(node == null){
Entry<K,V> newNode = new Entry<>(key,value);
table[index] = newNode;
}else{
Entry<K,V> pre = null;
while(node != null){
if(node.key.equals(key)){
V oldValue = node.value;
node.value = value;
return oldValue;
}
pre = node;
node = node.next;
}
pre.next = new Entry<>(key,value);
}
size++;
if((double) size / table.length >= LOAD_FACTOR_THRESHOLD ){
resize();
}
return null;
}
private void resize(){
/**
* 1.創造新數組,長度爲原數組的2倍
* 2.遍歷原數組,找到每一條鏈表的頭節點
* 3.遍歷每一條鏈表,新建結點並將節點採用頭插法插入到新數組中
*
*/
Entry<K,V>[] newTable = new Entry[table.length * 2];
for(int i = 0; i < table.length; i++){
Entry<K,V> node = table[i];
while(node != null){
Entry<K,V> newNode = new Entry<>(node.key,node.value);
int hash = node.key.hashCode();
hash = (hash >>> 16) ^ hash;
int index = hash ^ (newTable.length - 1);
//使用頭插,尾插也可以
newNode.next = newTable[index];
newTable[index] = newNode;
node = node.next;
}
}
}
}
寫一個類來測試一下這段代碼
package advance_ds.hashmap;
import java.util.Objects;
/**
* @author Maria
* @program JavaDaily
* @date 2020/3/21 21:29
*/
public class Person {
private String name;
private int age;
private int gender;
//自動生成的
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name) &&
Objects.equals(gender, person.gender);
}
//自動生成的
@Override
public int hashCode() {
return Objects.hash(name, age, gender);
}
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "p1";
p1.age = 18;
p1.gender = 0;
Person p2 = new Person();
p2.name = "p1";
p2.age = 18;
p2.gender = 0;
HashMap<Person,Integer> map = new HashMap<>();
map.put(p1,108);
System.out.println(map.get(p2));//結果爲108,成功取出!因爲key對應的hash相等
}
}