今天在項目中遇到一個很"奇葩"的問題。狀況大體是這樣的:Android終端和服務器(Spring),徹底相同的字符串鍵值對放入HashMap中居然順序不同,這直接致使了服務器和Android終端用HmacSHA256算法加密出的摘要也不同,服務器也就沒法進行正確的數據驗證。算法
而後帶着鬱悶的心情給程序加斷點進行緣由尋找,發現原來是HashMap的中服務器和終端雙方對於一樣的key存放順序居然不同!服務器
在HashCode產生衝突的狀況下,不一樣的key在HashMap中存入的位置應該是相同的,即便在hashCode產生衝入,若是key-value put的順序相同,其存放的位置也應該是相同的。ide
因此問題就應該出在HashMap上,只能去查看Java和Android關於HashMap的源碼了,發現二者的hashCode()方法居然不同,小小激動了一下,可仔細一看,發現Android只是優化Java中的hashCode()方法,使其更加易於閱讀而已,但所運用的原理仍是同樣的,真是=。=。
具體代碼比較以下:函數
<!-- Android --> @Override public int hashCode() { int hash = hashCode; if (hash == 0) { if (count == 0) { return 0; } final int end = count + offset; final char[] chars = value; for (int i = offset; i < end; ++i) { hash = 31*hash + chars[i]; } hashCode = hash; } return hash; } <!-- Java--> public int hashCode() { int h = hash; int len = count; if (h == 0 && len > 0) { int off = offset; char val[] = value; for (int i = 0; i < len; i++) { h = 31*h + val[off++]; } hash = h; } return h; }
無奈,我只能繼續在源碼裏查看比較,最後發現原來是二者的默認構造函數不同,本質上就是二者的table大小不同,Java中的table默認大小是16×0.75=12(容量×負載因子),而Android中table的默認大小是2,因此即便是一樣的字符串按一樣的順序放入HashMap中它們的key值存放順序也會不同。優化
<!-- Android --> private static final Entry[] EMPTY_TABLE = new HashMapEntry[MINIMUM_CAPACITY >>> 1]; //默認構造函數 public HashMap() { table = (HashMapEntry<K, V>[]) EMPTY_TABLE; threshold = -1; // Forces first put invocation to replace EMPTY_TABLE } <!-- Java --> static final int DEFAULT_INITIAL_CAPACITY = 16; static final float DEFAULT_LOAD_FACTOR = 0.75f; //默認構造函數 public HashMap() { this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR); }
其實仔細讀源碼會發現,在Android中所實現的HashMap類關於"閾值(threshold )"的設定也已經和Java不一樣了,具體請看截取的源碼:this
<!-- Android --> //閾值固定取其table大小的3/4 threshold = (newCapacity >> 1) + (newCapacity >> 2); <!-- Java --> //閾值取容量*負載因子或最大容量+1間的小值 threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
因此總結來看HashMap在不一樣平臺或不一樣語言中的實現細節是不同的,吃一塹,長一智,反正之後切記,牽扯到順序時HashMap真的不適合!加密
做者:XycZero
查看原文:http://www.xyczero.com/blog/article/16/.code