今天在项目中遇到一个很"奇葩"的问题。状况大体是这样的: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