Map 接口
Map 有鍵和值得概念,一個鍵映射到一個值,Map按鍵存儲和訪問值,鍵不能重複,即一個鍵只會存儲一份,給同一個鍵重複設置,會覆蓋原來的值。
Set 接口
Set接口,表示的數學中的集合概念,即沒有重複的元素集合。
public interface Map<K,V>{
V put(K key ,V value) // 保存鍵值對,如果原來有key,覆蓋,返回原來的值
V get(Object key); // 根據鍵獲取值,沒找到,返回null
V remove(Object key) //根據鍵刪除鍵值對,返回key原來的值,如果不存在,返回null
int size(); // 查看Map中鍵值對的個數
boolean isEmpty(); //是否爲空
boolean containsKey(Object key); //查看是否包含某一個鍵
boolean containsValue(Object value);// 查看是否包含某個值
void putAll(Map<? extends k,?extends V> m); //保存m中的所有鍵值對當前Map
void clear(); //清空所有Map中所有的鍵值對
Set<key> keySet(); //獲取Map中鍵的集合
Collection<V> values(); //獲取Map中的所有值得集合
Set<Map,Entry<K,V> > entrySet; //獲取Map中的所有鍵值對
interface Entry<K,V>{ //嵌套接口,表示一條鍵值對
K getKey(); // 鍵值對的鍵
V getValue(); //鍵值對的值
V setValue(V value);
boolean equals(Object o);
int hashCode();
}
boolean equals(Object o);
int hashCode();
}
實現原理
1.內部組成
HashMap內部的幾個主要的實例變量
transient Entry<K,V>[] table =(Entry<K,V>[]) EMPTY_TABLE; // Entryl類型的數組,稱爲哈希表或哈希桶,其中的每一個元素指向一個單鏈表,鏈表中的每一個節點表示一個鍵值對。
transient int size; // size表示實際鍵值對的個數
int threshold;
final float loadFactor;
Entry 是一個內部類,它的實例變量和構造方法如下:
static class Entry<K,V> implements Map.Entry<K,V>{
final K key; // 鍵
V value; // 值
Entry<K,V> next; //指向下一個Entry 節點
int hash; // 是key 的hash 值,直接存儲是爲了加快計算。
Entry(int h,K k ,V v, Entry<K,V> n){
value = v;
next = n;
key = k;
hash = h;
}
}
table 的初始值爲EMPTY_TABLE,是一個空表,具體定義爲:
static final Entry<?,?>[] EMPTY_TABLE = {};
當添加鍵值對後,table就不是空表了,它會隨着鍵值對的添加進行擴展,擴展的策略類似於ArrayList。添加第一個元素時,默認分配的大小爲16,不過,並不是size大於16時再進行擴展,而是與threshold有關。
threshold 表示閾值,當鍵值對個數size大於等於threshold時考慮進行擴展。
threshold 是怎麼算出來的? 一般而言,threshold 等於table.length 乘以 loadFactor,如果table.length爲16,loadFactor 爲0.75,則threshold爲12.
loadFactor 是負載因子,表示整體上table被佔用的程度,是一個浮點數,默認爲0.75,可以通過構造方法修改。
2.默認構造方法
public HashMap(){
this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);
}
// DEFAULT_INITIAL_CAPACITY爲16,DEFAULT_LOAD_FACTOR爲0.75,默認構造方法調用的構造方法主要代碼爲:
public HashMap(int initialCapacity,int loadFactor){
this.loadFactor = loadFactor;
threshold = initialCapacity;
}
3.保存鍵值對
public V put(K key , V value){
//爲table分配實際空間
if(table == EMPTY_TABLE){
inflateTable(threshold);
}
private void inflateTable(int toSize){}
int capacity = roundUpPowerOf2(toSize);
threshold = (int)Math.min(capacity*loadFactor,MAXIMUN_CAPACITY+1);
table = new Entry[capacity];
}
//判斷key是否爲null.單獨處理
if(key == null){
return putForNullkey(value)
}
int hash = hash(key);
// 基於key自身的hashCode() 又進行了一些位運算,目的是爲了隨機和均勻性
final int hash(Object k){
int h = 0;
h ^=k.hashCode();
h^=(h>>>20) ^ (h >>>12)
return h^(h>>>7) ^(h >>>4);
}
//計算將這個鍵值對放到table的那個位置
int i = indexFor(hash,table.length);
// HashMap中,length爲2的冪次方, h&(length-1)等同於求模運算h%length,找到了保存位置i,table[i]指向一個單鏈表
static int indexFor(int h,int length){
rteurn h & (length -1)
}
//就是在這個鏈表中逐個查找是否已經有這個鍵,出現key相同的情況時,用新值替換舊值
for(Entry<K,V> e = table[i]; e != null;e=e.next ){
Object k;
//先比較hash,爲什麼?因爲hashs是整數,比較的性能一般要比equals高很多,hash不同,就沒有必要調用equals(),整體上提高性能。
if(e.hash == hash && ((k = e.key) == key || key.equals(k))){
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//記錄修改次數,方便在迭代中檢測結構性變化
modCount++;
// 如果沒有找到相同的key,就在給定的位置添加一條
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, k key, V value, int ketIndex ){
//如果空間不夠,擴容
if(size >= threshold && (null != table[ketIndex])){
//擴展策略是乘2
resize(2 * table.length);
hash = (null != key)? hash(key):0;
bucketIndex = indexFor(hash, table.length);
}
//如果空間是夠的
createEntry(hash,key,value,bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex){
Entry<K, V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
//分配一個容量爲原來兩倍的數組,調用transfer方法將原來的鍵值對移植過來,然後更新內部的table變量,以及threshold的值。
void resize(int newCapacity){
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold =(int) Math.min(newCapacity * loadFactor, MAXINUM_CAPACITY + 1);
}
//參數rehash一般爲false.遍歷每個鍵值對,計算新位置,並保存到新位置
void transfer(Entry[] newTable, boolean rehash){
int newCapacity = newTable.length;
for(Entry<K, V> e : table){
while(null != e){
Entry<K, V> next = e.next;
if(rehash){
e.hash = null ==e.key ? 0 :hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
4.實例
1. Map<String, Integer> countMap = new HashMap<>();
2. countMap.put("hello", 1);
3. countMap.put("world", 3);
4. countMap.put("position", 4);
第一行代碼示意圖:
第二行代碼, “hello” 的hash 值爲96207088,模16的結果爲0,所以插入table[0] 指向的鏈表頭部。
第三行代碼 "world"hash值爲111207038,模結果爲14.
第四行代碼 “ position” 模結果爲0
5.查找方法
public V get(Object key){
if(key == null){
return getForNullKey();
}
Entry<K, V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
HashMap 支持key爲null,key爲null 的時間,放在table[0],調用getForNullKey() 獲取值,如果key不爲null,則調用getEntry()獲取鍵值對節點 entry,然後調用節點的getValue()方法獲取值。
final Entry<K, V> geyEntry(Object key){
if(size == 0){
return null;
}
int hash=(key == null) ? 0 : hash(key);
for(Entry<K, V> e = table[indexFor(hash,table.length)]; e != null; e = e.next){
Object k;
if(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k) ) ) )
return e;
}
return null;
}
public boolean containsValue(Object value){
if(value == null)
return containsNullValue();
Entry[] tab = table;
// 從table的第一個鏈表開始,從上到下,從左到右逐個節點進行訪問。
for(int i = 0; i < tab.length; i++){
for(Entry e = tab[i] ; e != null ; e = e.next ){
If(value.equals(e.value)){
return true;
}}}
return false;
}
6.根據鍵刪除鍵值對
public V remove(Object key){
Entry<K, V> e = removeEntryForKey(key);
return(e == null ? null : e.value);
}
final Entry<K, V> removeEntryForKey(Object key){
if(size == 0){
return null;
}
//計算hash,根據hash找到對應的table索引
int hash=(key == null) ? 0 : hash(key);
int i = indexFor(hash,table.length)
// 遍歷table[i],查找待刪除節點,使用變量prev指向前一個節點,next指向後一個節點。e指向當前節點
Entry<K, V> prev = table[i];
Entry<K, V> e = prev;
while(e != null){
Entry<K, V> next = e.next;
Object k;
// 判斷是否找到,依然是先比較hash值,hash相同時,再用equals比較
if(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
modCount++;
//刪除的邏輯就是讓長度減小。然後讓待刪節點的前後節點鏈起來,如果待刪節點是第一個節點,則讓table[i]直接指向後一個節點。
size--;
if(prev == e)
table[i] = next
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
}
7.原理小結
其實HashMap內部有一個哈希表,即數組table,每個元素table[i]指向一個單向鏈表,根據鍵存取值,用鍵算出hash,取模得到數組中索引位置buketIndex,然後操作table[buketIndex]指向單向鏈表。
存取的時候根據鍵的hash值,只有對應鏈表中操作,不會訪問別的鏈表,在對應鏈表操作時也是先比較hash值,如果相同再用equals方法比較。這就要求,相同的對象其hashCode返回值必須相同。
有以下特點:
(1) 根據鍵保存和獲取值得效率都很高,爲O(1);
(2) HashMap 中的鍵值對沒有順序,是隨機的
(3)HashMap 不是線程安全的,HashTable實現原理與HashMap類似,但沒有特別的優化,它內部通過Synchronized實現勒線程安全。
(4)在HashMap 中,鍵和值都可以爲null,而在Hashtable不可以。