在工作中碰到的一个case 是使用HashSet来记录一些View的索引。在某些情况下,需要读取 HashSet 中的值,进行操作。万恶的bug出现在从读取过程中。伪代码如下:
HashSet viewsId = new HashSet<>();
viewsId.add("imageview-1");
viewsId.add("textView-1");
viewsId.add("imageview-2");
viewsId.add("textView-2");
for (String viewId : viewsId){
//get result
}
期望的result应该是:imageview-1、textView-1、imageview-2、textView-2
实际的顺序是:
问题很明显,Set中的位置顺序并不是按照插入顺序的。修改为List 很容易解决这个问题。
Why?
HashSet 继承自 AbstractSet ,而Set 可以理解成阉割版的Map,这个可以从HashSet源码中发现(每个版本实现可能不一致):
private transient HashMap map;
/**
* Constructs a new, empty set; the backing HashMap instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
而add方法直接调用了HashMap的put。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
.....
modCount++;
addEntry(hash, key, value, i);
return null;
}
可以从上面代码中发现,插入前需要做一次hash来减少冲突,这里使用的hash方法是 著名的Wang/Jenkins hash算法。具体细节可以参考:
https://en.wikipedia.org/wiki/Jenkins_hash_function。
接着使用预哈希完毕之后的值进行一次indexFor()操作。
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
最后进行插入操作:
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
在addEntry时,如果当前table长度大于阈值threshold则会将table增大一倍,而默认的threshold是取值static final int DEFAULT_INITIAL_CAPACITY = 4;之后会再根据当前table的大小做一次indexFor()运算。将这个值作为插入的index。
总结
1.HashMap 中对于数据的存储位置是由hashcode来决定。因此遍历Map时,请注意使用的场景。