Java集合源码学习(四)HashMap分析

简介:

ArrayList、LinkedList和HashMap的源码是一起看的,横向对比吧,感觉对这三种数据结构的理解加深了很多。

1.数组、链表和哈希表结构

数据结构中有数组和链表来实现对数据的存储,这两者有不同的应用场景,
数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,插入和删除容易;
哈希表的实现结合了这两点,哈希表的实现方式有多种,在HashMap中使用的是链地址法,也就是拉链法
看下面这张流传很广的图,

拉链法实际上是一种链表数组的结构,由数组加链表组成,在这个长度为16的数组中(HashMap默认初始化大小就是16),每个元素存储的是一个链表的头结点。
通过元素的key的hash值对数组长度取模,将这个元素插入到数组对应位置的链表中。
例如在图中,337%16=1,353%16=1,于是将其插入到数组位置1的链表头结点中。

2.关于HashMap

(1)继承和实现

继承AbstractMap抽象类,Map的一些操作在AbstractMap里已经提供了默认实现,
实现Map接口,可以应用Map接口定义的一些操作,明确HashMap属于Map体系,
Cloneable接口,表明HashMap对象会重写java.lang.Object#clone()方法,HashMap实现的是浅拷贝(shallow copy),
Serializable接口,表明HashMap对象可以被序列化

(2)内部数据结构

HashMap的实际数据存储在Entry类的数组中,
上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[]。

1
2
3
4
/**
      * 内部实际的存储数组,如果需要调整,容量必须是2的幂
      */
     transient  Entry[] table;

再来看一下Entry这个内部静态类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static  class  Entry<K,V>  implements  Map.Entry<K,V> {
         final  K key; //Key-value结构的key
         V value; //存储值
         Entry<K,V> next; //指向下一个链表节点
         final  int  hash; //哈希值
 
         /**
          * Creates new entry.
          */
         Entry( int  h, K k, V v, Entry<K,V> n) {
             value = v;
             next = n;
             key = k;
             hash = h;
         }
         ......
   }

(3)线程安全

HashMap是非同步的,即线程不安全,在多线程条件下,可能出现很多问题,
1.多线程put后可能导致get死循环,具体表现为CPU使用率100%(put的时候transfer方法循环将旧数组中的链表移动到新数组)
2.多线程put的时候可能导致元素丢失(在addEntry方法的new Entry<K,V>(hash, key, value, e),如果两个线程都同时取得了e,则他们下一个元素都是e,然后赋值给table元素的时候有一个成功有一个丢失)

关于HashMap线程安全性更多的了解参考相关的网上资源,这里不多叙述。

需要线程安全的哈希表结构,可以考虑以下的方式:

使用Hashtable 类,Hashtable 是线程安全的;
使用并发包下的java.util.concurrent.ConcurrentHashMap,ConcurrentHashMap实现了更高级的线程安全;
或者使用synchronizedMap() 同步方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。

如:

1
2
3
public static < K ,V> Map< K ,V> synchronizedMap(Map< K ,V> m) {
         return new SynchronizedMap<>(m);
     }

  

3.常用方法

(1)Map接口定义的方法

HashMap可以应用所有Map接口定义的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public  interface  Map<K,V> {
     public  static  interface  Entry<K,V> {
         //获取该Entry的key
         public  abstract  Object getKey();
         //获取该Entry的value
         public  abstract  Object getValue();
         //设置Entry的value 
         public  abstract  Object setValue(Object obj);
         public  abstract  boolean  equals(Object obj);
         public  abstract  int  hashCode();
     }
     
     //返回键值对的数目 
     int  size();
     //判断容器是否为空 
     boolean  isEmpty();
     //判断容器是否包含关键字key 
     boolean  containsKey(Object key);
     //判断容器是否包含值value
     boolean  containsValue(Object value);
      //根据key获取value 
     Object get(Object key);
      //向容器中加入新的key-value对 
     Object put(Object key, Object value);
     //根据key移除相应的键值对 
     Object remove(Object key);
     //将另一个Map中的所有键值对都添加进去 
     void  putAll(Map<?  extends  K, ?  extends  V> m);
     //清除容器中的所有键值对 
     void  clear();
     //返回容器中所有的key组成的Set集合 
     Set keySet();
     //返回所有的value组成的集合 
     Collection values();
      //返回所有的键值对 
     Set<Map.Entry<K, V>> entrySet();
     
     //继承自Object的方法
     boolean  equals(Object obj);
     int  hashCode();
}

 (2)构造方法 

HashMap使用Entry[] 数组存储数据,
另外维护了两个非常重要的变量:initialCapacity(初始容量)、loadFactor(加载因子)

初始容量就是初始构造数组的大小,可以指定任何值,
但最后HashMap内部都会将其转成一个大于指定值的最小的2的幂,比如指定初始容量12,但最后会变成16,指定16,最后就是16。
加载因子是控制数组table的饱和度的,默认的加载因子是0.75,

1
DEFAULT_LOAD_FACTOR =  0 .75f;

也就是数组达到容量的75%,就会自动的扩容。

另外,HashMap的最大容量是2^30,
static final int MAXIMUM_CAPACITY = 1 << 30;
默认的初始化大小是16,
static final int DEFAULT_INITIAL_CAPACITY = 16;
HashMap提供了四种构造方法,可以使用默认的容量等进行初始化,
也可以显式制定大小和加载因子,还可以使用另外的map进行构造和初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public  HashMap() {
     this .loadFactor = DEFAULT_LOAD_FACTOR;
     threshold = ( int )(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
     table =  new  Entry[DEFAULT_INITIAL_CAPACITY];
      init();
     }
     
public  HashMap(Map<?  extends  K, ?  extends  V> m) {
         this (Math.max(( int ) (m.size() / DEFAULT_LOAD_FACTOR) +  1 ,
                       DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
         putAllForCreate(m);
     }
  
  public  HashMap( int  initialCapacity) {
         this (initialCapacity, DEFAULT_LOAD_FACTOR);
     }
  
   public  HashMap( int  initialCapacity,  float  loadFactor) {
       ......
   }

 

4.解决哈希冲突的办法

(1)什么是哈希冲突

理论上哈希函数的输入域是无限的,优秀的哈希函数可以将冲突减少到最低,但是却不能避免,下面是一个典型的哈希冲突的例子:

用班级同学做比喻,现有如下同学数据

张三,李四,王五,赵刚,吴露.....
假如我们编址规则为取姓氏中姓的开头字母在字母表的相对位置作为地址,则会产生如下的哈希表

位置 字母 姓名  
0 a    
1 b    
2 c    

...

10    L     李四  

...

22 W 王五,吴露  

..

25  张三,赵刚  


我们注意到,灰色背景标示的两行里面,关键字王五,吴露被编到了同一个位置,关键字张三,赵刚也被编到了同一个位置。老师再拿号来找张三,座位上有两个人,"你们俩谁是张三?"(这段描述很形象,引用自hash是如何处理冲突的?

(2)解决哈希冲突的方法

常见的办法开放定址法,再哈希法,链地址法以及建立一个公共溢出区等,这里只考察链地址法。
链地址法就是最开始我们提到的链表-数组结构,


将所有关键字为同义词的记录存储在同一线性链表中。

 

5.源码分析

(1)HashMap的存取实现

HashMap的存取主要是put和get操作的实现。

执行put方法时根据key的hash值来计算放到table数组的下标,
如果hash到相同的下标,则新put进去的元素放到Entry链的头部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public  V put(K key, V value) {
         if  (key ==  null )
             return  putForNullKey(value);
         int  hash = hash(key.hashCode());
         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.equals(k))) {
                 V oldValue = e.value;
                 e.value = value;
                 e.recordAccess( this );
                 return  oldValue;
             }
         }
 
         modCount++;
         addEntry(hash, key, value, i);
         return  null ;
     }

 get操作的实现:

1
2
3
4
5
6
7
8
9
10
public  V get(Object key) {
         if  (key ==  null )
             return  getForNullKey();
         int  hash = hash(key.hashCode());
         for  (Entry<K,V> e = table[indexFor(hash, table.length)];e !=  null ;e = e.next) <br>        {   Object k;
             if  (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                 return  e.value;
         }
         return  null ;
     }

  

注意HashMap支持key=null的情况,看这个代码:

1
2
3
4
5
6
7
8
9
10
11
private  V putForNullKey(V value) {
         for  (Entry<K,V> e = table[ 0 ]; e !=  null ; e = e.next) {
             if  (e.key ==  null ) {
                 V oldValue = e.value;
                 e.value = value;
                 e.recordAccess( this );
                 return  oldValue;
             }
         }
         ......
     }

(2)哈希函数

下面看一下HashMap使用的哈希函数,源码来自JDK1.6:

1
2
3
4
5
6
7
8
9
10
11
/**
      * 哈希函数
      * 看一下具体的操作,首先对h分别进行无符号右移20位和12位,
      * 然后对两个值进行按位异或,最后再与h进行按位异或,
      * 得到新的h后再进行一次同样的操作,分别右移7位和4位,具体的哈希函数优劣就不去研究了
      * 这个方法可以尽量减少碰撞
      */
     static  int  hash( int  h) {
         h ^= (h >>>  20 ) ^ (h >>>  12 );
         return  h ^ (h >>>  7 ) ^ (h >>>  4 );
     }

  (3)再散列rehash过程

当哈希表的容量超过默认容量时,必须调整table的大小。
当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,需要创建一个新的table数组,将table数组的元素转移到新的table数组中。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
      * 再散列过程
      * Rehashes the contents of this map into a new array with a
      * larger capacity.  This method is called automatically when the
      * number of keys in this map reaches its threshold.
      */
     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];
         transfer(newTable);
         table = newTable;
         threshold = ( int )(newCapacity * loadFactor);
     }
 
     /**
      * 把当前Entry[]表的全部元素转移到新的table中
      */
     void  transfer(Entry[] newTable) {
         Entry[] src = table;
         int  newCapacity = newTable.length;
         for  ( int  j =  0 ; j < src.length; j++) {
             Entry<K,V> e = src[j];
             if  (e !=  null ) {
                 src[j] =  null ;
                 do  {
                     Entry<K,V> next = e.next;
                     int  i = indexFor(e.hash, newCapacity);
                     e.next = newTable[i];
                     newTable[i] = e;
                     e = next;
                 while  (e !=  null );
             }
         }
     }

  


本文转自邴越博客园博客,原文链接:http://www.cnblogs.com/binyue/p/4428404.html,如需转载请自行联系原作者

相关文章
|
7天前
|
数据采集 运维 前端开发
【Java】全套云HIS源码包含EMR、LIS (医院信息化建设)
系统技术特点:采用前后端分离架构,前端由Angular、JavaScript开发;后端使用Java语言开发。
27 5
|
6天前
|
存储 Java 容器
HashMap 的基本操作【集合容器知识回顾 ⑤】
本文介绍了HashMap的基本操作,包括创建对象、添加、获取、删除和替换元素、获取所有key的集合、遍历HashMap,以及如何存储自定义类型键值对,并强调了当使用自定义对象作为键时需要重写equals和hashCode方法以确保正确的行为。
HashMap 的基本操作【集合容器知识回顾 ⑤】
|
2月前
|
Kubernetes jenkins 持续交付
从代码到k8s部署应有尽有系列-java源码之String详解
本文详细介绍了一个基于 `gitlab + jenkins + harbor + k8s` 的自动化部署环境搭建流程。其中,`gitlab` 用于代码托管和 CI,`jenkins` 负责 CD 发布,`harbor` 作为镜像仓库,而 `k8s` 则用于运行服务。文章具体介绍了每项工具的部署步骤,并提供了详细的配置信息和示例代码。此外,还特别指出中间件(如 MySQL、Redis 等)应部署在 K8s 之外,以确保服务稳定性和独立性。通过本文,读者可以学习如何在本地环境中搭建一套完整的自动化部署系统。
58 0
|
11天前
|
设计模式 Java
结合HashMap与Java 8的Function和Optional消除ifelse判断
`shigen`是一位致力于记录成长、分享认知和留住感动的博客作者。本文通过具体代码示例探讨了如何优化业务代码中的if-else结构。首先展示了一个典型的if-else处理方法,并指出其弊端;然后引入了策略模式和工厂方法等优化方案,最终利用Java 8的Function和Optional特性简化代码。此外,还提到了其他几种消除if-else的方法,如switch-case、枚举行、SpringBoot的IOC等。一起跟随shigen的脚步,让每一天都有所不同!
27 10
结合HashMap与Java 8的Function和Optional消除ifelse判断
|
19天前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
174 37
|
5天前
|
传感器 监控 数据可视化
【Java】智慧工地解决方案源码和所需关键技术
智慧工地解决方案是一种新的工程全生命周期管理理念。它通过使用各种传感器、数传终端等物联网手段获取工程施工过程信息,并上传到云平台,以保障数据安全。
26 7
|
17天前
|
设计模式 架构师 Java
Java开发工程师转架构师需要学习什么
Java开发工程师转型为架构师需掌握多项技能:精通Java及框架、数据库与分布式系统;熟悉设计模式与架构模式;积累项目经验;提升沟通与领导力;持续学习新技术;培养系统设计与抽象能力;了解中间件及开发工具;并注重个人特质与职业发展。具体路径应结合个人目标与实际情况制定。
42 18
|
1月前
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
102 6
【Java学习】多线程&JUC万字超详解
|
14天前
|
机器学习/深度学习 数据采集 JavaScript
ADR智能监测系统源码,系统采用Java开发,基于SpringBoot框架,前端使用Vue,可自动预警药品不良反应
ADR药品不良反应监测系统是一款智能化工具,用于监测和分析药品不良反应。该系统通过收集和分析病历、处方及实验室数据,快速识别潜在不良反应,提升用药安全性。系统采用Java开发,基于SpringBoot框架,前端使用Vue,具备数据采集、清洗、分析等功能模块,并能生成监测报告辅助医务人员决策。通过集成多种数据源并运用机器学习算法,系统可自动预警药品不良反应,有效减少药害事故,保障公众健康。
ADR智能监测系统源码,系统采用Java开发,基于SpringBoot框架,前端使用Vue,可自动预警药品不良反应
|
2月前
|
存储 Java 数据处理
如何使用 Java 迭代 HashMap 中的 ArrayList
【8月更文挑战第23天】
43 2
下一篇
无影云桌面