热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JDK源代码分析聚集篇-------HashMap(一队不够快,多排几队就是)

众所周知,Map是用来存储键值对的数据的,而且他的好处就是根据键值能够快速定位,技能保持这ArrayList的优势,又能够保持LinkedList的容易删除和增加的优势,那么我们来分析分析他的实
众所周知,Map是用来存储键值对的数据的,而且他的好处就是根据键值能够快速定位,技能保持这ArrayList的优势,又能够保持LinkedList的容易删除和增加的优势,那么我们来分析分析他的实现机理,老规矩,先给出类图:




首先先来分析一下HashMap

static final int DEFAULT_INITIAL_CAPACITY = 16;

最大容量2的15次方+1;
static final int MAXIMUM_CAPACITY = 1 <<30;

默认加载因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f;

内部实现的机制是用具有键值对格式的单个的entry数组实现:

  1. transient Entry[] table;
  2.    static class Entry implements Map.Entry {
  3.         final K key; //键
  4.         V value;    // 值
  5.         Entry next;  //下一个
  6.         final int hash;   //哈西值
  7.         /**
  8.          * Creates new entry.
  9.          */
  10.         Entry(int h, K k, V v, Entry n) {
  11.             value = v;
  12.             next = n;
  13.             key = k;
  14.             hash = h;
  15.         }
  16.         public final K getKey() {
  17.             return key;
  18.         }
  19.         public final V getValue() {
  20.             return value;
  21.         }
  22.         public final V setValue(V newValue) {
  23.         V oldValue = value;
  24.             value = newValue;
  25.             return oldValue;
  26.         }
  27.         public final boolean equals(Object o) {
  28.             if (!(o instanceof Map.Entry))
  29.                 return false;
  30.             Map.Entry e = (Map.Entry)o;
  31.             Object k1 = getKey();
  32.             Object k2 = e.getKey();
  33.             if (k1 == k2 || (k1 != null && k1.equals(k2))) {
  34.                 Object v1 = getValue();
  35.                 Object v2 = e.getValue();
  36.                 if (v1 == v2 || (v1 != null && v1.equals(v2)))
  37.                     return true;
  38.             }
  39.             return false;
  40.         }
  41.         public final int hashCode() {
  42.             return (key==null   ? 0 : key.hashCode()) ^
  43.                    (value==null ? 0 : value.hashCode());
  44.         }
  45.         public final String toString() {
  46.             return getKey() + "=" + getValue();
  47.         }
  48.         void recordAccess(HashMap m) {
  49.         }
  50.         void recordRemoval(HashMap m) {
  51.         }
  52.     }
当前的数组大小:
transient int size;

构造函数初始化:
设置初始化的容积和加载因子

  1. public HashMap(int initialCapacity, float loadFactor) {
  2.         //初始化容积必须大于0
  3.         if (initialCapacity < 0)
  4.             throw new IllegalArgumentException("Illegal initial capacity: " +
  5.                                                initialCapacity);
  6.         //超过最大容积的时候,那么等于最大容积
  7.         if (initialCapacity > MAXIMUM_CAPACITY)
  8.             initialCapacity = MAXIMUM_CAPACITY;
  9.         //初始化加载因子;
  10.         if (loadFactor <= 0 || Float.isNaN(loadFactor))
  11.             throw new IllegalArgumentException("Illegal load factor: " +
  12.                                                loadFactor);
  13.         //找出一个数的平方大于当前给出的初始化容积,比如是初始化是10的话,那么最终的容积就是16
  14.         int capacity = 1;
  15.         while (capacity < initialCapacity)
  16.             capacity <<= 1;
  17.         this.loadFactor = loadFactor;
  18.         threshold = (int)(capacity * loadFactor);
  19.         //用容积来初始化数组大小;size当前还是为0;
  20.         table = new Entry[capacity];
  21.         //初始化动作,可以留给子类实现,模板模式的应用
  22.         init();
  23.     }


 这是一个hashcode的转换算法;他能够把对象的hashcode转换成为小于length-1的整数作为数组的下标;那么势必会出现重合

  1.     static int hash(int h) {
  2.         h ^= (h >>> 20) ^ (h >>> 12);
  3.         return h ^ (h >>> 7) ^ (h >>> 4);
  4.     }
  5.     static int indexFor(int h, int length) {
  6.         return h & (length-1);
  7.     }


我们先看增加方法:

  1.  public V put(K key, V value) {
  2.         //判断键值是否为空;
  3.         if (key == null)
  4.             return putForNullKey(value);
  5.         
  6.         //得到hashcode
  7.         int hash = hash(key.hashCode());
  8.         //得到一个小于数组长度(取的是与操作)的下标
  9.         int i = indexFor(hash, table.length);
  10.         //在数组中找到该下标的entry值;
  11.         //事实上entry也是一个链表,相对于LinkedList来说,他的entry是单向的
  12.         for (Entry e = table[i]; e != null; e = e.next) {
  13.             Object k;
  14.             //如果存在键值是同一对象的entry,那么用新值覆盖旧值,不存在则再往下找,知道末尾
  15.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  16.                 V oldValue = e.value;
  17.                 e.value = value;
  18.                 e.recordAccess(this);
  19.                 return oldValue;
  20.             }
  21.         }
  22.         //增加修改次数
  23.         modCount++;
  24.         //增加这个entry到该下标列表的首部
  25.         addEntry(hash, key, value, i);
  26.         return null;
  27.     }
  28.     void addEntry(int hash, K key, V value, int bucketIndex) {
  29.     Entry e = table[bucketIndex];
  30.         //可见是放在該下标链表的第一位的
  31.         table[bucketIndex] = new Entry(hash, key, value, e);
  32.         //在这里增加了size;
  33.         if (size++ >= threshold)
  34.             resize(2 * table.length);
  35.     }


如果key是null的话:
 那么他可以不用通过hashcode定位数组中的队列下标-_-||事实上他也没有hashcode;规定在0位存放这个链表的头;可见在HashMap中是可以

存放null的key的;但是正因为其没有hashcode那么就只能存放一个元素,而不是像其他一样能存放多个;但是另外我们可见,你可以考虑把使

用的最多的值的键设置成为null,因为他是找到的最快的;
  1. private V putForNullKey(V value) {
  2.         for (Entry e = table[0]; e != null; e = e.next) {
  3.             if (e.key == null) {
  4.                 V oldValue = e.value;
  5.                 e.value = value;
  6.                 e.recordAccess(this);
  7.                 return oldValue;
  8.             }
  9.         }
  10.         modCount++;
  11.         addEntry(0, null, value, 0);
  12.         return null;
  13.     }

上面说了存放了,下面我们再来看看如何取出来把:

  1. final Entry getEntry(Object key) {
  2.         //经过算法得到这个key对应的hashcode,可见hashcode是固定的对应,而不是随机的;如果是null的话为0,经过与操作还是0,直接 
  3.        //定位到table[0]否则查找出下标
  4.        int hash = (key == null) ? 0 : hash(key.hashCode());
  5.         for (Entry e = table[indexFor(hash, table.length)];
  6.              e != null;
  7.              e = e.next) {
  8.             Object k;
  9.      //匹配这个key的hashcode和数值,可见不是同一的值其hashcode是可能一样的,否则如果是一一对应则没必要匹配key了
  10.             if (e.hash == hash &&
  11.                 ((k = e.key) == key || (key != null && key.equals(k))))
  12.                 return e;
  13.         } 
  14.     //没有找到,为空;
  15.        return null;

我们再来看看其容量是如何增长的:
  1.     public void putAll(Map m) {
  2.         //得到新增加进来的元素的个数
  3.         int numKeysToBeAdded = m.size();
  4.         if (numKeysToBeAdded == 0)
  5.             return;
  6.          //如果新增加的元素个数大于原来容积*加载因子;可见我们必须要扩充容量了
  7.         if (numKeysToBeAdded > threshold) {
  8.             //那么我们增加的容量将是该新增元素个数+预留空间的总数
  9.             int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
  10.             if (targetCapacity > MAXIMUM_CAPACITY)
  11.                 targetCapacity = MAXIMUM_CAPACITY;
  12.             int newCapacity = table.length;
  13.             //如果我们现在的表的容量小于新增加的容量,那么我们扩展两倍,如果还小,再扩大两倍,直到他的大于当前需要的容量
  14.             while (newCapacity < targetCapacity)
  15.                 newCapacity <<= 1;
  16.             if (newCapacity > table.length)
  17.                 resize(newCapacity);
  18.         }
  19.         
  20.         //添置新的"家具"
  21.         for (Iterator> i = m.entrySet().iterator(); i.hasNext(); ) {
  22.             Map.Entry e = i.next();
  23.             put(e.getKey(), e.getValue());
  24.         }
  25.     }
用新的容积开辟了一块足够大的存储空间,

  1. void resize(int newCapacity) {
  2.         Entry[] oldTable = table;
  3.         int oldCapacity = oldTable.length;
  4.         if (oldCapacity == MAXIMUM_CAPACITY) {
  5.             threshold = Integer.MAX_VALUE;
  6.             return;
  7.         }
  8.         Entry[] newTable = new Entry[newCapacity];
  9.         transfer(newTable);
  10.         table = newTable;
  11.         threshold = (int)(newCapacity * loadFactor);
  12.     }
  13. 并且把家具从以前的“小房子”帮到了“大房子”
  14.  void transfer(Entry[] newTable) {
  15.         Entry[] src = table;
  16.         int newCapacity = newTable.length;
  17.         for (int j = 0; j < src.length; j++) {
  18.             //得到各个数组元素的链表的头
  19.             Entry e = src[j];
  20.             if (e != null) {
  21.                 src[j] = null//一定要把原来的“小房子”收拾好,方便垃圾回收;
  22.                 do {
  23.                     Entry next = e.next;
  24.                     int i = indexFor(e.hash, newCapacity);
  25.                     e.next = newTable[i];
  26.                     newTable[i] = e;
  27.                     e = next;
  28.                 } while (e != null);
  29.             }
  30.         }
  31.     }

新增,查找,都看过了,我们来看一下删除:
  1. public V remove(Object key) {
  2.         Entry e = removeEntryForKey(key);
  3.         return (e == null ? null : e.value);
  4.     }
  5.   
  6.     final Entry removeEntryForKey(Object key) {
  7.         //定位查找到链表表头;
  8.         int hash = (key == null) ? 0 : hash(key.hashCode());
  9.         int i = indexFor(hash, table.length);
  10.         Entry prev = table[i];
  11.         Entry e = prev;
  12.         while (e != null) {
  13.             Entry next = e.next;
  14.             Object k;
  15.             if (e.hash == hash &&
  16.                 ((k = e.key) == key || (key != null && key.equals(k)))) {
  17.                 modCount++;
  18.                 //若能找到,修改前后的链表节点的指向,从中删除;
  19.                 size--;
  20.                 if (prev == e)
  21.                     table[i] = next;
  22.                 else
  23.                     prev.next = next;
  24.                 e.recordRemoval(this);
  25.                 return e;
  26.             }
  27.             //方便链表往下传递;保存prev是因为是单向链表,所以一旦找到的目标节点,无法通过该节点得到钱一个节点,就无法更改前
  28. 一个节点的指//向,所以要保存prev
  29.             prev = e;
  30.             e = next;
  31.         }
  32.         return e;
  33.     }


现在我们再看一下如何单独取到他的键值集合或者值集合:

  1. values实现了一个内部私有类;
  2.   public Collection values() {
  3.         Collection vs = values;
  4.         return (vs != null ? vs : (values = new Values()));
  5.     }
  6. //AbstractCollection 把一些基本的curd委托给了iteartor,所以只需重载其获取iteartor的方法;
  7.     private final class Values extends AbstractCollection {
  8.         
  9.         //重载了iteartor
  10.         public Iterator iterator() {
  11.             return newValueIterator();
  12.         }
  13.         public int size() {
  14.             return size;
  15.         }
  16.         public boolean contains(Object o) {
  17.             return containsValue(o);
  18.         }
  19.         public void clear() {
  20.             HashMap.this.clear();
  21.         }
  22.     }
  23. 下面是该iteartor的获取:其中主要的是看看我们获取的value的collection是如何排序的;
  24.     private abstract class HashIterator implements Iterator {
  25.         Entry next;    // next entry to return
  26.         int expectedModCount;   // For fast-fail
  27.         int index;      // current slot
  28.         Entry current; // current entry
  29.         HashIterator() {
  30.             expectedModCount = modCount;
  31.             //如果该HashMap有元素
  32.             if (size > 0) { // advance to first entry
  33.                 Entry[] t = table;
  34.                 然后把next和index都调至该table数组的链表头中不为空的地方;
  35.                 while (index < t.length && (next = t[index++]) == null)
  36.                     ;
  37.             }
  38.         }
  39.         public final boolean hasNext() {
  40.             return next != null;
  41.         }
  42.         final Entry nextEntry() {
  43.             if (modCount != expectedModCount)
  44.                 throw new ConcurrentModificationException();
  45.             Entry e = next;
  46.             if (e == null)
  47.                 throw new NoSuchElementException();
  48.             //代表这个链表遍历完成了,需要开始遍历下一个table下标的链表了
  49.             if ((next = e.next) == null) {
  50.                 Entry[] t = table;
  51.             //找到下一个不为空的table元素
  52.                 while (index < t.length && (next = t[index++]) == null)
  53.                     ;
  54.             }
  55.         current = e;
  56.             return e;
  57.         }
  58.         public void remove() {
  59.             if (current == null)
  60.                 throw new IllegalStateException();
  61.             if (modCount != expectedModCount)
  62.                 throw new ConcurrentModificationException();
  63.             Object k = current.key;
  64.             current = null;
  65.             HashMap.this.removeEntryForKey(k);
  66.             expectedModCount = modCount;
  67.         }
  68.     }
  69. ValueIterator :
  70.  private final class ValueIterator extends HashIterator {
  71.         public V next() {
  72.             return nextEntry().value;
  73.         }
  74.     }
这是我们会问加入我在这个value的collection中增加元素会出现什么样的情况呢,次序会不会打乱,对不起,AbstractCollection不存在add

的,iteartor也是不允许增加元素的;

对于来说keySet来说是同理的,不过因为key是不存在一样的,所以,我们不会返回collection而返回set,避免了重复key的出现;

好,我们今天先把HashMap分析完成;


推荐阅读
  • 缓存这个东西就是为了提高运行速度的,由于缓存是在寸土寸金的内存里面,不是在硬盘里面,所以容量是很有限的。LRU这个算法就是把最近一次使用时间离现在时间最远的数据删除掉。先说说List:每 ... [详细]
  • HashTable与ConcurrentHashMap均可实现HashMap的功能,对外提供了键值对存储的数据结构。但是在内部结构及实现上有何区别,性能上的差异到底在哪里又是如何导致的 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 广度优先遍历(BFS)算法的概述、代码实现和应用
    本文介绍了广度优先遍历(BFS)算法的概述、邻接矩阵和邻接表的代码实现,并讨论了BFS在求解最短路径或最短步数问题上的应用。以LeetCode中的934.最短的桥为例,详细阐述了BFS的具体思路和代码实现。最后,推荐了一些相关的BFS算法题目供大家练习。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
author-avatar
手浪用户2602890531
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有