严格意义上来说,hashmap的时间复杂度并不是O(1),具体原因看get函数;此外,当在hashmap里面存的内容过多时,重新resize hashmap的长度,这也是一个耗时的时间。
首先看看hashmap的代码:
static final int DEFAULT_INITIAL_CAPACITY = 16这个是hasmmap默认的大小。
static final int MAXIMUM_CAPACITY =1 << 30 hashmap最大的容量,也就是2的幂30。
static final floatDEFAULT_LOAD_FACTOR = 0.75f 负载因子,这里取了0.75似乎java的工程师认为,100个盒子在用了75个的状态下,既能保证get的速度,将collision降的比较低,又不会浪费太多的盒子,文献上说,这个值大概在2/3左右即可。
transient Entry[] table 这个值可以认为是一个队列,每一个entry后面都会跟着一个链表,最后hashmap逻辑样子便是:
transient int size key的数量。
下面开始说说hashmap的函数
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12)
return h ^ (h >>> 7) ^ (h >>> 4)
}
static int indexFor(int h, int length) {
return h & (length-1) 由于length是2的倍数,减去1后全是1
}
通过以上两个算法,算出了一个object在hash中的位置。有了以上的基础,看看hashmap的get和put到底是什么逻辑:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
首先通过indexfor取到在这个table[]entry中的位置,然后开始按照这个链表循环依次往下找,
对于put:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
这里存在两种情况:如果找到,则将原来的值替换,换成新的值;如果没有,则调用addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
这里首先用e找到在buket(木桶)table[]的位置,然后调用了在hashmap里的一个新的类构造函数enrty: */
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
将新的值作为链表的首地址,并把n 作为后面的next保存起来,最后将引用丢给table[bucketIndex],这样变完成了put。
可是当put的值越来来越多,当size的大小超过threshold的时候(也就是认为100个位置放得东西太多了,有可能会indexfor散列到同一个位置,不方便get)便需要resize一下,这里变有可能造成一个死循环的问题,首先看看resize的函数:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
这里调用最后一个函数transfer,于是,在并发的情况下,循环产生了:
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
首先用src指向了原来的木桶数组,对其进行循环,如src[j]不为空,将它置为空,并对e进行循环(此处的e维持着一个经过IndexFor后算出的索引位置都在j上的一个链表)
在这里重新计算这个值得hash(因为长度变化了,所以新的位置有可能和原来的位置不一样,这也是为什么hashmap不能维持存储的信息的顺序的问题)然后将这个e的next指向了新的木桶位置,原来的木桶放得东西放在了这个e的后面。
比如在newTable[i]原来只有一个object a 现在有两个线程需要对src里的一个object b进行transfer,两个线程都算出要把它放在i的位置,在第一个线程已经完成的情况下(即现在newTable[i]现在的object是b-àa)第二个线程执行到e.next = newTable[i]
这里的e.next是b,它又指向了b,结果造成了b指向自己的问题,于是死循环诞生了,归根结底是因为
e.next = newTable[i]
newTable[i] = e 这两行代码的问题。
参考的资料http://en.wikipedia.org/wiki/Hash_table