今天在項目中遇到一個很"奇葩"的問題。情況大致是這樣的:Android終端和服務器(Spring),完全相同的字符串鍵值對放入HashMap中竟然順序不一樣,這直接導致了服務器和Android終端用HmacSHA256算法加密出的摘要也不一樣,服務器也就無法進行正確的數據驗證。
然後帶著郁悶的心情給程序加斷點進行原因尋找,發現原來是HashMap的中服務器和終端雙方對於同樣的key存放順序竟然不一樣!
在HashCode產生沖突的情況下,不同的key在HashMap中存入的位置應該是相同的,即使在hashCode產生沖入,如果key-value put的順序相同,其存放的位置也應該是相同的。
所以問題就應該出在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不同了,具體請看截取的源碼:
<!-- Android -->
//阈值固定取其table大小的3/4
threshold = (newCapacity >> 1) + (newCapacity >> 2);
<!-- Java -->
//阈值取容量*負載因子或最大容量+1間的小值
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
所以總結來看HashMap在不同平台或不同語言中的實現細節是不一樣的,吃一塹,長一智,反正以後切記,牽扯到順序時HashMap真的不適合!
更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11