Java1.8-IdentityHashMap源碼解析

一. 概述

??IdentityHashMap利用Hash表來(lái)實(shí)現(xiàn)Map接口席揽,比較鍵(和值)時(shí)使用引用相等性代替對(duì)象相等性谓厘,也就是說(shuō)使用 == 而不是使用 equals幌羞。

  1. 比如對(duì)于要保存的key,k1和k2竟稳,當(dāng)且僅當(dāng)k1== k2的時(shí)候属桦,IdentityHashMap才會(huì)相等熊痴,而對(duì)于HashMap來(lái)說(shuō),相等的條件則是:(k1==null ? k2==null : k1.equals(k2))聂宾。
  2. IdentityHashMap不是Map的通用實(shí)現(xiàn)果善,它有意違反了Map的常規(guī)協(xié)定。并且IdentityHashMap允許key和value都為null系谐。
  3. 同HashMap巾陕,IdentityHashMap也是無(wú)序的,并且該類(lèi)不是線(xiàn)程安全的蔚鸥,如果要使之線(xiàn)程安全惜论,可以調(diào)用Collections.synchronizedMap(new IdentityHashMap(...))方法來(lái)實(shí)現(xiàn)许赃。

我們?cè)偻ㄟ^(guò)一個(gè)例子來(lái)幫我們進(jìn)行分析:

public static void main(String[] args) {
    IdentityHashMap<String ,Integer> identityHashMap = new IdentityHashMap<>();
    identityHashMap.put(new String("A"), 1);
    identityHashMap.put(new String("B"), 2);
    identityHashMap.put(new String("A"), 3);
    System.out.println(identityHashMap);
}

打又古纭:

{B=2, A=1, A=3}
1. 屬性
public class IdentityHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, java.io.Serializable, Cloneable
{
    /**
     * 默認(rèn)容量大小
     */
    private static final int DEFAULT_CAPACITY = 32;

    /**
     * 最小容量
     */
    private static final int MINIMUM_CAPACITY = 4;

    /**
     * 最大容量
     * 事實(shí)上,該Map最多只能有MAXIMUM_CAPACITY-1個(gè)元素混聊,因?yàn)樗仨氂幸粋€(gè)key等于null的位置弹谁,
     * 這是為了避免get,put,remove方法的無(wú)限循環(huán)
     */
    private static final int MAXIMUM_CAPACITY = 1 << 29;

    /**
     * 用于存儲(chǔ)實(shí)際元素的數(shù)組
     */
    transient Object[] table; // non-private to simplify nested class access

    /**
     * map的大小
     */
    int size;

    /**
     * 結(jié)構(gòu)性修改
     */
    transient int modCount;

    /**
     * key為null的處理
     */
    static final Object NULL_KEY = new Object();
}

可以看到,IdentityHashMap底層是使用了一個(gè)數(shù)組來(lái)保存數(shù)據(jù)句喜。

2. 方法
put方法
  1. put的時(shí)候先通過(guò)引用是否相等判斷key是不是已經(jīng)在表中存在预愤,如果存在更新oldValue為新的value,如果元素個(gè)數(shù)達(dá)到閾值咳胃,擴(kuò)容處理植康,然后再找合適的位置放置key和value。
  2. put比較有意思的一點(diǎn)就是展懈,在數(shù)組的i索引處存key销睁,而緊挨著i的i+1處存value,并且由于hash方法的原因存崖,key所對(duì)應(yīng)的index全是偶數(shù)冻记,自然i+1就是奇數(shù)了。這也說(shuō)明了另一點(diǎn)来惧,數(shù)組初始化的時(shí)候冗栗,數(shù)組的長(zhǎng)度被定義為默認(rèn)容量的2倍,因?yàn)閿?shù)組元素的每次保存是都占了數(shù)組的兩個(gè)位置供搀。
  3. put的擴(kuò)容條件是當(dāng)存放的數(shù)組達(dá)到數(shù)組長(zhǎng)度的1/3的時(shí)候隅居,就需要擴(kuò)容。
public V put(K key, V value) {
    // key為null處理
    final Object k = maskNull(key);
    // 使用了jdk原先的這種標(biāo)簽來(lái)進(jìn)行循環(huán)跳轉(zhuǎn)
    retryAfterResize: for (;;) {
        final Object[] tab = table;
        final int len = tab.length;
        int i = hash(k, len);

        for (Object item; (item = tab[i]) != null;
             i = nextKeyIndex(i, len)) {
             // 如果key與數(shù)組原有的key相等葛虐,使用新的value代替舊的value胎源,并返回舊的value
            if (item == k) {
                @SuppressWarnings("unchecked")
                    V oldValue = (V) tab[i + 1];
                tab[i + 1] = value;
                return oldValue;
            }
        }
        // size加1
        final int s = size + 1;
        // Use optimized form of 3 * s.
        // Next capacity is len, 2 * current capacity.
        // 如果3*size 大于數(shù)組的length,則進(jìn)行擴(kuò)容挡闰,這里計(jì)算要注意乒融,+的優(yōu)先級(jí)是高于>號(hào)的掰盘,所以先算乘以3,再進(jìn)行比較赞季。
        if (s + (s << 1) > len && resize(len))
            //  擴(kuò)容后重新計(jì)算元素的值愧捕,尋找合適的位置進(jìn)行存放
            continue retryAfterResize;

        modCount++;
        tab[i] = k;
        tab[i + 1] = value;
        // 更新size
        size = s;
        return null;
    }
}
get方法

??get方法則比較簡(jiǎn)單,根據(jù)key獲取數(shù)組的索引申钩,然后對(duì)象比較引用次绘,基本類(lèi)型比較數(shù)據(jù)是否相等即可。如果i處找到對(duì)象撒遣,說(shuō)明i+1處就是索引對(duì)應(yīng)的value邮偎,這也解釋了初始化數(shù)組的時(shí)候2倍長(zhǎng)度的原因了。

public V get(Object key) {
    Object k = maskNull(key);
    Object[] tab = table;
    int len = tab.length;
    // 獲取元素的index
    int i = hash(k, len);
    while (true) {
        Object item = tab[i];
        // 判斷是否相等义黎,引用是否相等
        if (item == k)
            // 獲取下一個(gè)索引位置的value
            return (V) tab[i + 1];
        // 獲取不到禾进,返回null
        if (item == null)
            return null;
        // 遍歷下一個(gè)key的索引,這里解決散列沖突的辦法是若沖突廉涕,則往后尋找空閑區(qū)域
        i = nextKeyIndex(i, len);
    }
}
nextKeyIndex方法

??該方法用于解決hash沖突時(shí)泻云,取下一個(gè)位置進(jìn)行判斷,put的時(shí)候也是同樣的做法狐蜕。至于往后移2個(gè)單位宠纯,也是因?yàn)閜ut的時(shí)候一個(gè)元素的key和value各占一個(gè)位置嘍。

private static int nextKeyIndex(int i, int len) {
        // 往后移兩個(gè)單位
        return (i + 2 < len ? i + 2 : 0);
    }
containsValue方法

從循環(huán)遍歷來(lái)看层释,也能得出數(shù)組偶數(shù)索引處存的全是元素的key婆瓜,而奇數(shù)位置存的是元素的value。

public boolean containsValue(Object value) {
        Object[] tab = table;
        for (int i = 1; i < tab.length; i += 2)
            if (tab[i] == value && tab[i - 1] != null)
                return true;

        return false;
    }
capacity方法
/**
 * 該值計(jì)算的是最小的大于expectedMaxSize的二次冪的值贡羔,
 * 比如expectedMaxSize是5廉白,返回的就是8
 */
private static int capacity(int expectedMaxSize) {
    // assert expectedMaxSize >= 0;
    return
        (expectedMaxSize > MAXIMUM_CAPACITY / 3) ? MAXIMUM_CAPACITY :
        (expectedMaxSize <= 2 * MINIMUM_CAPACITY / 3) ? MINIMUM_CAPACITY :
        Integer.highestOneBit(expectedMaxSize + (expectedMaxSize << 1));
}
remove方法
public V remove(Object key) {
    Object k = maskNull(key);
    Object[] tab = table;
    int len = tab.length;
    // 計(jì)算hash
    int i = hash(k, len);

    while (true) {
        Object item = tab[i];
        // 查找引用相等的
        if (item == k) {
            modCount++;
            size--;
            @SuppressWarnings("unchecked")
                V oldValue = (V) tab[i + 1];
            tab[i + 1] = null;
            tab[i] = null;
            // 刪除該元素后,需要把原來(lái)有沖突往后移的元素移到前面來(lái)
            closeDeletion(i);
            return oldValue;
        }
        if (item == null)
            return null;
        i = nextKeyIndex(i, len);
    }
}
resize方法
private boolean resize(int newCapacity) {
    // assert (newCapacity & -newCapacity) == newCapacity; // power of 2
    int newLength = newCapacity * 2;
    // 舊的數(shù)組
    Object[] oldTable = table;
    int oldLength = oldTable.length;
    // 舊數(shù)組的長(zhǎng)度如果是最大值的2倍
    if (oldLength == 2 * MAXIMUM_CAPACITY) { // can't expand any further
        // 并且如果原先元素的個(gè)數(shù)是最大容量-1治力,無(wú)法擴(kuò)容蒙秒,拋異常
        if (size == MAXIMUM_CAPACITY - 1)
            throw new IllegalStateException("Capacity exhausted.");
        return false;
    }
    // 如果舊的數(shù)組容量大于新數(shù)組容量
    if (oldLength >= newLength)
        return false;
    // 新數(shù)組
    Object[] newTable = new Object[newLength];
    // 遍歷舊數(shù)組
    for (int j = 0; j < oldLength; j += 2) {
        Object key = oldTable[j];
        // 舊數(shù)組的key不為null,重新獲取index后放到新的數(shù)組里
        if (key != null) {
            Object value = oldTable[j+1];
            oldTable[j] = null;
            oldTable[j+1] = null;
            int i = hash(key, newLength);
            while (newTable[i] != null)
                i = nextKeyIndex(i, newLength);
            newTable[i] = key;
            newTable[i + 1] = value;
        }
    }
    // 將新數(shù)組重新指向table
    table = newTable;
    return true;
}
equals和hashCode方法
  1. IdentityHashMap重寫(xiě)了equals和hashcode方法宵统,不過(guò)需要注意的是hashCode方法并不是借助Object的hashCode來(lái)實(shí)現(xiàn)的践磅,而是通過(guò)System.identityHashCode方法來(lái)實(shí)現(xiàn)的鸠项。
  2. 并且,hashCode的生成是與key和value都有關(guān)系的,這就間接保證了key和value這對(duì)數(shù)據(jù)具備了唯一的hash值蝌借。同時(shí)通過(guò)重寫(xiě)equals方法羽资,判定只有key值全等情況下才會(huì)判斷key值相等角撞。這就是IdentityHashMap與普通HashMap不同的關(guān)鍵所在哲鸳。
public int hashCode() {
    int result = 0;
    Object[] tab = table;
    for (int i = 0; i < tab.length; i +=2) {
        Object key = tab[i];
        if (key != null) {
            Object k = unmaskNull(key);
            // 最終hashCode的生成與key和value都有關(guān)系
            result += System.identityHashCode(k) ^
                      System.identityHashCode(tab[i + 1]);
        }
    }
    return result;
}
public boolean equals(Object o) {
    if (o == this) {
        return true;
    } else if (o instanceof IdentityHashMap) {
        IdentityHashMap<?,?> m = (IdentityHashMap<?,?>) o;
        if (m.size() != size)
            return false;

        Object[] tab = m.table;
        for (int i = 0; i < tab.length; i+=2) {
            Object k = tab[i];
            if (k != null && !containsMapping(k, tab[i + 1]))
                return false;
        }
        return true;
    } else if (o instanceof Map) {
        Map<?,?> m = (Map<?,?>)o;
        return entrySet().equals(m.entrySet());
    } else {
        return false;  // o is not a Map
    }
}

二、總結(jié)

  1. IdentityHashMap的實(shí)現(xiàn)不同于HashMap涤伐,雖然也是數(shù)組馒胆,不過(guò)IdentityHashMap中沒(méi)有用到鏈表缨称,解決沖突的方式是計(jì)算下一個(gè)有效索引,并且將數(shù)據(jù)key和value緊挨著存在map中祝迂,即table[i]=key睦尽,那么table[i+1]=value。
  2. IdentityHashMap的hash的計(jì)算沒(méi)有使用Object的hashCode方法型雳,而是使用的System.identityHashCode方法当凡,這是Object.hashCode方法根據(jù)對(duì)象的內(nèi)存地址來(lái)計(jì)算散列碼時(shí)所使用的方式。
  3. 其實(shí)有的時(shí)候我們常說(shuō)的IdentityHashMap能保存重復(fù)的key是一種不太恰當(dāng)?shù)恼f(shuō)法纠俭,因?yàn)镮dentityHashMap的==操作是比較的內(nèi)存地址沿量,如果不是指向同一塊內(nèi)存,那這時(shí)候才可以保存相同的數(shù)據(jù)冤荆。
// 像這種情況朴则,就不能保存相同的key
IdentityHashMap<String, Integer> map = new IdentityHashMap<>();
map.put("A", 1);
map.put("A", 3);
System.out.println(map);    

三、問(wèn)題

1)有一個(gè)問(wèn)題沒(méi)想清楚匙赞,就是計(jì)算index的時(shí)候佛掖,返回的結(jié)果一定是偶數(shù)么妖碉?有時(shí)間了再仔細(xì)想想 (代碼如下):

private static int hash(Object x, int length) {
    int h = System.identityHashCode(x);
    // Multiply by -127, and left-shift to use least bit as part of hash
    return ((h << 1) - (h << 8)) & (length - 1);
}

2)還有一個(gè)問(wèn)題涌庭,就是說(shuō) Object.hashCode和System.identityHashCode方法都是native方法,底層的實(shí)現(xiàn)有什么區(qū)別么欧宜?這個(gè)等以后慢慢再想坐榆。

參考自:
Java-IdentityHashMap實(shí)現(xiàn)
JDK1.8源碼分析之IdentityHashMap

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市冗茸,隨后出現(xiàn)的幾起案子席镀,更是在濱河造成了極大的恐慌,老刑警劉巖夏漱,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件豪诲,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡挂绰,警方通過(guò)查閱死者的電腦和手機(jī)屎篱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)葵蒂,“玉大人交播,你說(shuō)我怎么就攤上這事〖叮” “怎么了秦士?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)永高。 經(jīng)常有香客問(wèn)我隧土,道長(zhǎng)提针,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任曹傀,我火速辦了婚禮关贵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘卖毁。我一直安慰自己揖曾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布亥啦。 她就那樣靜靜地躺著炭剪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪翔脱。 梳的紋絲不亂的頭發(fā)上奴拦,一...
    開(kāi)封第一講書(shū)人閱讀 49,792評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音届吁,去河邊找鬼错妖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛疚沐,可吹牛的內(nèi)容都是我干的暂氯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼亮蛔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼痴施!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起究流,我...
    開(kāi)封第一講書(shū)人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤辣吃,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后芬探,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體神得,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年偷仿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了哩簿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炎疆,死狀恐怖卡骂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情形入,我是刑警寧澤全跨,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站亿遂,受9級(jí)特大地震影響浓若,放射性物質(zhì)發(fā)生泄漏渺杉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一挪钓、第九天 我趴在偏房一處隱蔽的房頂上張望是越。 院中可真熱鬧,春花似錦碌上、人聲如沸倚评。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)天梧。三九已至,卻和暖如春霞丧,著一層夾襖步出監(jiān)牢的瞬間呢岗,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工蛹尝, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留后豫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓突那,卻偏偏與公主長(zhǎng)得像挫酿,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子陨收,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348