[url]http://www.hetaoblog.com/%E8%AF%B4%E4%B8%80%E8%AF%B4java%E9%87%8C%E9%9D%A2%E7%9A%84hashcode4-hashmap%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%901/[/url]
现在进入hashcode第4篇,HashMap代码分析,
先看到这个类的成员变量,有下面8个,
static final int DEFAULT_INITIAL_CAPACITY = 16; // 默认的容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量, 取到2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认的load factor,
transient Entry[] table; // 这里,用一个数组存储了真正的Entry,Entry里面有成员k和v
transient int size; // 大小
int threshold; // 容量限制
final float loadFactor; // 实际load factor
transient volatile int modCount;
重点:
* 实际的kv数据用一个entry类,存放在一个数组里面
下面看下默认的构造函数
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY]; // 构造Entry表格
init(); // 这个函数是空的,可以忽略
}
下面是put函数,这段函数的主要意思:
1. 因为HashMap允许null(而HashTable不允许), 所以如果是null的话先固定放到第一个
2. 对对象的hashcode()再做一次hash,然后取索引,这里略有点故事,一般hash表的数组长度应该是质数,而jdk里面的HashMap选了2的指数作为数组大小,这里要做一次保护,这个下次可以单独写一点
3. 接下来,取到这个哈希数组里面的索引这项,然后对这项上的链表进行遍历,看看有没有存在一个key,和新放进来的元素hash值一样,并且(是同一个元素或者equals方法也返回相等)(这就是为什么Object.hashcode()里面会有那样的约定,如果你违法了那边的约定,假设两个对象equals返回true,本质上是同一个对象,但是hashcode不一样的话,那两次put会放到两个不同的地方了;
如果有的话,说明是同一个key对象,用新的value替换老的value,并且返回老的value
4. 如果不存在同一个key,那么就把该元素加入到这个哈希数组的这个位置(就是通过hashcode()再做hash,然后取索引的这项)
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 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 e = table[bucketIndex];
table[bucketIndex] = new Entry(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
以及Entry的实现和构造函数:
static class Entry implements Map.Entry {
final K key;
V value;
Entry next;
final int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry n) {
value = v;
next = n;
key = k;
hash = h;
}
可以看到,
1. 新添加的元素被放到了哈希数组这个位置上面的链表的第一个;
2. 如果HashMap里面存放的总的元素个数超过了threshold(也就是capacity*loadfactor), 那么就把存放数据的数组扩大一倍
这里面是说总的元素个数,下面的代码是会打印2的。其实就是已经存了比较多的数据了,基本上直接hash一次访问到的概率比较小了,很可能很多元素都要继续通过访问后面的Entry链表来得到,效率就下降了,Hash的优势已经不那么明显了,所以把表格扩大一倍,把原来的元素根据hash值重新做一次索引和分布,尽可能让链表缩短,提高访问效率
Map m = new HashMap();
m.put("BB", 10);
m.put("Aa", 11);
System.out.println(m.size());
现在进入hashcode第4篇,HashMap代码分析,
先看到这个类的成员变量,有下面8个,
static final int DEFAULT_INITIAL_CAPACITY = 16; // 默认的容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量, 取到2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认的load factor,
transient Entry[] table; // 这里,用一个数组存储了真正的Entry,Entry里面有成员k和v
transient int size; // 大小
int threshold; // 容量限制
final float loadFactor; // 实际load factor
transient volatile int modCount;
重点:
* 实际的kv数据用一个entry类,存放在一个数组里面
下面看下默认的构造函数
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY]; // 构造Entry表格
init(); // 这个函数是空的,可以忽略
}
下面是put函数,这段函数的主要意思:
1. 因为HashMap允许null(而HashTable不允许), 所以如果是null的话先固定放到第一个
2. 对对象的hashcode()再做一次hash,然后取索引,这里略有点故事,一般hash表的数组长度应该是质数,而jdk里面的HashMap选了2的指数作为数组大小,这里要做一次保护,这个下次可以单独写一点
3. 接下来,取到这个哈希数组里面的索引这项,然后对这项上的链表进行遍历,看看有没有存在一个key,和新放进来的元素hash值一样,并且(是同一个元素或者equals方法也返回相等)(这就是为什么Object.hashcode()里面会有那样的约定,如果你违法了那边的约定,假设两个对象equals返回true,本质上是同一个对象,但是hashcode不一样的话,那两次put会放到两个不同的地方了;
如果有的话,说明是同一个key对象,用新的value替换老的value,并且返回老的value
4. 如果不存在同一个key,那么就把该元素加入到这个哈希数组的这个位置(就是通过hashcode()再做hash,然后取索引的这项)
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 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 e = table[bucketIndex];
table[bucketIndex] = new Entry(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
以及Entry的实现和构造函数:
static class Entry implements Map.Entry {
final K key;
V value;
Entry next;
final int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry n) {
value = v;
next = n;
key = k;
hash = h;
}
可以看到,
1. 新添加的元素被放到了哈希数组这个位置上面的链表的第一个;
2. 如果HashMap里面存放的总的元素个数超过了threshold(也就是capacity*loadfactor), 那么就把存放数据的数组扩大一倍
这里面是说总的元素个数,下面的代码是会打印2的。其实就是已经存了比较多的数据了,基本上直接hash一次访问到的概率比较小了,很可能很多元素都要继续通过访问后面的Entry链表来得到,效率就下降了,Hash的优势已经不那么明显了,所以把表格扩大一倍,把原来的元素根据hash值重新做一次索引和分布,尽可能让链表缩短,提高访问效率
Map m = new HashMap();
m.put("BB", 10);
m.put("Aa", 11);
System.out.println(m.size());