Java集合框架之 Java HashMap 源码解析.docx

上传人:b****6 文档编号:8114986 上传时间:2023-01-28 格式:DOCX 页数:20 大小:108.80KB
下载 相关 举报
Java集合框架之 Java HashMap 源码解析.docx_第1页
第1页 / 共20页
Java集合框架之 Java HashMap 源码解析.docx_第2页
第2页 / 共20页
Java集合框架之 Java HashMap 源码解析.docx_第3页
第3页 / 共20页
Java集合框架之 Java HashMap 源码解析.docx_第4页
第4页 / 共20页
Java集合框架之 Java HashMap 源码解析.docx_第5页
第5页 / 共20页
点击查看更多>>
下载资源
资源描述

Java集合框架之 Java HashMap 源码解析.docx

《Java集合框架之 Java HashMap 源码解析.docx》由会员分享,可在线阅读,更多相关《Java集合框架之 Java HashMap 源码解析.docx(20页珍藏版)》请在冰豆网上搜索。

Java集合框架之 Java HashMap 源码解析.docx

Java集合框架之JavaHashMap源码解析

Java集合框架之JavaHashMap源码解析

继上一篇文章Java集合框架综述后,今天正式开始分析具体集合类的代码,首先以既熟悉又陌生的HashMap开始。

签名(signature)

1.public class HashMap 

2.extends AbstractMap 

3.implements Map, Cloneable, Serializable 

可以看到HashMap继承了

∙标记接口Cloneable,用于表明HashMap对象会重写java.lang.Object#clone()方法,HashMap实现的是浅拷贝(shallowcopy)。

∙标记接口Serializable,用于表明HashMap对象可以被序列化

比较有意思的是,HashMap同时继承了抽象类AbstractMap与接口Map,因为抽象类AbstractMap的签名为

1.public abstract class AbstractMap implements Map 

StackOverfloooow上解释到:

在语法层面继承接口Map是多余的,这么做仅仅是为了让阅读代码的人明确知道HashMap是属于Map体系的,起到了文档的作用

AbstractMap相当于个辅助类,Map的一些操作这里面已经提供了默认实现,后面具体的子类如果没有特殊行为,可直接使用AbstractMap提供的实现。

Cloneable接口

1.It's evil, don't use it.  

Cloneable这个接口设计的非常不好,最致命的一点是它里面竟然没有clone方法,也就是说我们自己写的类完全可以实现这个接口的同时不重写clone方法。

关于Cloneable的不足,大家可以去看看《EffectiveJava》一书的作者给出的理由,在所给链接的文章里,JoshBloch也会讲如何实现深拷贝比较好,我这里就不在赘述了。

Map接口

在Eclipse中的outline面板可以看到Map接口里面包含以下成员方法与内部类:

Map_field_method

可以看到,这里的成员方法不外乎是“增删改查”,这也反映了我们编写程序时,一定是以“数据”为导向的。

在上篇文章讲了Map虽然并不是Collection,但是它提供了三种“集合视角”(collectionviews),与下面三个方法一一对应:

∙SetkeySet(),提供key的集合视角

∙Collectionvalues(),提供value的集合视角

∙Set>entrySet(),提供key-value序对的集合视角,这里用内部类Map.Entry表示序对

AbstractMap抽象类

AbstractMap对Map中的方法提供了一个基本实现,减少了实现Map接口的工作量。

举例来说:

如果要实现个不可变(unmodifiable)的map,那么只需继承AbstractMap,然后实现其entrySet方法,这个方法返回的set不支持add与remove,同时这个set的迭代器(iterator)不支持remove操作即可。

相反,如果要实现个可变(modifiable)的map,首先继承AbstractMap,然后重写(override)AbstractMap的put方法,同时实现entrySet所返回set的迭代器的remove方法即可。

设计理念(designconcept)

哈希表(hashtable)

HashMap是一种基于哈希表(hashtable)实现的map,哈希表(也叫关联数组)一种通用的数据结构,大多数的现代语言都原生支持,其概念也比较简单:

key经过hash函数作用后得到一个槽(buckets或slots)的索引(index),槽中保存着我们想要获取的值,如下图所示

hashtabledemo

很容易想到,一些不同的key经过同一hash函数后可能产生相同的索引,也就是产生了冲突,这是在所难免的。

所以利用哈希表这种数据结构实现具体类时,需要:

∙设计个好的hash函数,使冲突尽可能的减少

∙其次是需要解决发生冲突后如何处理。

后面会重点介绍HashMap是如何解决这两个问题的。

HashMap的一些特点

∙线程非安全,并且允许key与value都为null值,HashTable与之相反,为线程安全,key与value都不允许null值。

∙不保证其内部元素的顺序,而且随着时间的推移,同一元素的位置也可能改变(resize的情况)

∙put、get操作的时间复杂度为O

(1)。

∙遍历其集合视角的时间复杂度与其容量(capacity,槽的个数)和现有元素的大小(entry的个数)成正比,所以如果遍历的性能要求很高,不要把capactiy设置的过高或把平衡因子(loadfactor,当entry数大于capacity*loadFactor时,会进行resize,reside会导致key进行rehash)设置的过低。

∙由于HashMap是线程非安全的,这也就是意味着如果多个线程同时对一hashmap的集合试图做迭代时有结构的上改变(添加、删除entry,只改变entry的value的值不算结构改变),那么会报ConcurrentModificationException,专业术语叫fail-fast,尽早报错对于多线程程序来说是很有必要的。

∙Mapm=Collections.synchronizedMap(newHashMap(...)); 通过这种方式可以得到一个线程安全的map。

源码剖析

首先从构造函数开始讲,HashMap遵循集合框架的约束,提供了一个参数为空的构造函数与有一个参数且参数类型为Map的构造函数。

除此之外,还提供了两个构造函数,用于设置HashMap的容量(capacity)与平衡因子(loadFactor)。

1.public HashMap(int initialCapacity, float loadFactor) { 

2.    if (initialCapacity < 0) 

3.        throw new IllegalArgumentException("Illegal initial capacity:

 " + 

4.                                           initialCapacity); 

5.    if (initialCapacity > MAXIMUM_CAPACITY) 

6.        initialCapacity = MAXIMUM_CAPACITY; 

7.    if (loadFactor <= 0 || Float.isNaN(loadFactor)) 

8.        throw new IllegalArgumentException("Illegal load factor:

 " + 

9.                                           loadFactor); 

10.    this.loadFactor = loadFactor; 

11.    threshold = initialCapacity; 

12.    init(); 

13.} 

14.public HashMap(int initialCapacity) { 

15.    this(initialCapacity, DEFAULT_LOAD_FACTOR); 

16.} 

17.public HashMap() { 

18.    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); 

19.} 

20. 

21.从代码上可以看到,容量与平衡因子都有个默认值,并且容量有个最大值 

22. 

23./** 

24.* The default initial capacity - MUST be a power of two. 

25.*/ 

26.static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 

27./** 

28.* The maximum capacity, used if a higher value is implicitly specified 

29.* by either of the constructors with arguments. 

30.* MUST be a power of two <= 1<<30. 

31.*/ 

32.static final int MAXIMUM_CAPACITY = 1 << 30; 

33./** 

34.* The load factor used when none specified in constructor. 

35.*/ 

36.static final float DEFAULT_LOAD_FACTOR = 0.75f; 

可以看到,默认的平衡因子为0.75,这是权衡了时间复杂度与空间复杂度之后的最好取值(JDK说是最好的),过高的因子会降低存储空间但是查找(lookup,包括HashMap中的put与get方法)的时间就会增加。

这里比较奇怪的是问题:

容量必须为2的指数倍(默认为16),这是为什么呢?

解答这个问题,需要了解HashMap中哈希函数的设计原理。

哈希函数的设计原理

1./** 

2.  * Retrieve object hash code and applies a supplemental hash function to the 

3.  * result hash, which defends against poor quality hash functions.  This is 

4.  * critical because HashMap uses power-of-two length hash tables, that 

5.  * otherwise encounter collisions for hashCodes that do not differ 

6.  * in lower bits. Note:

 Null keys always map to hash 0, thus index 0. 

7.  */ 

8.final int hash(Object k) { 

9.     int h = hashSeed; 

10.     if (0 !

= h && k instanceof String) { 

11.         return sun.misc.Hashing.stringHash32((String) k); 

12.     } 

13.     h ^= k.hashCode(); 

14.     // This function ensures that hashCodes that differ only by 

15.     // constant multiples at each bit position have a bounded 

16.     // number of collisions (approximately 8 at default load factor). 

17.     h ^= (h >>> 20) ^ (h >>> 12); 

18.     return h ^ (h >>> 7) ^ (h >>> 4); 

19.} 

20./** 

21.  * Returns index for hash code h. 

22.  */ 

23.static int indexFor(int h, int length) { 

24.     // assert Integer.bitCount(length) == 1 :

 "length must be a non-zero power of 2"; 

25.     return h & (length-1); 

26.} 

看到这么多位操作,是不是觉得晕头转向了呢,还是搞清楚原理就行了,毕竟位操作速度是很快的,不能因为不好理解就不用了。

网上说这个问题的也比较多,我这里根据自己的理解,尽量做到通俗易懂。

在哈希表容量(也就是buckets或slots大小)为length的情况下,为了使每个key都能在冲突最小的情况下映射到[0,length)(注意是左闭右开区间)的索引(index)内,一般有两种做法:

1.让length为素数,然后用hashCode(key)modlength的方法得到索引

2.让length为2的指数倍,然后用hashCode(key)&(length-1)的方法得到索引

HashTable用的是方法1,HashMap用的是方法2。

因为本篇主题讲的是HashMap,所以关于方法1为什么要用素数,我这里不想过多介绍,大家可以看这里。

重点说说方法2的情况,方法2其实也比较好理解:

因为length为2的指数倍,所以length-1所对应的二进制位都为1,然后在与hashCode(key)做与运算,即可得到[0,length)内的索引

但是这里有个问题,如果hashCode(key)的大于length的值,而且hashCode(key)的二进制位的低位变化不大,那么冲突就会很多,举个例子:

Java中对象的哈希值都32位整数,而HashMap默认大小为16,那么有两个对象那么的哈希值分别为:

0xABAB0000与0xBABA0000,它们的后几位都是一样,那么与16异或后得到结果应该也是一样的,也就是产生了冲突。

造成冲突的原因关键在于16限制了只能用低位来计算,高位直接舍弃了,所以我们需要额外的哈希函数而不只是简单的对象的hashCode方法了。

具体来说,就是HashMap中hash函数干的事了

首先有个随机的hashSeed,来降低冲突发生的几率

然后如果是字符串,用了sun.misc.Hashing.stringHash32((String)k);来获取索引值

最后,通过一系列无符号右移操作,来把高位与低位进行异或操作,来降低冲突发生的几率

右移的偏移量20,12,7,4是怎么来的呢?

因为Java中对象的哈希值都是32位的,所以这几个数应该就是把高位与低位做异或运算,至于这几个数是如何选取的,就不清楚了,网上搜了半天也没统一且让人信服的说法,大家可以参考下面几个链接:

HashMap.Entry

HashMap中存放的是HashMap.Entry对象,它继承自Map.Entry,其比较重要的是构造函数

1.static class Entry implements Map.Entry { 

2.    final K key; 

3.    V value; 

4.    Entry next; 

5.    int hash; 

6.    Entry(int h, K k, V v, Entry n) { 

7.        value = v; 

8.        next = n; 

9.        key = k; 

10.        hash = h; 

11.    } 

12.    // setter, getter, equals, toString 方法省略 

13.    public final int hashCode() { 

14.        //用key的hash值与上value的hash值作为Entry的hash值 

15.        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); 

16.    } 

17.    /** 

18.     * This method is invoked whenever the value in an entry is 

19.     * overwritten by an invocation of put(k,v) for a key k that's already 

20.     * in the HashMap. 

21.     */ 

22.    void recordAccess(HashMap m) { 

23.    } 

24.    /** 

25.     * This method is invoked whenever the entry is 

26.     * removed from the table. 

27.     */ 

28.    void recordRemoval(HashMap m) { 

29.    } 

30.} 

可以看到,Entry实现了单向链表的功能,用next成员变量来级连起来。

介绍完Entry对象,下面要说一个比较重要的成员变量

/**

*Thetable,resizedasnecessary.LengthMUSTAlwaysbeapoweroftwo.

*/

//HashMap内部维护了一个为数组类型的Entry变量table,用来保存添加进来的Entry对象

transientEntry[]table=(Entry[])EMPTY_TABLE;

你也许会疑问,Entry不是单向链表嘛,怎么这里又需要个数组类型的table呢?

我翻了下之前的算法书,其实这是解决冲突的一个方式:

链地址法(开散列法),效果如下:

链地址法处理冲突得到的散列表

就是相同索引值的Entry,会以单向链表的形式存在

链地址法的可视化

网上找到个很好的网站,用来可视化各种常见的算法,很棒。

瞬间觉得国外大学比国内的强不知多少倍。

下面的链接可以模仿哈希表采用链地址法解决冲突,大家可以自己去玩玩

∙https:

//www.cs.usfca.edu/~galles/visualization/OpenHash.html

get操作

get操作相比put操作简单,所以先介绍get操作

1.public V get(Object key) { 

2.    //单独处理key为null的情况 

3.    if (key == null) 

4.        return getForNullKey(); 

5.    Entry entry = getEntry(key); 

6.    return null == entry ?

 null :

 entry.getValue(); 

7.} 

8.private V getForNullKey() { 

9.    if (size == 0) { 

10.        return null; 

11.    } 

12.    //key为null的Entry用于放在table[0]中,但是在table[0]冲突链中的Entry的key不一定为null 

13.    //所以需要遍历冲突链,查找key是否存在 

14.    for (Entry e = table[0]; e !

= null; e = e.next) { 

15.        if (e.key == null) 

16.            return e.value; 

17.    } 

18.    return null; 

19.} 

20.final Entry getEntry(Object key) { 

21.    if (size == 0) { 

22.        return null; 

23.    } 

24.    int hash = (key == null) ?

 0 :

 hash(key); 

25.    //首先定位到索引在table中的位置 

26.    //然后遍历冲突链,查找key是否存在 

27.    for (Entry e = table[indexFor(hash, table.length)]; 

28.         e !

= null; 

29.         e = e.next) { 

30.        Object k; 

31.        if (e.hash == hash && 

32.            ((k = e.key) == key || (key !

= null && key.equals(k)))) 

33.            return e; 

34.    } 

35.    return null; 

36.} 

put操作(含update操作)

因为put操作有可能需要对HashMap进行resize,所以实现略复杂些

1.private void inflateTable(int toSize) { 

2.    //辅助函数,用于填充HashMap到指定的capacity 

3.    // Find a power of 2 >= toSize 

4.    int capacity = roundUpToPowerOf2(toSize); 

5.    //threshold为resize的阈值,超过后HashMap会进行resize,内容的entry会进行rehash 

6.    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); 

7.    table = new Entry[capacity]; 

8.    initHashSeedA

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 工学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1