//预先了解
HashMap中元素的是以Entry的形式存在的,他是一个key value的封装类
HashMap中的逻辑结构:链表数组??可以把他看做是一个数组,数组的每一个元素,可以是空,可以是单个元素,也可以是一个(单向)链表(的一头)逻辑结构基本是这样的:
在插入元素的时候,首先要计算一下hash值,估计一下在数组层面,这个keyvalue得放在那个位置,然后判断这个位置上是不是“已经有人了”,如果有,则判断有没有重复的人,重复的替代,没重复的话--->将自己包装成一个Entry,然后占去数组上的位置,让自己的next指针指向原先此处的元素。
首先跟着注释把流程先过一遍,然后再仔细看里面调用到的方法
public V put(K key, V value) {
// 先判断键值为空的特殊情况,因为hashmap中的key=null是放在第一个的。
if (key == null)
return putForNullKey(value);
// 将key原先的hashCode处理
int hash = hash(key.hashCode());
// 根据这个key的hash,以及当前map的数组长度,计算出这个Entry该放在数组的何处
int i = indexFor(hash, table.length);
// 找到数组中对应位置的元素,并把引用给e,即Entry e=table[i]
// ,然后从这个Entry开始,顺着他的next链走下去,直到尽头或者找到与这个key的hash一样的Entry(替换他)
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
// 此处就是在迭代,判断是否与之前的key一样的
// 先判断hash是否一样,如果hash一样了再判断是否可以替代
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
// 这个方法是空的
e.recordAccess(this);
// 将被替代的Entry中的值返回
return oldValue;
}
}
// 此处的执行情况--->找遍了map,没找到可以替代的,那么准备往map中加入这个KV
// 先记录一下map的操作数又增加了一次
modCount++;
// 然后将这个KV封装成Entry加入
addEntry(hash, key, value, i);
// 此时没有被替代的,则返回空
return null;
}
// 再来看下是怎么处理key为空值的
// 对于key值为空,value为某值的键值对
private V putForNullKey(V value) {
// 还是一样,遍历一下数组层面0 下标索引出的Entry,(null与其他的没什么区别,只不过是规定了放在第一个罢了)
for (Entry<K, V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 还是遍历完没有找到的情况,等到后面一起说
modCount++;
addEntry(0, null, value, 0);
return null;
}
hash()这个方法其实我也不是很懂这里,我是觉得这样处理方便后面的求index什么的
//map规定长度为2的次幂,长度减一,二进制就全是1,这样按位与得到一个位置,计算快速,
static int indexFor(int h, int length) {
return h & (length-1);
}
// 1.将keyvalue封装成entry,存到对应的位置
// 2.检查是否需要扩充容量
void addEntry(int hash, K key, V value, int bucketIndex) {
// 数组上面放置的这个entry对象,也就是要被替换的那个 ,这里先这个地方存一下他
Entry<K, V> e = table[bucketIndex];
// 然后在这个位置上放上新封装好的entry,这个新的entry在初始化的时候就设置好了他的next指针指向的是这个被替代的entry
table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
// 检查下当前的size是否超过了阈值,如果超过了,还得reszize
if (size++ >= threshold)
resize(2 * table.length);
}
// resize传入的方法是原先的size*2,说明大小是扩充了一倍
void resize(int newCapacity) {
// 先记录一下之前map中所有的entry数组对象
Entry[] oldTable = table;
// 再记录一下旧的map的长度
int oldCapacity = oldTable.length;
// 如果之前的长度已经到了设置的最大值
if (oldCapacity == MAXIMUM_CAPACITY) {
// 把阈值设为Interget.MAX_VALUE,然后返回
// 说的就是,扩充系数是0.75原先的容量比如说是Interget.MAX_VALUE,那么阈值为Interget。MAX_VALUE*0.75,现在已经达到了,还想扩大,那是不可能的了,
// 只能把阈值调高,调到跟最大值一样高,这样下次再插入元素的时候,不会再过来判断了,因为反正我是不可能再给你扩充容量的了,再来多的元素,效率低那我也没办法了
threshold = Integer.MAX_VALUE;
return;
}
// 新建一个数组,新的空间用于存放旧的entry
Entry[] newTable = new Entry[newCapacity];
// 又执行一下这个方法,
transfer(newTable);
table = newTable;
// 然后将阈值调整一下,调整为当前容量*加载系数,也就是上面说的
threshold = (int) (newCapacity * loadFactor);
}
// 将原先所有的元素放入到这个新的数组中</span>
void transfer(Entry[] newTable) {
// 用src(source)记录原来的table
Entry[] src = table;
int newCapacity = newTable.length;
// 遍历原来table数组中的元素
for (int j = 0; j < src.length; j++) {
// 将这个位置上的元素给记录一下
Entry<K, V> e = src[j];
// 如果这个位置上是有元素的
if (e != null) {
// 原先的这个元素置空??
src[j] = null;
do {
// 先记录一下e所指向的元素
Entry<K, V> next = e.next;
// 计算一下e应该放在何处
int i = indexFor(e.hash, newCapacity);
// 如果e要放置的那个位置是有元素的,那么就得替换,把e放上去,然后指向原先的那个,所以在替换前得先记录下一下
// 因为这里是在原先的map上进行重调整,所以不需要之前put时进行的查找重复key的操作
e.next = newTable[i];
// 记录好了后再把这个e放到要放的位置
newTable[i] = e;
// 然后把e的指针设置成原本的,刚才被替换掉的元素
e = next;
// 由于长度扩充了,所以e指向的元素也不一定还在数组的这个位置,所以要一步步迭代下去,知道这个链子走完了
// dowhile中可能有点绕,其他他就做了2件事:
// 1.e过去,e的next对应的是要过去的那个地方原先的元素
// 2.对e原先指向的元素(也就是e这条链上的元素)进行迭代,这条链完了,再去下一个元素的链
} while (e != null);
}
}
}
总结一下hashMap的好处:
1.数组的优点,指定角标可以直接查询,查找速度快。
2.链表的优点:修改的时候改变一下指针即可,修改速度快。
3.hashMap中既有数组的特点又有链表的特点,利用元素的hash来确定元素在的大致位置,极大减少了查找的时间。然后再通过链表的性质进行修改,也免去了数组的重复移位。
注意点:
1.当大量的元素,他的key都存放到了数组的某一个位置的时候,这个时候数组里只有一个元素,这个元素对应这个一条长长的链表,这个时候map的性能极差,就相当于是一个单链表,在jdk1.8中引用了红黑树缓解了这个问题。
2.在刚才的源码分析中,容量扩充resize是极其复杂的,需要迭代所有的元素。所以在使用的时候,最好在初始化的时候就指定好hashmap的长度,这样就没必要进行resize,性能也就提高了啦