java容器类2:Map及HashMap深入解读

Java的编程过程中经常会和Map打交道,现在我们来一起了解一下Map的底层实现,其中的思想结构对我们平时接口设计和编程也有一定借鉴作用。(以下接口分析都是以jdk1.8源码为参考依据)

java.util.map类图

1. Map

An object that maps keys to values.  A map cannot contain duplicate keys;
each key can map to at most one value.

Map提供三种访问数据的方式: 键值集、数据集、数据-映射,对应下表中的标记为黄色的三个接口。public interface Map<K, V>

 
方法名描述
void clear()从此映射中移除所有映射关系(可选操作)。
boolean containsKey(Object key)如果此映射包含指定键的映射关系,则返回 true。
boolean containsValue(Object value)如果此映射将一个或多个键映射到指定值,则返回 true。
Set<Map.Entry<K,V>> entrySet()返回此映射中包含的映射关系的 Set 视图。
boolean equals(Object o)比较指定的对象与此映射是否相等。
V get(Object key)返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。
int hashCode()返回此映射的哈希码值。
boolean isEmpty()如果此映射未包含键-值映射关系,则返回 true。
Set<K> keySet()返回此映射中包含的键的 Set 视图。
V put(K key, V value)将指定的值与此映射中的指定键关联(可选操作)。
void putAll(Map<? extends K,? extends V> m)从指定映射中将所有映射关系复制到此映射中(可选操作)。
V remove(Object key)如果存在一个键的映射关系,则将其从此映射中移除(可选操作)。
int size()返回此映射中的键-值映射关系数。
Collection<V> values()返回此映射中包含的值的 Collection 视图。

在Java8中的Map有增添了一些新的接口不在上述表格之中,这里不一一列举。

这里涉及到一个静态内部接口:Map.Entry<K,V> ,用于存储一个键值对,该接口中设置set和get键值和value值的接口。

image

所以Map中存储数据都是以这种Entry为数据单元存储的。

2. AbatractMap

AbstractMap中增加了两个非常重要的成员变量:

transient Set<K> keySet;
transient Collection<V> values;

通过这两个成员变量,我们已经知道Map是如何存储数据的了:键值存入keySet中,value存入values中。(由于Map需要保证键值的唯一性所以选择Set作为键值的存储结构,而Value则对此没有任何要求所以选择Collection作为存储结构)

AbstractMap实现了Map中的部分接口,都是通过调用接口:Set<Entry<K,V>> entrySet() 实现的,而该接口的具体实现却留给了具体的子类。以下代码列出了equal()方法的具体实现:

复制代码

 public boolean equals(Object o) {if (o == this)return true;if (!(o instanceof Map))return false;Map<?,?> m = (Map<?,?>) o;if (m.size() != size())return false;try {Iterator<Entry<K,V>> i = 

entrySet().

iterator();while (i.hasNext()) {Entry<K,V> e = i.next();K key = e.getKey();V value = e.getValue();if (value == null) {if (!(m.get(key)==null && m.containsKey(key)))return false;} else {if (!value.equals(m.get(key)))return false;}}} catch (ClassCastException unused) {return false;} catch (NullPointerException unused) {return false;}return true;}

复制代码

3. HashMap

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable除了继承了AbstractMap中HashMap中的两个成员变量以外,又增加了如下几个成员变量:transient Set<Map.Entry<K,V>> entrySet;transient Node<K,V>[] table;transient int size;transient int modCount;作为table存储的基本类型,Node类的源码如下:

View Code

Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。

建议看HashMap源码前了解一些散列表(HashTable)的基础知识:http://www.cnblogs.com/NeilZhang/p/5651492.html

包括:散列函数、碰撞处理、负载因子等。

3.1 hash值计算

复制代码

static final int hash(Object key) {   //jdk1.8 & jdk1.7int h;// h = key.hashCode() 为第一步 取hashCode值// h ^ (h >>> 16)  为第二步 高位参与运算return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

复制代码

首先获取key值的hash值(每个类都有计算hash值的方法),然后将该hash值的高16位异或低16位即得到散列值。

3.2 hash散列函数

       通过hash函数可以得到key值对应的hash值,那么如何通过该hash将key散列到hashtale中呢?下面再介绍一个函数:

对应的运算如下所示:length为table的长度(通常选择2^n)

static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的return h & (length-1);               //第三步 取模运算
}

hashMap哈希算法例图

这里的取模运算等于 hash%length ,然而&运算比%运算的效率更高。

3.3 碰撞算法:HashTable+链表+红黑树

当hash散列函数对不同的值散列到table的同一个位置该如何处理?何时需要扩容table的大小,分配一个更大容量的table?

下面这张网络上流行的图基本解释了当发生碰撞时的处理办法,

hashMap内存结构图

1、HashMap的主要存储为HashTable

2、当散列到的位置已经有元素存在时,通过链表将当前元素链接到table中的元素后面

3、当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能。

红黑树的相关知识可以参考:算法导论 第三部分——基本数据结构——红黑树

3.4 hashtable的扩容

这里先列出了HashMap源码中的几个常量:

复制代码

/*** 默认hashtable的长度 16*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/***  hashtable的最大长度*/static final int MAXIMUM_CAPACITY = 1 << 30;/*** hashtable的默认负载因子*/static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 当Hashtable中链表长度大于该值时,将链表转换成红黑树*/static final int TREEIFY_THRESHOLD = 8;

复制代码

HashMap构造函数可以传入table的初始大小和负载因子的大小:

复制代码

  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;this.threshold = 

tableSizeFor(initialCapacity);

    }

复制代码

这里有一个很巧妙牛逼的tableSizeFor算法:返回一个大于等于且最接近 cap 的2的幂次方整数,如给定9,返回2的4次方16。它的具体实现(全部通过位运算完成):

复制代码

/*** Returns a power of two size for the given target capacity.*/static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

复制代码

那么关键的问题,什么时候会增大table的容量呢?原来table中的Node如何重新散列到新的table中?下面围绕这两个问题展开:

HashMap中有个成员变量 : threshhold,当table中存放的node个数大于该值时就会调用resize()函数,给table重新分配一个2倍的容量的数组(具体可能涉及很多边界问题),并且将原来table中的元素重新散列到扩容的新表中(个人猜想这过程应该是非常耗时的,所以为了避免HashTable不断扩容的操作,使用者可以在构造函数的时候预先设置一个较大容量的table)。

那么这个threshhold的值时如何计算的呢?

1、构造函数的时候赋值: this.threshold = tableSizeFor(initialCapacity);

2、resize()的时候 threshold也会随着table容量的翻倍而翻倍。

3、threshold 的初始值: DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY

这里有个疑问: 通过HashMap()和HashMap(int,int)两种构造函数得得到的threshold值计算方法不同,前一种永远是table.length * 0.75 第二种是通过tableSizeFor(cap)计算所得,为table.length 这时负载因子似乎失去了意义?

HashTable重新散列:

当重新分配了一个table时,需要将原来table中的Node重新散列到新的table中。源码中针对hashtable、链表、红黑树中节点分别作了处理。

1. 如果是table中的值(next为null):直接映射到大的table中,刚看的时候没理解为什么不需要判断如果新位置已经有元素怎么办?

这里不需要考虑大的table中该节点已经有Node了,比如和value | 1111 的元素只有一个(table中不是链表),那么 value | 11111 的元素也一定只有一个。(1111为扩容前table长度减1,11111位扩容后table长度减1)

在扩充HashMap的时候,不需要像JDK1.7的实现那样

2、 如果是链表中的值,则重新散列后他们可能有两种不同的值(增加了一个异或位),需要重新散列到两个位置。

java1.8 重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,HashMap的源码真的有太多精妙的地方了。

3、如果是红黑树中的节点,重新散列后的值也可能出现两种,需要对红黑数进行操作,重新散列(这一块没有具体看源码)。

resize()函数源码:

View Code

3.5 put方法分析

      介绍了上面的这么多下面分析put函数就不是那么难了:

abc

JDK1.8HashMap的put方法源码如下:

复制代码

1 public V put(K key, V value) {2     // 对key的hashCode()做hash3     return putVal(hash(key), key, value, false, true);4 }56 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,7                boolean evict) {8     Node<K,V>[] tab; Node<K,V> p; int n, i;9     // 步骤①:tab为空则创建
10     if ((tab = table) == null || (n = tab.length) == 0)
11         n = (tab = resize()).length;
12     // 步骤②:计算index,并对null做处理 
13     if ((p = tab[i = (n - 1) & hash]) == null)
14         tab[i] = newNode(hash, key, value, null);
15     else {
16         Node<K,V> e; K k;
17         // 步骤③:节点key存在,直接覆盖value
18         if (p.hash == hash &&
19             ((k = p.key) == key || (key != null && key.equals(k))))
20             e = p;
21         // 步骤④:判断该链为红黑树
22         else if (p instanceof TreeNode)
23             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
24         // 步骤⑤:该链为链表
25         else {
26             for (int binCount = 0; ; ++binCount) {
27                 if ((e = p.next) == null) {
28                     p.next = newNode(hash, key,value,null);//链表长度大于8转换为红黑树进行处理
29                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  
30                         treeifyBin(tab, hash);
31                     break;
32                 }// key已经存在直接覆盖value
33                 if (e.hash == hash &&
34                     ((k = e.key) == key || (key != null && key.equals(k))))                                          break;
36                 p = e;
37             }
38         }
39
40         if (e != null) { // existing mapping for key
41             V oldValue = e.value;
42             if (!onlyIfAbsent || oldValue == null)
43                 e.value = value;
44             afterNodeAccess(e);
45             return oldValue;
46         }
47     }48     ++modCount;
49     // 步骤⑥:超过最大容量 就扩容
50     if (++size > threshold)
51         resize();
52     afterNodeInsertion(evict);
53     return null;
54 }

复制代码

 

HashMap实际使用中注意点:

当HashMap的key值为自定义类型时,需要重写它的 equals() 和 hashCode() 两个函数才能得到期望的结果。如下例所示:

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

public class PhoneNumber

{

    private int prefix; //区号

    private int phoneNumber; //电话号

 

    public PhoneNumber(int prefix, int phoneNumber)

    {

        this.prefix = prefix;

        this.phoneNumber = phoneNumber;

    }

 

    @Override

    public boolean equals(Object o)

    {

        if(this == o)

        {

            return true;

        }

        if(!(o instanceof PhoneNumber))

        {

            return false;

        }

        PhoneNumber pn = (PhoneNumber)o;

        return pn.prefix == prefix && pn.phoneNumber == phoneNumber;

    }

 

    @Override

    public int hashCode()

    {

        int result = 17;

        result = 31 * result + prefix;

        result = 31 * result + phoneNumber;

        return result;

    }

}

这里有个疑问: 为什么在put() 一个元素时,不直接调用equals() 判断集合中是否存在相同的元素,而是先调用 hashCode() 看是否有相同hashCode() 元素再通过equal进行确认?

答: 这里是从效率的方面考虑的,一个集合中往往有大量的元素如果一个个调用equals比较必然效率很低。如果两个元素相同他们的hashCode必然相等(反之不成立),先调用hashCode可以过滤大部分元素。

 

HashMap与ArrayMap的区别

        由于HashMap在扩容时需要重建hash table 是一件比较耗时的操作,为了优化性能Androd的系统中提供了ArrayMap,当容量较小时ArrayMap的性能更优。

       ArrayMap使用的是数组存放key值和value值,扩容时只需要重建一个size*2的数组让后将之前的数据拷贝进去,再新添新数据。但是ArrayMap也有缺点: 它在每次put数据时,如果这个key值map中不存在,那么都可能会涉及到数组的拷贝操作。

      HashMap每次put、delete操作(不涉及扩容或者容量重新分配)耗时较小,但是扩容操作时较耗时。

      ArrayMap每次put、delete操作耗时,但是扩容操作不那么耗时。

参考:
http://www.cnblogs.com/NeilZhang/p/5657265.html
http://www.importnew.com/20386.html
ArrayMap :https://blog.csdn.net/hp910315/article/details/48634167

梦想不是浮躁,而是沉淀和积累

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/439629.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

两阶段3D目标检测网络 SIENet: Spatial Information Enhancement Network for 3D Object Detection from Point Cloud

本文介绍一篇两阶段的3D目标检测网络&#xff1a;SIENet。 这里重点是理解本文提出的 Hybrid-Paradigm Region Proposal Network 和 Spatial Information Enhancement module。 论文链接为&#xff1a;https://arxiv.org/abs/2103.15396 项目链接为&#xff1a;https://githu…

java容器类3:set/HastSet/MapSet深入解读

介绍 Set&#xff1a;集合&#xff0c;是一个不包含重复数据的集合。&#xff08;A collection that contains no duplicate elements. &#xff09; set中最多包含一个null元素&#xff0c;否者包含了两个相同的元素&#xff0c;不符合定义。 上一篇学习了Java中的容器类的一…

Bandit算法原理及Python实战

目录 1&#xff09;什么是Bandit算法 为选择而生。 Bandit算法与推荐系统 怎么选择Bandit算法&#xff1f; 2)常用Bandit算法 Thompson sampling算法 UCB算法 Epsilon-Greedy算法 Greedy算法 3&#xff09;Bandit算法Python实战 参考资料&#xff1a; 推荐系统里面有…

ava容器类4:Queue深入解读

Collection的其它两大分支&#xff1a;List和Set在前面已近分析过&#xff0c;这篇来分析一下Queue的底层实现。 前三篇关于Java容器类的文章&#xff1a; java容器类1&#xff1a;Collection,List,ArrayList,LinkedList深入解读 java容器类2&#xff1a;Map及HashMap深入解…

Waymo离线点云序列3D物体检测网络 (3D Auto Labeling): Offboard 3D Object Detection from Point Cloud Sequences

本文介绍一篇Waymo基于点云序列的3D物体检测网络&#xff1a;3D Auto Labeling&#xff0c;论文已收录于CVPR 2021。 这里重点是理解本文提出的 Object-centric Auto Labeling。 论文链接为&#xff1a;https://arxiv.org/abs/2103.05073 2021-09-02补充&#xff1a;本文作者…

Waymo自动驾驶数据集介绍与使用教程

本文将对Waymo自动驾驶数据集&#xff08;Waymo Open Dataset&#xff09;进行介绍。 论文链接为&#xff1a;https://arxiv.org/abs/1912.04838v7 项目链接为&#xff1a;https://github.com/waymo-research/waymo-open-dataset 数据集链接为&#xff1a;https://waymo.com…

Java 并发基础——线程安全性

线程安全&#xff1a;多个线程访问某个类时&#xff0c;不管运行时环境采用何种调度方式或者这些线程将如何交替执行&#xff0c;并且在主调代码中不需要任何额外的同步或协调&#xff0c;这个类都能表现出正确的行为&#xff0c;那么久称这个类是线程安全的。 在线程安全类中封…

详解一阶段3D物体检测网络 SE-SSD: Self-Ensembling Single-Stage Object Detector From Point Cloud

本文介绍一篇一阶段的3D物体检测网络&#xff1a;SE-SSD&#xff0c;论文已收录于 CVPR 2021。 这里重点是理解本文提出的 Consistency Loss 、Orientation-Aware Distance-IoU Loss、Shape-Aware Data Augmentation。 论文链接为&#xff1a;https://arxiv.org/pdf/2104.0980…

详解3D点云分割网络 Cylindrical and Asymmetrical 3D Convolution Networksfor LiDAR Segmentation

本文介绍一篇3D点云分割网络&#xff1a;Cylinder3D&#xff0c;论文已收录于 CVPR 2021。 这里重点是理解本文提出的 Cylindrical Partition 和 Asymmetrical 3D Convolution Network。 论文链接为&#xff1a;https://arxiv.org/pdf/2011.10033.pdf 项目链接为&#xff1a;…

自动驾驶3D物体检测研究综述 3D Object Detection for Autonomous Driving: A Survey

本文介绍一篇最新的自动驾驶3D物体检测研究综述&#xff08;2021年6月份发布&#xff09;&#xff0c;来源于中国人民大学&#xff0c;论文共26页&#xff0c;99篇参考文献。 论文链接为&#xff1a;https://arxiv.org/pdf/2106.10823.pdf 0. Abstract 自动驾驶被看作是避免人…

Java中接口的多继承

我们知道Java的类只能继承一个类&#xff0c;但可以实现多个接口。但是你知道么&#xff1f;Java中的接口却可以继承多个接口。本文就来说一说Java中接口的多继承。 进入主题之前&#xff0c;先扩展一下。Java为什么只支持单继承呢&#xff1f; 我们不妨假设Java支持多继承&a…

详解基于IMU/GPS的行人定位: IMU/GPS Based Pedestrian Localization

本文介绍一篇使用 IMU/GPS 数据融合的行人定位论文&#xff0c;这里重点是理解本文提出的 Stop Detection 和 GPS Correction。 论文地址为&#xff1a;https://www.researchgate.net/publication/261452498_IMUGPS_based_pedestrian_localization 1. Introduction 低成本的 …

每次maven刷新jdk都要重新设置

pom.xml <java.version>17</java.version> 改为<java.version>1.8</java.version>

【CodeForces - 706D】Vasiliy's Multiset(01字典树)

题干&#xff1a; Author has gone out of the stories about Vasiliy, so here is just a formal task description. You are given q queries and a multiset A, initially containing only integer 0. There are three types of queries: " x" — add integer …

详解自动驾驶仿真框架OpenCDA: An Open Cooperative Driving Automation Framework Integrated with Co-Simulation

本文介绍一款同时支持协同驾驶开发与测试、自动驾驶全栈开发 和 CARLA-SUMO联合仿真的开源框架 OpenCDA&#xff0c;论文已收录于 ITSC 2021。主要feature有&#xff1a; 支持CARLA-SUMO联合仿真&#xff0c;CARLA端主管环境渲染、传感器模拟、车辆动力&#xff0c;Sumo端主管…

JavaMonitor 监视器

为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用&#xff1f; 当一个线程需要调用对象的wait()方法的时候&#xff0c;这个线程必须拥有该对象的锁&#xff0c;接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的…

KITTI自动驾驶数据集可视化教程

本文介绍关于自动驾驶数据集KITTI的基本操作&#xff0c;包括Camera和LiDAR可视化教程&#xff0c;源码已上传&#xff1a;https://download.csdn.net/download/cg129054036/20907604 1. 数据准备 将 KITTI 数据 (calib, image_2, label_2, velodyne) 添加到 dataset/KITTI/ob…

重读经典《Quaternion kinematics for the error-state Kalman filter》

本文将介绍一篇关于 四元数运动学的误差卡尔曼滤波 经典论文。本文结构如下&#xff1a; 第1章四元数定义和性质介绍&#xff0c;包括&#xff1a;加法、减法、乘法&#xff08;矩阵表示&#xff09;、模、幂数、指数运算等。第2章旋转群定义和性质介绍&#xff0c;包括&#…

【CodeForces - 789C】Functions again(最大子段和变形,dp,思维)

题干&#xff1a; Something happened in Uzhlyandia again... There are riots on the streets... Famous Uzhlyandian superheroes Shean the Sheep and Stas the Giraffe were called in order to save the situation. Upon the arriving, they found that citizens are wo…

一步步编写操作系统 55 CPL和DPL入门2

接上节。 图中第132行的jmp指令&#xff0c;段选择子为SELECTOR_CODE&#xff0c;其RPL的值为RPL0&#xff0c;RPL0定义在include/boot.inc中&#xff0c;其值为0。选择子的索引部分值为1&#xff0c;表示对应GDT中第1个段描述符&#xff0c;该描述符的DPL为0&#xff0c;&…