文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java HashMap源码是什么

2023-06-17 06:10

关注

本篇内容主要讲解“Java HashMap源码是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java HashMap源码是什么”吧!

签名(signature)

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

可以看到HashMap继承了

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

public abstract class AbstractMap<K,V> implements Map<K,V>

Stack Overfloooow上解释到:

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

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

Cloneable接口

<code>It's evil, don't use it. </code>

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

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

Map接口

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

Java HashMap源码是什么
Map_field_method

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

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

AbstractMap抽象类

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

举例来说:

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

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

设计理念(design concept)

哈希表(hash table)

HashMap是一种基于哈希表(hash table)实现的map,哈希表(也叫关联数组)一种通用的数据结构,大多数的现代语言都原生支持,其概念也比较简单:key经过hash函数作用后得到一个槽(buckets或slots)的索引(index),槽中保存着我们想要获取的值,如下图所示

Java HashMap源码是什么
hash table demo

很容易想到,一些不同的key经过同一hash函数后可能产生相同的索引,也就是产生了冲突,这是在所难免的。
所以利用哈希表这种数据结构实现具体类时,需要:

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

HashMap的一些特点

源码剖析

首先从构造函数开始讲,HashMap遵循集合框架的约束,提供了一个参数为空的构造函数与有一个参数且参数类型为Map的构造函数。除此之外,还提供了两个构造函数,用于设置HashMap的容量(capacity)与平衡因子(loadFactor)。

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(); } public HashMap(int initialCapacity) {     this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() {     this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }  从代码上可以看到,容量与平衡因子都有个默认值,并且容量有个***值   static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  static final int MAXIMUM_CAPACITY = 1 << 30;  static final float DEFAULT_LOAD_FACTOR = 0.75f;

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

这里比较奇怪的是问题:容量必须为2的指数倍(默认为16),这是为什么呢?解答这个问题,需要了解HashMap中哈希函数的设计原理。

哈希函数的设计原理

 final int hash(Object k) {      int h = hashSeed;      if (0 != h && k instanceof String) {          return sun.misc.Hashing.stringHash42((String) k);      }      h ^= k.hashCode();      // This function ensures that hashCodes that differ only by      // constant multiples at each bit position have a bounded      // number of collisions (approximately 8 at default load factor).      h ^= (h >>> 20) ^ (h >>> 12);      return h ^ (h >>> 7) ^ (h >>> 4); }  static int indexFor(int h, int length) {      // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";      return h & (length-1); }

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

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

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

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

  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,那么有两个对象那么的哈希值分别为:0xABAB00000xBABA0000,它们的后几位都是一样,那么与16异或后得到结果应该也是一样的,也就是产生了冲突。

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

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

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

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

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

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

HashMap.Entry

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

static class Entry<K,V> implements Map.Entry<K,V> {     final K key;     V value;     Entry<K,V> next;     int hash;     Entry(int h, K k, V v, Entry<K,V> n) {         value = v;         next = n;         key = k;         hash = h;     }     // setter, getter, equals, toString 方法省略     public final int hashCode() {         //用key的hash值与上value的hash值作为Entry的hash值         return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());     }          void recordAccess(HashMap<K,V> m) {     }          void recordRemoval(HashMap<K,V> m) {     } }

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

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


//HashMap内部维护了一个为数组类型的Entry变量table,用来保存添加进来的Entry对象
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

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

我翻了下之前的算法书,其实这是解决冲突的一个方式:链地址法(开散列法),效果如下:

Java HashMap源码是什么
链地址法处理冲突得到的散列表

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

链地址法的可视化

网上找到个很好的网站,用来可视化各种常见的算法,很棒。瞬间觉得国外大学比国内的强不知多少倍。

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

get操作

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

public V get(Object key) {     //单独处理key为null的情况     if (key == null)         return getForNullKey();     Entry<K,V> entry = getEntry(key);     return null == entry ? null : entry.getValue(); } private V getForNullKey() {     if (size == 0) {         return null;     }     //key为null的Entry用于放在table[0]中,但是在table[0]冲突链中的Entry的key不一定为null     //所以需要遍历冲突链,查找key是否存在     for (Entry<K,V> e = table[0]; e != null; e = e.next) {         if (e.key == null)             return e.value;     }     return null; } final Entry<K,V> getEntry(Object key) {     if (size == 0) {         return null;     }     int hash = (key == null) ? 0 : hash(key);     //首先定位到索引在table中的位置     //然后遍历冲突链,查找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; }

put操作(含update操作)

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

private void inflateTable(int toSize) {     //辅助函数,用于填充HashMap到指定的capacity     // Find a power of 2 >= toSize     int capacity = roundUpToPowerOf2(toSize);     //threshold为resize的阈值,超过后HashMap会进行resize,内容的entry会进行rehash     threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);     table = new Entry[capacity];     initHashSeedAsNeeded(capacity); }  public V put(K key, V value) {     if (table == EMPTY_TABLE) {         inflateTable(threshold);     }     if (key == null)         return putForNullKey(value);     int hash = hash(key);     int i = indexFor(hash, table.length);     //这里的循环是关键     //当新增的key所对应的索引i,对应table[i]中已经有值时,进入循环体     for (Entry<K,V> e = table[i]; e != null; e = e.next) {         Object k;         //判断是否存在本次插入的key,如果存在用本次的value替换之前oldValue,相当于update操作         //并返回之前的oldValue         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {             V oldValue = e.value;             e.value = value;             e.recordAccess(this);             return oldValue;         }     }     //如果本次新增key之前不存在于HashMap中,modCount加1,说明结构改变了     modCount++;     addEntry(hash, key, value, i);     return null; } void addEntry(int hash, K key, V value, int bucketIndex) {     //如果增加一个元素会后,HashMap的大小超过阈值,需要resize     if ((size >= threshold) && (null != table[bucketIndex])) {         //增加的幅度是之前的1倍         resize(2 * table.length);         hash = (null != key) ? hash(key) : 0;         bucketIndex = indexFor(hash, table.length);     }     createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) {     //首先得到该索引处的冲突链Entries,有可能为null,不为null     Entry<K,V> e = table[bucketIndex];     //然后把新的Entry添加到冲突链的开头,也就是说,后插入的反而在前面(***次还真没看明白)     //需要注意的是table[bucketIndex]本身并不存储节点信息,     //它就相当于是单向链表的头指针,数据都存放在冲突链中。     table[bucketIndex] = new Entry<>(hash, key, value, e);     size++; } //下面看看HashMap是如何进行resize,庐山真面目就要揭晓了 void resize(int newCapacity) {     Entry[] oldTable = table;     int oldCapacity = oldTable.length;     //如果已经达到***容量,那么就直接返回     if (oldCapacity == MAXIMUM_CAPACITY) {         threshold = Integer.MAX_VALUE;         return;     }     Entry[] newTable = new Entry[newCapacity];     //initHashSeedAsNeeded(newCapacity)的返回值决定了是否需要重新计算Entry的hash值     transfer(newTable, initHashSeedAsNeeded(newCapacity));     table = newTable;     threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }  void transfer(Entry[] newTable, boolean rehash) {     int newCapacity = newTable.length;     //遍历当前的table,将里面的元素添加到新的newTable中     for (Entry<K,V> e : table) {         while(null != e) {             Entry<K,V> next = e.next;             if (rehash) {                 e.hash = null == e.key ? 0 : hash(e.key);             }             int i = indexFor(e.hash, newCapacity);             e.next = newTable[i];             / final boolean initHashSeedAsNeeded(int capacity) {     boolean currentAltHashing = hashSeed != 0;     boolean useAltHashing = sun.misc.VM.isBooted() &&             (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);     //这里说明了,在hashSeed不为0或满足useAltHash时,会重算Entry的hash值     //至于useAltHashing的作用可以参考下面的链接     // http://stackoverflow.com/questions/29918624/what-is-the-use-of-holder-class-in-hashmap     boolean switching = currentAltHashing ^ useAltHashing;     if (switching) {         hashSeed = useAltHashing             ? sun.misc.Hashing.randomHashSeed(this)             : 0;     }     return switching; }

remove操作

public V remove(Object key) {     Entry<K,V> e = removeEntryForKey(key);     //可以看到删除的key如果存在,就返回其所对应的value     return (e == null ? null : e.value); } final Entry<K,V> removeEntryForKey(Object key) {     if (size == 0) {         return null;     }     int hash = (key == null) ? 0 : hash(key);     int i = indexFor(hash, table.length);     //这里用了两个Entry对象,相当于两个指针,为的是防治冲突链发生断裂的情况     //这里的思路就是一般的单向链表的删除思路     Entry<K,V> prev = table[i];     Entry<K,V> e = prev;     //当table[i]中存在冲突链时,开始遍历里面的元素     while (e != null) {         Entry<K,V> next = e.next;         Object k;         if (e.hash == hash &&             ((k = e.key) == key || (key != null && key.equals(k)))) {             modCount++;             size--;             if (prev == e) //当冲突链只有一个Entry时                 table[i] = next;             else                 prev.next = next;             e.recordRemoval(this);             return e;         }         prev = e;         e = next;     }     return e; }

到现在为止,HashMap的增删改查都介绍完了。
一般而言,认为HashMap的这四种操作时间复杂度为O(1),因为它hash函数性质较好,保证了冲突发生的几率较小。

HashMap的序列化

介绍到这里,基本上算是把HashMap中一些核心的点讲完了,但还有个比较严重的问题:保存Entry的table数组为transient的,也就是说在进行序列化时,并不会包含该成员,这是为什么呢?

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

为了解答这个问题,我们需要明确下面事实:

我们可以试想下面的场景:

我们在机器A上算出对象A的哈希值与索引,然后把它插入到HashMap中,然后把该HashMap序列化后,在机器B上重新算对象的哈希值与索引,这与机器A上算出的是不一样的,所以我们在机器B上get对象A时,会得到错误的结果。

所以说,当序列化一个HashMap对象时,保存Entry的table是不需要序列化进来的,因为它在另一台机器上是错误的。

因为这个原因,HashMap重现了writeObjectreadObject 方法

private void writeObject(java.io.ObjectOutputStream s)     throws IOException {     // Write out the threshold, loadfactor, and any hidden stuff     s.defaultWriteObject();      // Write out number of buckets     if (table==EMPTY_TABLE) {         s.writeInt(roundUpToPowerOf2(threshold));     } else {        s.writeInt(table.length);     }      // Write out size (number of Mappings)     s.writeInt(size);      // Write out keys and values (alternating)     if (size > 0) {         for(Map.Entry<K,V> e : entrySet0()) {             s.writeObject(e.getKey());             s.writeObject(e.getValue());         }     } }  private static final long serialVersionUID = 362498820763181265L;  private void readObject(java.io.ObjectInputStream s)      throws IOException, ClassNotFoundException {     // Read in the threshold (ignored), loadfactor, and any hidden stuff     s.defaultReadObject();     if (loadFactor <= 0 || Float.isNaN(loadFactor)) {         throw new InvalidObjectException("Illegal load factor: " +                                            loadFactor);     }      // set other fields that need values     table = (Entry<K,V>[]) EMPTY_TABLE;      // Read in number of buckets     s.readInt(); // ignored.      // Read number of mappings     int mappings = s.readInt();     if (mappings < 0)         throw new InvalidObjectException("Illegal mappings count: " +                                            mappings);      // capacity chosen by number of mappings and desired load (if >= 0.25)     int capacity = (int) Math.min(                 mappings * Math.min(1 / loadFactor, 4.0f),                 // we have limits...                 HashMap.MAXIMUM_CAPACITY);      // allocate the bucket array;     if (mappings > 0) {         inflateTable(capacity);     } else {         threshold = capacity;     }      init();  // Give subclass a chance to do its thing.      // Read the keys and values, and put the mappings in the HashMap     for (int i = 0; i < mappings; i++) {         K key = (K) s.readObject();         V value = (V) s.readObject();         putForCreate(key, value);     } } private void putForCreate(K key, V value) {     int hash = null == key ? 0 : hash(key);     int i = indexFor(hash, table.length);           for (Entry<K,V> e = table[i]; e != null; e = e.next) {         Object k;         if (e.hash == hash &&             ((k = e.key) == key || (key != null && key.equals(k)))) {             e.value = value;             return;         }     }      createEntry(hash, key, value, i); }

简单来说,在序列化时,针对Entry的key与value分别单独序列化,当反序列化时,再单独处理即可。

到此,相信大家对“Java HashMap源码是什么”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯