jdk中的HashMap的实现
来源:互联网 发布:淘宝联系客服链接 编辑:程序博客网 时间:2024/06/07 06:09
发现一篇关于HashMap理解得很好得文章,做一下分享 原文链接:http://blog.csdn.net/ghsau/article/details/16890151
HashMap可以说是Java中最常用的集合类框架之一,是Java语言中非常典型的数据结构,我们总会在不经意间用到它,很大程度上方便了我们日常开发。在很多Java的笔试题中也会被问到,最常见的,“HashMap和HashTable有什么区别?”,这也不是三言两语能说清楚的,这种笔试题就是考察你来笔试之前有没有复习功课,随便来个快餐式的复习就能给出简单的答案。
了解HashMap之前,我们需要知道Object类的两个方法hashCode和equals,我们先来看一下这两个方法的默认实现:
- /** JNI,调用底层其它语言实现 */
- public native int hashCode();
- /** 默认同==,直接比较对象 */
- public boolean equals(Object obj) {
- return (this == obj);
- }
- public boolean equals(Object anObject) {
- if (this == anObject) {
- return true;
- }
- if (anObject instanceof String) {
- String anotherString = (String) anObject;
- int n = value.length;
- if (n == anotherString.value.length) {
- char v1[] = value;
- char v2[] = anotherString.value;
- int i = 0;
- // 逐个判断字符是否相等
- while (n-- != 0) {
- if (v1[i] != v2[i])
- return false;
- i++;
- }
- return true;
- }
- }
- return false;
- }
- 自反性:对于任何非空引用值 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。
- 在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
- 如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
- 以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
java.util
类 HashMap<K,V>
java.lang.Object java.util.AbstractMap<K,V> java.util.HashMap<K,V>
- 所有已实现的接口:
- Serializable,Cloneable,Map<K,V>
- 直接已知子类:
- LinkedHashMap,PrinterStateReasons
- HashMap通过键的hashCode来快速的存取元素。
- 当不同的对象hashCode发生碰撞时,HashMap通过单链表来解决,将新元素加入链表表头,通过next指向原有的元素。单链表在Java中的实现就是对象的引用(复合)。
- public V put(K key, V value) {
- // 处理key为null,HashMap允许key和value为null
- if (key == null)
- return putForNullKey(value);
- // 得到key的哈希码
- int hash = hash(key);
- // 通过哈希码计算出bucketIndex
- int i = indexFor(hash, table.length);
- // 取出bucketIndex位置上的元素,并循环单链表,判断key是否已存在
- 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;
- }
- }
- // key不存在时,加入新元素
- modCount++;
- addEntry(hash, key, value, i);
- return null;
- }
Java Collections Framework中实际操作的都是数组或者链表,而我们通常不需要显示的维护集合的大小,而是集合类框架中内部维护,方便的同时,也带来了性能的问题。
HashMap有两个参数影响其性能:初始容量和加载因子。默认初始容量是16,加载因子是0.75。容量是哈希表中桶(Entry数组)的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。
HashMap中定义的成员变量如下:
- /**
- * The default initial capacity - MUST be a power of two.
- */
- static final int DEFAULT_INITIAL_CAPACITY = 16;// 默认初始容量为16,必须为2的幂
- /**
- * The maximum capacity, used if a higher value is implicitly specified
- * by either of the constructors with arguments.
- * MUST be a power of two <= 1<<30.
- */
- static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量为2的30次方
- /**
- * The load factor used when none specified in constructor.
- */
- static final float DEFAULT_LOAD_FACTOR = 0.75f;// 默认加载因子0.75
- /**
- * The table, resized as necessary. Length MUST Always be a power of two.
- */
- transient Entry<K,V>[] table;// Entry数组,哈希表,长度必须为2的幂
- /**
- * The number of key-value mappings contained in this map.
- */
- transient int size;// 已存元素的个数
- /**
- * The next size value at which to resize (capacity * load factor).
- * @serial
- */
- int threshold;// 下次扩容的临界值,size>=threshold就会扩容
- /**
- * The load factor for the hash table.
- *
- * @serial
- */
- final float loadFactor;// 加载因子
HashMap()
构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity)
构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity, float loadFactor)
构造一个带指定初始容量和加载因子的空 HashMap。
HashMap(Map<? extendsK,? extendsV> m)
构造一个映射关系与指定 Map 相同的 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);
- // Find a power of 2 >= initialCapacity
- // 这里需要注意一下
- int capacity = 1;
- while (capacity < initialCapacity)
- capacity <<= 1;
- // 设置加载因子
- this.loadFactor = loadFactor;
- // 设置下次扩容临界值
- threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
- // 初始化哈希表
- table = new Entry[capacity];
- useAltHashing = sun.misc.VM.isBooted() &&
- (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
- init();
- }
我们在日常做底层开发时,必须要严格控制入参,可以参考一下Java源码及各种开源项目源码,如果参数不合法,适时的抛出一些运行时异常,最后到应用层捕获。看第14-16行代码,这里做了一个移位运算,保证了初始容量一定为2的幂,假如你传的是5,那么最终的初始容量为8。源码中的位运算随处可见啊=。=!
到现在为止,我们有一个很强烈的问题,为什么HashMap容量一定要为2的幂呢?HashMap中的数据结构是数组+单链表的组合,我们希望的是元素存放的更均匀,最理想的效果是,Entry数组中每个位置都只有一个元素,这样,查询的时候效率最高,不需要遍历单链表,也不需要通过equals去比较K,而且空间利用率最大。那如何计算才会分布最均匀呢?我们首先想到的就是%运算,哈希值%容量=bucketIndex,SUN的大师们是否也是如此做的呢?我们阅读一下这段源码:
- /**
- * Returns index for hash code h.
- */
- static int indexFor(int h, int length) {
- return h & (length-1);
- }
通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点,可以想想为什么)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地降低 rehash 操作次数。如果初始容量大于最大条目数除以加载因子(实际上就是最大条目数小于初始容量*加载因子),则不会发生 rehash 操作。
如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。 当HashMap存放的元素越来越多,到达临界值(阀值)threshold时,就要对Entry数组扩容,这是Java集合类框架最大的魅力,HashMap在扩容时,新数组的容量将是原来的2倍,由于容量发生变化,原有的每个元素需要重新计算bucketIndex,再存放到新数组中去,也就是所谓的rehash。HashMap默认初始容量16,加载因子0.75,也就是说最多能放16*0.75=12个元素,当put第13个时,HashMap将发生rehash,rehash的一系列处理比较影响性能,所以当我们需要向HashMap存放较多元素时,最好指定合适的初始容量和加载因子,否则HashMap默认只能存12个元素,将会发生多次rehash操作。
HashMap所有集合类视图所返回迭代器都是快速失败的(fail-fast),在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器自身的 remove 或 add 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败。注意,迭代器的快速失败行为不能得到保证,一般来说,存在不同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。至于为什么通过迭代器自身的remove或add方法就不会出现这个问题,可以参考我之前的文章List比较好玩的操作中第三个和第四个示例。
HashMap是线程不安全的实现,而HashTable是线程安全的实现,关于线程安全与不安全可以参考我之前的文章Java线程(一):线程安全与不安全,所谓线程不安全,就是在多线程情况下直接使用HashMap会出现一些莫名其妙不可预知的问题,多线程和单线程的区别:单线程只有一条执行路径,而多线程是并发执行(非并行),会有多条执行路径。如果HashMap是只读的(加载一次,以后只有读取,不会发生结构上的修改),那使用没有问题。那如果HashMap是可写的(会发生结构上的修改),则会引发诸多问题,如上面的fail-fast,也可以看下这里,这里就不去研究了。
那在多线程下使用HashMap我们需要怎么做,几种方案:
- 在外部包装HashMap,实现同步机制
- 使用Map m = Collections.synchronizedMap(new HashMap(...));,这里就是对HashMap做了一次包装
- 使用java.util.HashTable,效率最低
- 使用java.util.concurrent.ConcurrentHashMap,相对安全,效率较高
1、jdk中的HashMap是通过数组加链表实现的,存入数据的时候,如果没有发生哈希碰撞,则直接以Entry<k,v>的形式存入对应的数组Entry<k,v> [] table的相应位置,如果发生了碰撞,则判断所在位置的k是否和所要放入的索引相等,如果相等则对Entry<k,v>进行覆盖,如果不相等,则会生成一个链表来维护这样的关系,将Entry<k,v>这个元素放在当前位置元素的末尾。换句话说,一个哈希的存储位置可能出现任意个元素Entry<k,v>,如果存在多个这样的不同元素,则用链表进行链接。
2、HashMap是支持null作为key,也支持null作为value的,这也是HashMap和HashTable的一个区别。
3、HashMap中存在初始容量和加载因子。默认初始容量是16,加载因子是0.75。容量指的是存储Entry<k,v>的个数,加载因子的作用在于当当前容量乘以加载因子大于初始容量的时候就会使用rehash方法进行扩容。
4、容量必须是2的幂,因为为了提高效率这里使用了位运算:使用h&(length-1)代替了h%length,然而这样仅当h是2的幂的时候才成立。
5、HashMap不是线程安全,转换成线程安全的方法在上方已经说明,HashMap同样使用了快速失败机制,存在一个私有成员变量modCount,一旦发现存在并发修改异常便抛出ConcurrentModificationException,而不冒着可能存在错误的风险继续进行。
- jdk中的HashMap的实现
- JDK HashMap的内部实现分析
- JDK源码阅读之HashMap的实现
- HashMap类的JDK实现剖析
- JDK源码阅读:实现自己的HashMap
- JDK学习---深入理解java中的HashMap、HashSet底层实现
- java中的HashMap的实现
- JDK——HashMap实现
- jdk:HashSet基于HashMap实现
- jdk中的LinkedList的实现
- jdk 1.7中HashMap的HashIterator实现细节小记
- javascript实现java中的HashMap的功能
- java 中的HashMap的一些实现细节
- js中的hashmap实现
- js中的hashmap实现
- JDK的HashMap源码解读
- 深入JDK源代码之HashMap实现
- 深入JDK源代码之HashMap实现
- 在Spring3中,配置DataSource的方法有5种
- jQuery过滤
- Android应用稳定性优化--快速点击产生异常的解决思路
- Unity日常——关于射线
- MATLAB学习笔记(3)
- jdk中的HashMap的实现
- [线性代数]遗留问题
- python: 下划线 使用
- Spring、Spring MVC、MyBatis 整合文件配置详解
- map和set增删查改
- Kali下 如何安装 Nessus
- JDBC入门技术:如何使用连接池技术连接数据库
- MVC(Model View Controller)模型-视图-控制器
- 代理模式