HashMap是大家在经常使用的一个容器,那么他在1.7环境下到底是怎么实现的,我们一起来看一下
1、HashMap构造方法
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
void init() {
我们可以看到,在你调用空参的构造函数的时候,他会自动调用2个参数的构造方法,其中DEFAULT_INITIAL_CAPACITY是默认的数组初始值,大小是1 << 4 = 16,DEFAULT_LOAD_FACTOR是默认的加载因子,与扩容有关,默认值是0.75f。在构造方法中回进行一些赋值操作,包括阈值和加载因子,至于init()方法,他在1.7中是一个空方法。在1.8中会有实现。
2.put方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);//初始化
}
if (key == null)//key为空的时候进行操作
return putForNullKey(value);
int hash = hash(key);//得到hash值,进行一些异或操作(不同为1,相同为0)
int i = indexFor(hash, table.length);//得到数组下标
for (Entry<K,V> e = table[i]; e != null; e = e.next) {//从table[i]的头节点开始循环,遍历这个索引上的链表,寻找是否有key相同的值
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//判断key和hash是否相等
V oldValue = e.value;//如果相等,得到未被覆盖时候的值
e.value = value;//覆盖原有值
e.recordAccess(this);//hashmap中未被实现,空方法
return oldValue;//返回旧值
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
我们能看到,当table为空的时候,他会进行table的初始化,调用inflateTable(threshold)方法。threshold是一个阈值,初始化的时候被赋值为DEFAULT_INITIAL_CAPACITY,也就是16
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize找到一个大于toSize的2的幂次方的数
int capacity = roundUpToPowerOf2(toSize);//处理toSize
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];//初始化数组
initHashSeedAsNeeded(capacity);
}
其中,他roundUpToPowerOf2(toSize)方法,如果toSize大于最大的数组值,就将他赋值为MAXIMUM_CAPACITY,否则执行(number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1,
Integer.highestOneBit((number - 1) << 1),这个方法内部是对number - 1进行一些右移操作,得到一个小于number - 1的2的幂次方数,至于为什么是number - 1呢,因为如果number刚好是一个2的幂次方数的话,减一是为了得到他本身而不是得到比他小的一个2的幂次方。最后对这个幂次方数左移,一位也就是乘以二,返回一个大于等于toSize的2的幂次方数。最后得到的capacity 是初始化数组的大小。然后重新给threshold赋值,最后初始化数组。至于initHashSeedAsNeeded(capacity)是为了看一下capacity是否需要重置一下哈希种子,是为了得到的哈希值更加散列。
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
然后判断一下key是否为null,当key为空时候会有一个单独的逻辑去处理,也就是putForNullKey(value)方法。
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {//遍历索引为0的位置上的链表
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);//key为null的时候放在数组的第0个位置
return null;
}
这个方法是逻辑比较简单的,就是遍历table[0]位置上的链表,如果找到key为null的Entry,就会覆盖原来的value,将原来的value返回。如果没有,直接执行addEntry(0, null, value, 0)方法,然后返回null。至于addEntry方法时区判断是否需要扩容。我们待会研究扩容的逻辑。
继续put方法的逻辑,他内部会使用一些异或操作得到key的hash值,再使用hash值和table的长度得到存储位置的下标,再然后就去遍历这个存储位置下的链表,如果找到key值相同的entry,就覆盖value,并且返回oldValue,没找到,执行addEntry方法。
void addEntry(int hash, K key, V value, int bucketIndex) {
//threshold = table.length(默认16) * loadFactor(默认0.75)
//只有当size >= threshold,且当前数组索引的位置有元素的时候才扩容,如果没有元素就不会扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//扩容,原来数组的2倍
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);//得到下标
}
createEntry(hash, key, value, bucketIndex);
}
至于这个方法,主要逻辑就在于扩容方法,resize方法。至于createEntry方法是比较简单的,不在此细说
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];//得到链表的头节点
table[bucketIndex] = new Entry<>(hash, key, value, e);//头插法,e是下个节点
size++;
}
我们主要来看一下扩容方法,resize(2 * table.length);可看出是扩容为原来的两倍
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, initHashSeedAsNeeded(newCapacity));//转移
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
首先拿为扩容之前的table,如果数组长度等于MAXIMUM_CAPACITY,就不再扩容,否则创建一个新的数组,然后transfer方法转移数据。最后改变他变了指向,重新赋值一下threshold ,下面我们来看一下transfer方法。
void transfer(Entry[] newTable, boolean rehash) {//多线程情况下可能会出线循环链表的问题
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//循环
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
这个方法内部逻辑也比较简单,就是循环旧table,利用key的hash值与新数组来得到在新数组下存储的下标,使用头插法转移到新数组,但是这样的方式在多线程的情况下会出现循环链表的问题,所以并不推荐在循环链表下调用此方法,所以在多线程情况下要么尽量不要去扩容,要么就去使用ConcurrentHashMap这个类去解决。
3.get方法
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
get方法比较简单,首先判断key是否为null,如果为null,调用getForNullKey方法
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {//key为null的时候,在table[0]位置找,因为存的时候,key为null就存在table[0]处
if (e.key == null)
return e.value;//找到,返回值
}
return null;
}
这个方法比较简单,就是直接循环遍历table[0]的位置,因为key为null的entry在put的时候就直接put在table[0]的位置,所以就直接在这个位置上遍历链表去查找。如果不为null,调用getEntry(key)。
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);//计算hash
for (Entry<K,V> e = table[indexFor(hash, table.length)];//table[indexFor(hash, table.length)计算下标
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;//找到就返回entry对象
}
return null;
}
这个方法就是利用key的hash和table的长度去计算key所在的tabel数组的位置,去遍历查找链表,如果找到就返回entry,否则就返回null
总结
以上就是1.7环境下HashMap的部分主要方法的源码,感谢您的观看。