HashMap源码分析(一)

来源:互联网 发布:中国未来国运如何 知乎 编辑:程序博客网 时间:2024/06/09 15:09

//预先了解

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指针指向的是这个被替代的entrytable[bucketIndex] = new Entry<K, V>(hash, key, value, e);// 检查下当前的size是否超过了阈值,如果超过了,还得reszizeif (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;}// 新建一个数组,新的空间用于存放旧的entryEntry[] newTable = new Entry[newCapacity];// 又执行一下这个方法,transfer(newTable);table = newTable;// 然后将阈值调整一下,调整为当前容量*加载系数,也就是上面说的threshold = (int) (newCapacity * loadFactor);}// 将原先所有的元素放入到这个新的数组中</span>void transfer(Entry[] newTable) {// 用src(source)记录原来的tableEntry[] 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,性能也就提高了啦

0 0
原创粉丝点击