摘要:经过大学四年,我也刚刚毕业了3个月了,学习android也有一两年了,可是在大学的生活中,磨练出来的解决实际问题的能力是有的,完成开发任务还是可以的,但是总觉得技术遇到了瓶颈期,没有办法进步很多。因此想静下心来看JDK的源码,看看这些人是怎么设计出来的?为什么要这么设计?我能从中体会到什么?对我今后的职业生涯有什么帮助?跟着优秀的人一起成长吧。
我们用HaspMap最关心的两个方法和几个变量是:
/** 默认的初始化容量,2^4 */
static final int DEFAULT_INITIAL_CAPACITY = 16;
/** 默认的负载因子 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/*数组,Entry是存放Key value的基础bean */
transient Entry[] table;
/** 负载因子*/
final float loadFactor;
/** *下一次的负载table容量 (capacity * load factor) *
int threshold;
public V put(K key,V value)
public V get(Object key)
为了帮助理解和记忆,贴上两张图:
从上图我们可以发现哈希表是由数组+链表组成的,一个长度为 我们看到的第一个变量DEFAULT_INITIAL_CAPACITY=16 的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key.hashCode()%table.length)获得,也就是元素的key的哈希值对数组长度取模得到。
HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。HashMap里面实现一个静态内部类Entry,其重要的属性有
key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
HashMap存取过程
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;
}
看源码得出几个结论:
1.HashMap可以存储null的key。
2.通过key.hashCode找到hash值,然后跟table.length的取模,得到数组下标。然后遍历链表,如果hash值且key值相等,则替换原来的值。
3.table添加新的元素时并没有奇怪,奇怪的是我们知道数据的默认容量是16,超过了16怎么办,HashMap是怎么处理的呢?
所以要看一下上述代码中addEntry这个方法里面到底做什么操作。
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
到这里已经很明显,我们发现了HashMap的处理方式,是size > threshold的时候就会去扩容。那么这个threshold是什么东西呢?文章的开头我们已经把他列出来了,threshold = capacity loadFactor的结果,loadFactor是负载因此,capacity是容量。那么我们可以得出很简单的结论就是首次初始化的时候threshold = DEFAULT_INITIAL_CAPACITY(16)DEFAULT_LOAD_FACTOR (0.75) ,就是超过了容量的75%的时候就会去扩容。
当哈希表的容量超过默认容量时,必须调整table的大小。当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,需要创建一张新表,将原表的映射到新表中。
我带着好奇心继续探索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);
}
/**
* Transfers all entries from current table to newTable.
*/
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;
//重新计算index
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
到这里基本上清楚了HashMap的进行put时候的整个过程了。相同get方法我就不贴代码了。有兴趣可以去研究源码。
当我看源码看到这一步我可能比较困惑,HashMap究竟优势在哪儿?
我们要分析一下HashMap的数据结构,上述我们已经了解HashMap的数据结构是数组加链表。
数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;
链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。
那么哈希表((Hash table)综合这两者的特性
OK了,这酸爽!!!