1. 程式人生 >實用技巧 >其它 >Day18-集合(下)集合原始碼及面試題

Day18-集合(下)集合原始碼及面試題


技術標籤:Java原始碼javahashmap


HashSet原始碼

package com.dream.enum_util_class;

import java.util.EnumMap;
import java.util.Map.Entry;
import java.util.Set;

public class Test02 {

	public static void main(String[] args) {
		
		/**
		 * 
		 * EnumMap工具類
		 * 
		 * 將列舉類中物件放入Map集合中
		 */
		
		EnumMap<Signal, String> map = new EnumMap<>(Signal.class);
		map.put(Signal.RED, "紅燈");
		map.put(Signal.YELLOW, "黃燈");
		map.put(Signal.GREEN, "綠燈");
		
		Set<Entry<Signal,String>> entrySet = map.entrySet();
		for (Entry<Signal, String> entry : entrySet) {
			Signal key = entry.getKey();
			String value = entry.getValue();
			System.out.println(key + " -- " + value);
		}
		
	}
}

理解HashSet底層由HashMap實現

HashMap原始碼

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
    
    //預設長度
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//16
    //預設負載因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //最大長度
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //負載因子
    final float loadFactor;//0.75
    //閾值(容量*負載因子)
    int threshold;//12
    //空例項的陣列
    static final Entry<?,?>[] EMPTY_TABLE = {};
    //存放資料的容器
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//new Entry[16]
    //hash掩碼/hash隨機值
    transient int hashSeed = 0;
    
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    
    public HashMap(int initialCapacity, float loadFactor) {
        //initialCapacity = 16
        //loadFactor = 0.75
        
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))//NAN:not a Number
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }
    
    public V put(K key, V value) {
        //第一次新增元素時進入的判斷
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //判斷key是否為null
        if (key == null)
            return putForNullKey(value);
        //獲取到key的hash值
        int hash = hash(key);
        //通過hash值獲取陣列中的下標
        int i = indexFor(hash, table.length);
        //e = 楊晨的Entry
        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))) {
                //如果key相同,就替換value值,並返回老的value
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
    
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;//16
        //當容量 == 最大容量時
        if (oldCapacity == MAXIMUM_CAPACITY) {
            //閾值 = Integer.MAX_VALUE
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];//new Entry[32];
        //資料的移位:把原來陣列中的資料遷移到新陣列中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        //計算閾值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    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;
            }
        }
    }
    
    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;
    }
    
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //判斷是否擴容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            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++;
    }
    
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
    
    final int hash(Object k) {
        int h = hashSeed;
        //如果key是String型別,則計算hash值的過程中使用到hashSeed
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    
    private void inflateTable(int toSize) {//16
        // 傳入的數字經過計算,都會或得到2的冪
        int capacity = roundUpToPowerOf2(toSize);//capacity = 16

        //threshold = 12
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }
    
    private static int roundUpToPowerOf2(int number) {//16
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }
    
    //獲取的hash掩碼
    final boolean initHashSeedAsNeeded(int capacity) {
        boolean currentAltHashing = hashSeed != 0;
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }
    
    //對映關係類/節點類
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key; ----- key
        V value; --------- value
        Entry<K,V> next; - 下一個節點的地址
        int hash; -------- key的hash值

        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    }
    
}
  1. HashMap的陣列長度為什麼是2的冪?

    陣列長度2的冪會使元素在陣列中分佈的更加均勻

    因為如果不是2的冪,length-1的二進位制有可能某一位出現0,就會導致陣列中某個下標上永遠存不到元素,就會使得空間的浪費

  2. HashMap的擴容機制/擴容倍數?

    HashMap擴容的長度是原來的2倍

  3. HashMap預設負載因子是多少?

    0.75

  4. 為什麼HashMap預設負載因子是0.75

    取得了時間和空間的平衡

    負載因子過小:儲存元素個數較少的時候就擴容,浪費空間

    負載因子過大:儲存元素個數較多的時候就擴容,浪費時間

  5. HashMap的陣列最大長度是多少?

    1<<30

  6. HashMap的陣列最大長度為什麼是1<<30?

    HashMap陣列長度必須是2的冪,並且陣列長度的型別是int

    所以1<<30是int型別取值範圍內能取到最大的2的冪

  7. 什麼是HashMap的hash表

    就是陣列

  8. 什麼是hash碰撞/衝突

    準元素在陣列中的下標上有元素,這就是hash碰撞

  9. 什麼是hash桶

    Hash陣列中的Entry,因為Entry中有個叫做next的屬性,Entry就是一個單向列表

  10. HashMap存放null鍵的位置在哪?

    在陣列中下標為0的位置

  11. HashMap擴容出現的問題/Hash迴環/Hash死迴圈

    在擴容的情況下,Hash桶中Entry的next指向下一個Entry的地址出現重複

    一般Hash迴環出現在多執行緒擴容下,出現這個問題都是程式設計師的責任,因為HashMap明確說明該集合是執行緒不安全,在多執行緒的情況下應該使用ConcurrentHashMap

  12. JDK1.8版本對於HashMap進行了哪些改動?

hash()進行了改進:將hashCode值認為有高16位和低16位,並且將高16位 ^ 低16位,讓hash值分佈更加均勻

JDK1.7:陣列+連結串列的形式,頭插法

JDK1.8:陣列+連結串列+紅黑樹,尾插法

  1. JDK1.8版本HashMap為什麼有黑紅數的改進?

加入紅黑樹使得查詢更有效率

  1. JDK1.8版本HashMap什麼時候從連結串列轉換為紅黑樹?

陣列長度大於64,並且連結串列長度大於8

  1. JDK1.8版本HashMap為什麼連結串列大於8就轉換為紅黑樹?

因為泊松分佈額統計概率學,統計出連結串列大於8的情況以及是及其微小了

經驗:要防止hash碰撞,一定也要重寫hashCode()

TreeSet原始碼

public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>{
    
    private transient NavigableMap<E,Object> m;
    
    private static final Object PRESENT = new Object();
    
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }
    
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    
    TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }
    
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    
}

TreeSet底層由TreeMap實現

TreeMap原始碼

public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>{
    
    private final Comparator<? super K> comparator;
    
    private transient Entry<K,V> root = null;
    
    public TreeMap() {
        comparator = null;
    }
    
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    
    public V put(K key, V value) {
        //t = 麻生希的Entry
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); //目的:1.檢查Key是否為null 2.檢查該類是否實現內建比較器

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        //比較結果
        int cmp;
        Entry<K,V> parent;
        
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {//外接比較器的邏輯
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }else {//內建比較器的邏輯
            
            if (key == null)
                throw new NullPointerException();
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                //椎名空的Entry
                parent = t;
                //4
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        //北島玲的Entry
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        //紅黑樹的制衡
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
    
    final int compare(Object k1, Object k2) {
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
    }
    
    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left = null;
        Entry<K,V> right = null;
        Entry<K,V> parent;
        boolean color = BLACK;
        
        Entry(K key, V value, Entry<K,V> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }
    }
    
}

外接比較器優先於 內建比較器

https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

《演算法導論》中對於紅黑樹的定義如下:

  1. 每個結點或是紅的,或是黑的
  2. 根節點是黑的
  3. 每個葉結點是黑的
  4. 如果一個結點是紅的,則它的兩個兒子都是黑的
  5. 對每個結點,從該結點到其子孫節點的所有路徑上包含相同數目的黑結點