1. 程式人生 > >24、HashSet與HashMap原始碼分析

24、HashSet與HashMap原始碼分析

1、HashSet底層是使用HashMap實現的。當使用add方法將物件新增到set當中時,實際上是將該物件作為底層所維護的Map物件的Key,,而value則都是同一個Object物件(該物件我們用不上);

HashSet部分原始碼:

 private transient HashMap<E,Object> map;
 public HashSet() {
	map = new HashMap<E,Object>();
    }
 public boolean add(E e) {
	return map.put(e, PRESENT)==null;
    }



HashSet的預設構造方法是生成一個HashMap物件,賦給變數map,HashSet的add()方法是向生成的HashMap中放入一個鍵值對,這個鍵值對的鍵key就是我們要存入HashSet的資料,值value是一個常量PRESENT,這個PRESENT是這樣定義的:

 private static final Object PRESENT = new Object();


其他方法的定義:

 public boolean remove(Object o) {
	return map.remove(o)==PRESENT;
    }

 public void clear() {
	map.clear();
    }

 public boolean isEmpty() {
	return map.isEmpty();
    }

 public int size() {
	return map.size();
    }

 public Iterator<E> iterator() {
	return map.keySet().iterator();
    }

HashSet的底層都是藉助於HashMap來實現的。

2、HashMap的原始碼分析(這個很複雜)。先看一下最後面的記憶體映像圖,理解HashMap的儲存形式。

首先HashMap定義了幾個重要的成員變數

static final int DEFAULT_INITIAL_CAPACITY = 16;  //預設初始容量,是一個常量16

static final float DEFAULT_LOAD_FACTOR = 0.75f;  //預設負載因子,常量float 0.75,就是說陣列資料達到陣列容量75%時,擴充套件陣列

transient Entry[] table;   //用來存放資料的陣列,陣列元素為Entry型別的。這是存放Map鍵值對的主體。

int threshold;

transient int size;

final float loadFactor;

HashMap底層維護一個數組(Entry[]),我們向HashMap中所放置的物件實際上是儲存在該陣列中。

再看一下Entry的定義: 

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
	    V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

Entry定義了四個成員變數,key和value分別用來存放鍵值對對映的鍵和值,這裡注意key的定義是final的,也就是說key初始化完畢後就不能改變了,也因此HashMap只提供key的getter方法,value則提供了setter和getter方法。換句話說,如果我的HashMap中儲存了“aaa”鍵和“value1”值這樣一個鍵值對,而我想改成鍵為“bbb”而值不變依然為“value1”,只能通過先刪除aaa鍵值對,再增加“bbb”鍵值對,不能直接修改“aaa”這個鍵。next指向另一個Entry'物件(相當於連結串列的指標),hash為一個final的int變數,也是隻有在初始化時賦值,他是鍵key的hash值,Entry只有一個帶引數的構造方法,所以生成新物件時就確定了key和hash。 

HashMap預設構造方法:

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }


生成一個預設長度(16)的Entry陣列。

往HashMap物件中存放鍵值對方法put():

 public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

對於key==null的鍵值對,呼叫了putForNullKey()方法:

private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }


從這個方法看,好像是key為null的鍵值對是固定儲存在table[0]的。(這裡有疑問)。e.recordAccess()方法不明白意思。最後是呼叫了addEntry(0, null, value, 0);這個方法(應該就是table[0]處,說明key為null的hash值為0,儲存在陣列table第一位):

 void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

這個方法先把bucketIndex處的物件賦值給e(bucketIndex也就是要新增的鍵值對在table陣列的下標,從put方法看,就是i,i = indexFor(hash, table.length);),然後生成一個新的Entry物件,存放在table[bucketIndex]處,而這個新Entry物件的next指向了原來的那個Entry物件,就是e。

put()方法中的for迴圈用來判斷鏈上是否已經存有相同鍵(key)的元素,判斷是否相等,就是要hash值相等並且((k = e.key) == key || key.equals(k)),如果有相同的鍵存在,就修改其值,反之就是沒有相同的鍵,那麼就執行addEntry()方法,新增新的鍵值對。

HashMap在記憶體中的示意映像圖:

HashMap底層維護一個數組(Entry[]),我們向HashMap中所放置的物件實際上是儲存在該陣列中。

當向HashMap中put一對鍵值時,他會根據key的hashCode值計算出一個位置,該位置就是此物件準備往陣列中存放的位置。

如果該位置沒有物件存在就將此物件直接放進陣列當中;如果該位置已經有物件存在了,則順著此存在的物件的鏈開始尋找(Entry類有一個Entry型別的next成員變數,指向了該物件的下一個物件),如果此鏈上有物件的話,再去使用equals方法進行比較,如果對此鏈上的某個物件的equals方法比較為false,則將該物件放到陣列當中,然後將陣列中該位置以前存在的那個物件連線到此物件的後面。


3、Properties類

主要用於一些屬性檔案,key = value形式
Properties的key和value都是String的

獲取環境變數:

import java.util.Iterator;
import java.util.Properties;
import java.util.Set;

public class PropertiesTest
{
	public static void main(String[] args)
	{
		Properties p = System.getProperties();
		
		Set set = p.keySet();
		
		for(Iterator iter = set.iterator();iter.hasNext();)
		{
			String key = (String)iter.next();
			String value = p.getProperty(key);
			
			System.out.println(key + "=" + value);
		}
	}
}