HashMap源码分析
来源:互联网 发布:葛优演技 知乎 编辑:程序博客网 时间:2024/06/10 11:39
由于最近出JavaSE的教程视频,录制到集合这里了,所以就写个博客吧,也顺便理清下思路。
好的,其实HashMap用一张图,稍微加点注释就能解释完的,但是,你要是这么说,别人肯定说你没水准~就像很多培训机构一样,讲解的知识点要录制一个多小时,中间还要讲个故事鼓励大家~
还需要两个方法,equals、hashcode,那么如果要是put,先根据hashcode找到数组中可以安放元素的位置,然后根据equals到链表里面判断有没有相同的值,如果
没有,那么将此元素放到第一位,如果有,将链表此节点的数据替换,并且返回被换了的值,哦,对了,如果数组这个位置上面没有的话,此元素就放到数组上面啦~
那么如果你要是面试的话,HashMap的优缺点说下数组、链表的优缺点就行了~
额,到此好像讲解完了~好吧,这样对不起观众~分析下源码吧,这样估计就能讲解个把小时了~
咱们先看下JDK api对HashMap的解释:
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap
方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:
Map m = Collections.synchronizedMap(new HashMap(...));
由所有此类的“collection 视图方法”所返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException
。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
首先我们看看equals、hashcode方法:
//这里equals被设置成了final,就是为了防止别人重写~public final boolean equals(Object o) { //判断是不是Entry,如果是Entry可以继续比较 if (!(o instanceof Map.Entry))//这里的Entry是个链表结构,是单向链表(单向就是里面只有next引用,没有pre引用) return false; Map.Entry e = (Map.Entry)o; //分别取得两者的key值 Object k1 = getKey(); Object k2 = e.getKey(); //判断两个key的引用或者内容相等 if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); //判断两个key的值是否想的 if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; }
下面看下JDK api对equals的解释:就看中文吧,不装逼~
equalspublic boolean equals(Object obj)指示其他某个对象是否与此对象“相等”。equals 方法在非空对象引用上实现相等关系:自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。对于任何非空引用值 x,x.equals(null) 都应返回 false。Object 类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true(x == y 具有值 true)。注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。参数:obj - 要与之比较的引用对象。返回:如果此对象与 obj 参数相同,则返回 true;否则返回 false。
下面看看hashcode:
public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());//通过key、value一起来位运算来获得hashCode值 }
下面是JDK api对hashcode的解释:
hashCodepublic int hashCode()返回该对象的哈希码值。支持此方法是为了提高哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。hashCode 的常规协定是:在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。//全放数组里面肯定快啊,另外你删除也是很影响性能的~因为是数组啊实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)返回:此对象的一个哈希码值。
至此,我们把概念都了解完了,下面开始了解了解具体的底层代码:
1、创建HashMap对象
2、put
3、get
4、rehash
先看看HashMap中的几个参数:
/** * 默认初始化容量,必须是2的倍数,默认是16个长度:1 << 4是位运算,可以查资料~ */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * 表示此HashMap最大容量是1<<30=1,073,741,824 这么长 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 加载因子,当容量不够,就乘以这个倍数去扩容 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 空数组 */ static final Entry<?,?>[] EMPTY_TABLE = {}; /** * 是个链表类型的数组,初始化为空 */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
1、看下HashMap的构造器:其他几个都调此构造器
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(); }
至此,大家可以看到,如果你即使在新建一个HashMap对象制定一个容量长度,那么他/它给你的还是一个{}!!!
2、put
public V put(K key, V value) { //如果是空数组,那么新建一个数组 if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null)//如果key是null,那么执行存储一个key为null的Entry return putForNullKey(value); int hash = hash(key);//获取key的hash码 int i = indexFor(hash, table.length);//根据key的hash码来,此算法能保证key放在桶(即上面的数组)中的合适位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) {//这里其实还有判断功能,如果key的hash获得的数组i位置不为空,那么在到链表里面找具体的key Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//如果找到的key相等,那么将此key的value替换掉,并且返回旧的value V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //添加数据 addEntry(hash, key, value, i); return null; }
下面是添加数据的方法:addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {//bucketIndex:桶索引,也就是数组的索引 if ((size >= threshold) && (null != table[bucketIndex])) {//这段是又一次判断到底这个数组索引处是否有数据,如果已经有了从新计算bucketIndex桶索引 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } //具体的添加数据方法 createEntry(hash, key, value, bucketIndex); }
下面是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);//新建一个Entry放到当前位置,也就是将心得key:value生成的Entry放到链表的头位置 size++; }HashMap.Entry的构造器:
Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n;//这里可以看到,将新的Entry放到了头链表头位置 key = k; hash = h; }
至此,put方法分析完成!看不明白的建议看源码!
3、get
public V get(Object key) { if (key == null)//如果是null,返回null对应的值 return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } //根据key获得hash,在根据hash获取桶的位置也就是数组的索引,找到那个元素,在此元素即链表上查找具体的key,然后执行判断 int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = 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; } return null; }
至此get方法分析完。
4、rehash:也就是数组扩容了
void resize(int newCapacity) {//容量是原来的2倍 Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity];//扩容是new了个新数组 transfer(newTable, initHashSeedAsNeeded(newCapacity));//这里执行拷贝操作 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
我们到现在可以知道,HashMap中最主要的是桶(数组)的概念,还有每个数组上面的链表
hash(key)来获取key的hash,indexFor(hash, table.length)获取hash的数组索引。
未完待续
需要了解的知识:(如果这些不了解以后研究JDK底层源码还是有点看不明白的)
1、二进制的负数、源码、补码、反码
2、位移<<、>>、>>>运算
3、Java中的与(&)、或(|)、 非(~ ) 、 异或(^)
4、二进制、八进制、十进制、十六进制之间互相转换
- 源码分析:HashMap
- 源码分析:HashMap
- HashMap源码分析
- HashMap 源码分析
- HashMap源码分析
- HashMap LinkedHashMap源码分析
- HashMap源码分析
- HashMap 源码分析
- HashMap源码分析
- HashMap源码分析
- HashMap源码分析
- Java HashMap 源码分析
- HashMap源码分析
- java HashMap源码分析
- 源码分析HashMap
- HashMap源码分析
- HashMap源码分析
- HashMap源码分析
- MySQL5.6安装步骤(windows7/8_64位)
- django-admin.py startproject 无法创建项目
- Android Studio 安装
- R语言包_gbm
- mysql的算术运算符和比较运算符
- HashMap源码分析
- 字符串处理相关
- 『细节』是什么
- Java设计模式-1-概述
- Py2exe打包时添加自定义库搜索目录
- cocos2d-x内存管理(1)
- POJ 3617 字典序最小问题
- 第0课第4节 刚接触开发板之使用vmwae和预先做好的ubuntu
- iOS笔试知识点集锦