HashMap是jdk提供的最常用的容器之一,jdk 1.7及之前版本,HashMap底層基於數組和單鏈表結構,數組每個元素是一對鍵值對對象,該對象包括hash值,key,value以及單鏈表下一個鍵值對的引用。jdk 1.8對HashMap底層結構做了一些改進,當數組同一位置的鍵值對超過8個,不再以單鏈表形式存儲,而是改爲紅黑樹。進一步提升了性能。
本文基於jdk 1.7,參考源碼,手寫一個簡單的HashMap,實現自動擴容功能、put、get、entrySet方法。
HashMap結構如下圖:
1、HashMap的一些性質
通過閱讀源碼,相較於HashTable,HashMap有以下性質:
- 線程安全:Hashtable線程安全,同步,同步方式是鎖住整個Hashtable,效率相對低下
HashMap線程不安全,非同步,效率相對高 - 父類:Hashtable是Dictionary
HashMap是AbstractMap - null值:Hashtable鍵與值不能爲null
HashMap鍵最多一個null,值可以多個null - 初始容量:HashMap的初始容量爲16,
Hashtable初始容量爲11 - 擴容方式:HashMap擴容時是當前容量翻倍即:capacity2,
Hashtable擴容時是容量翻倍+1即:capacity2+1。 - 計算hash的方法:HashMap計算hash對key的hashcode進行了二次hash,以獲得更好的散列值,然後對table數組長度取摸。
Hashtable計算hash是直接使用key的hashcode對table數組的長度直接進行取模 - 存儲結構:jdk1.8以後,HashMap在鏈表長度超過閾值時,改用數組+紅黑樹的方式存儲
Hashtable採用數組+鏈表方式存儲
這些性質都體現在源碼中,通過代碼,可以有更深刻的認識。
2、手寫HashMap
這裏還是用常規命名MyHashMap作爲我們的HashMap的類名
定義接口 MyMap
public interface MyMap<K, V> {
V put(K k,V v);
V get(K k);
Set<? extends Entry<K, V>> entrySet();
interface Entry<K, V>{
K getKey();
V getValue();
}
}
這裏定義了後面即將去實現的三個方法,和一個內部接口類型。
MyhashMap的一些屬性
public class MyHashMap<K,V> implements MyMap<K,V> {
//默認初始化的容量,16
private static final int DEFAULT_INITIAL_CAPACITY = 1<<4;
//默認初始化的擴容因子
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
//容量
private int initialCapacity;
//擴容因子
private float loadFactor;
//Entry數量,也就是map的長度
int size;
//entry數組
private Node<K,V>[] table;
有兩個常量,一個初始化容量16,一個擴容因子0.75,都與jdk源碼保持一致。兩個變量initialCapacity,loadFactor就是對應的兩個屬性,size是MyHashMap鍵值對個數,table是存放鍵值對的數組。
前面說了HashMap是基於數組+鏈表形式存儲鍵值對,那麼鏈表數據結構就在下面的靜態內部類Node的結構中體現
靜態內部類 Node
static class Node<K,V> implements MyMap.Entry<K,V>{
K key;
V value;
Node<K,V> next;
public Node() {
}
Node(K key, V value, Node<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Node<?, ?> node = (Node<?, ?>) o;
return Objects.equals(key, node.key) &&
Objects.equals(value, node.value);
}
@Override
public int hashCode() {
return Objects.hash(key, value);
}
@Override
public String toString() {
return key+"="+value;
}
}
Node類實現前面定義的MyMap.Entry接口,有三個屬性,key、value以及單鏈表下一個節點的引用,實際上jdk源碼還有一個hash屬性存儲hash值,這裏簡化掉。Node的兩個方法getKey,getValue非常簡單,獲取鍵和值,源碼有個setValue方法,這裏也簡化掉,:)然後就是重寫hashCode方法和equals方法,使得判斷兩個Node相等的依據是key值和value值都相等。
MyHashMap的構造方法
public MyHashMap(int initialCapacity, float loadFactor) {
if (initialCapacity<0) {
throw new IllegalArgumentException("Illegal initial capacity: "+initialCapacity);
}
if (loadFactor <= 0 || Float.isNaN(loadFactor)){
throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
}
this.loadFactor = loadFactor;
this.initialCapacity = initialCapacity;
this.table = new Node[this.initialCapacity];
}
public MyHashMap() {
this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);
}
用戶可以構造自定義初始容量和擴容因子的MyHashMap,如果自定義的數據不合法,拋出運行時異常。使用空構造是使用默認的16和0.75作爲初始容量和擴容因子。非常簡單。
put方法
接下來看put方法
@Override
public V put(K key, V value) {
V oldValue = null;
//是否需要擴容
if (size>=initialCapacity*loadFactor){
//數組容量擴大爲兩倍
expand(2*initialCapacity);
}
//根據key的hash值確定應該放入的數組位置
int index = hash(key)&(initialCapacity-1);
if (table[index]==null){
table[index] = new Node<K,V>(key,value,null);
}else{//遍歷單鏈表
Node<K,V> node = table[index];
Node<K,V> e = node;
while(e!=null){
if (e.key==key||e.key.equals(key)){
oldValue = e.value;
e.value=value;
return oldValue;
}
e = e.next;
}
table[index] = new Node<K,V>(key,value,node);
}
++size;
return oldValue;
}
插入node之前先檢查鍵值對數量是否大於容量*擴容因子,若超過則需先擴容。擴容方法和hash方法後面再看。
put方法返回值爲map中對應key原來的value值,若存在該key,更新其value值,並返回舊值;
若不存在該key,則通過hash取模將鍵值對插入到數組對應index的位置,若該位置有其他node,將要插入的鍵值對插入到單鏈表頭部。size加1.
get方法
獲取對應key值的鍵值對的value值。
@Override
public V get(K key) {
int index = hash(key)&(initialCapacity-1);
Node<K,V> e = table[index];
while(e!=null){
if (e.key==key||e.key.equals(key)){
return e.value;
}
e=e.next;
}
return null;
}
根據key的hash得到數組index,遍歷該位置的單鏈表獲取鍵值對。
entrySet方法
@Override
public Set<Node<K, V>> entrySet() {
Set<Node<K,V>> set = new HashSet<>();
for (Node<K,V> node:table){
while(node!=null){
set.add(node);
node = node.next;
}
}
return set;
}
遍歷數組和鏈表,返回所有鍵值對的集合。
擴容方法 expand
//擴容方法,將舊數組的數據取出來通過put方法放進新數組
private void expand(int i) {
Node<K,V> [] newTable = new Node[i];
initialCapacity = i;
size = 0;
Set<Node<K, V>> set = entrySet();
//替換數組引用
if (newTable.length>0){
table = newTable;
}
for (Node<K,V> node:set){
put(node.key,node.value);
}
}
源碼裏該方法叫resize,通過entrySet方法獲取所有鍵值對的集合,put進新的數組裏面。
hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
}
參考源碼的hash方法。
以上就是完整的MyHashMap類。實現HashMap的基本功能。
測試類:
public class TestMain {
public static void main(String[] args) {
MyHashMap<String, String> myHashMap = new MyHashMap<>();
//put()
for (int i = 1; i <=500 ; i++) {
myHashMap.put("KEY_"+i,"VALUE_"+i);
}
//size
System.out.println("【SIZE】-->"+myHashMap.size);
//get()
System.out.println(myHashMap.get("KEY_444"));
//entrySet()
for (MyHashMap.Node<String, String> entry : myHashMap.entrySet()) {
if(entry.getKey().equals("KEY_333"))
System.out.println(entry);
}
}
}
運行結果: