HashMap 源码解析(一)之使用、构造以及计算容量
@
简介
HashMap 是基于哈希表的 Map 接口的实现。 它的使用频率是非常的高。
集合和映射
作为集合框架中的一员,在深入之前, 让我们先来简单了解一下集合框架以及 HashMap 在集合框架中的位置。
从图中可以看出
- 集合框架分为两种, 即集合(Collections)和映射(Map)
- HashMap 是 AbstractMap 的子类。而 AbstractMap 实现了 Map, 因此它有 Map 的特性。
- 通过Map接口, 可以生成集合(Collections)。
那集合(Collections)和映射(Map)是什么关系呢?
从图中我们可以看出, Map 和 Collection 是一种并行的关系。可以这么理解:
- 集合(Collectin)是一组单独的元素, 通常应用了某种规则。 List 是按特定顺序来存储元素, 而 Set 存储的是不重复的元素。
- 映射(Map)是一系列 “Key-Value” 的集合。
- 在 Map 中可以通过一定的方法产生 Collection。
HashMap 特点
很多时候, 我们都说, HashMap 具有如下的特点:
- 根据键的 HashCode 存储数据, 具有很快的访问速度;
- 此类不保证映射的顺序,特别是它不保证该顺序恒久不变;
- 允许键为 null, 但最多一条记录;
- 允许多条记录的值为 null;
- 线程不安全。
也许你现在对这些特点的印象还不够深刻, 在后续的源码解析过程中, 可以一一的见识庐山真面目。
使用
HashMap 的使用应该算是很简单的。有以下的方法时使用频率相对来说最高的。
方法名 | 作用 |
---|---|
V put(K key, V value) | 将指定的值与此映射中的指定键关联 |
V get(Object key) | 返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null。 |
int size() | 返回此映射中的键-值映射关系数。 |
V remove(Object key) | 从此映射中移除指定键的映射关系(如果存在)。 |
Set<Map.Entry<K,V>> entrySet() | 返回此映射所包含的映射关系的 Set 视图。 |
Set keySet() | 返回此映射中所包含的键的 Set 视图。 |
以下为一个示例
public void testHashMap() {
HashMap<String, String> animals = new HashMap<String, String>();
animals.put("Tom", "Cat");
animals.put("Tedi", "Dog");
animals.put("Jerry", "Mouse");
animals.put("Don", "Duck");
// 遍历方法1 键值视图
System.out.println("====================KeySet======================");
Set<String> names = animals.keySet();
for (String name:
names) {
System.out.println("KeySet: "+name+" is a " + animals.get(name));
}
// 通过 Entry 进行遍历
System.out.println("==================Entry========================");
Set<Map.Entry<String, String>> entrys= animals.entrySet();
for(Map.Entry<String, String> entry:entrys){
System.out.println("Entry: "+entry.getKey()+" is a " + entry.getValue());
}
animals.remove("Don");
// 通过 KeySet Iterator 进行遍历
System.out.println("======= KeySet Iterator after remove()=============");
Iterator<String > iter = animals.keySet().iterator();
while (iter.hasNext()) {
String name = iter.next();
String pet = animals.get(name);
System.out.println(" KeySet Iterator : "+name+" is a " + pet);
}
animals.clear();
// 通过 Entry Iterator 进行遍历
System.out.println("========== Entry Iterator after clear()==========");
Iterator<Map.Entry<String, String>> entryIter = animals.entrySet().iterator();
while (entryIter.hasNext()) {
Map.Entry<String, String> animal = entryIter.next();
System.out.println(" Entry Iterator : "+animal.getKey()+" is a " + animal.getValue());
}
}
以上的例子对 HashMap 的常用的基本方法进行了使用。
构造
相关属性
/**
* 最大容量, 当传入容量过大时将被这个值替换
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* HashMap的扩容阈值(=负载因子*table的容量),在HashMap中存储的Node键值对超过这个数量时,自动扩容容量为原来的二倍
*/
int threshold;
/**
* 这就是经常提到的负载因子
*/
final float loadFactor;
构造方法
HashMap 的构造方法有四个函数, 第四个暂且先不讲。 前三个基本最后基本都是为了初始化 initialCapacity 和 loadFactor 的。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
该方法是我们最常用的, 将 loadFactor 和 其余参数定义为默认的值。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
当我们需要明确指出我们的容量和负载因子时, 使用该函数。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
当我们需要明确指出我们的容量和负载因子时, 使用该函数。
public HashMap(int initialCapacity, float loadFactor) {
// 初始化的容量不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 初始化容量不大于最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子不能小于 0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
我们观察以上的三个构造构造函数, 发现在其中并没有对存储的对象 table 的初始化, 源码中也没有代码块进行初始化或者其他的。其实是延迟到第一次使用时进行初始化, 在 resize() 中进行了初始化。
在构造函数中,最值得我们深究的就是 tableSizeFor 函数。在初始化时,将这个函数的返回值赋给了 threshold , 并不是说 threshold 就等于这个值了, 在后续会从新计算 threshold 的
tableSizeFor 函数
该函数是获取大于或等于传入容量 initialCapacity 的2的整数次幂。 试想, 如果我们自己来实现这个函数应该怎么实现呢?
一般的算法(效率低, 不值得借鉴)
我们要计算比一个数距离最近的二次幂, 大多数人的想法,应该是一次取2的 0 次幂到 31 逐个与当前的数字进行比较, 第一个大于或等于的值就是我们想要的了。函数大致如下:
public int getNearestPowerOfTwo(int cap){
int num=0;
for (int i = 0; i < 31; i++) {
if ((num = (1 << i)) >= cap){
break;
}
}
return num;
}
这是我随手写的, 还有很大的改进空间, 在这里就不深究了。
tableSizeFor 函数算法
而 HashMap 中的定义如下:
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;
}
我们先不说这个算法的原理, 来看和我之前的函数相比效率。
效率比较
public void compare(){
long start = System.currentTimeMillis();
for (int i = 0; i < (1 << 30); i++) {
getNearestPowerOfTwo(i);
}
long end = System.currentTimeMillis();
System.out.println((end-start));
long start2 = System.currentTimeMillis();
for (int i = 0; i < (1 << 30); i++) {
tableSizeFor(i);
}
long end2 = System.currentTimeMillis();
System.out.println((end2-start2));
}
结果如下:
8094
2453
也就是时间上相比是 3.3 倍左右。接下来让我们看看其实现原理。
tableSizeFor 函数原理
核心思想
将该数的低位二进制位全部变为1, 并加1返回。
举个例子:
低位二进制全部变为1
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
其原理是:
首先, 我们忽略最高位之外的所有位数, 看图解说:
Step 1. 右移 1 位,并与之前的数做或运算。 则紧邻的后 1 位变成了 1. 而此时已经确定了 2 个 1, 因此下一次可以右移2位。
Step 2. 右移 2 位,并与之前的数做或运算, 则紧邻的后 2 也变成了 1. 而此时已经确定了 4 个 1, 因此下一次可以右移 4 位。
Step 3. 右移 4 位,并与之前的数做或运算, 则紧邻的后 4 位也变成了1. 而此时已经确定了8 个 1, 因此下一次可以右移 8 位。
...
依次类推, 最后右移了 31 位。
1 + 2 + 4 + 8 + 16 = 31;
由于 int 类型去掉符号位之后就只剩下 31 位了,因此, 右移了 31 位之后可以保证最高位后面的数字都为 1。
第一步为什么要 n = cap - 1?
如果不做该操作, 则如传入的 cap 是 2 的整数幂, 则返回值是预想的 2 倍。
HashMap 源码解析(一)之使用、构造以及计算容量的更多相关文章
- Java中的容器(集合)之HashMap源码解析
1.HashMap源码解析(JDK8) 基础原理: 对比上一篇<Java中的容器(集合)之ArrayList源码解析>而言,本篇只解析HashMap常用的核心方法的源码. HashMap是 ...
- 最全的HashMap源码解析!
HashMap源码解析 HashMap采用键值对形式的存储结构,每个key对应唯一的value,查询和修改的速度很快,能到到O(1)的平均复杂度.他是非线程安全的,且不能保证元素的存储顺序. 他的关系 ...
- 【转】Java HashMap 源码解析(好文章)
.fluid-width-video-wrapper { width: 100%; position: relative; padding: 0; } .fluid-width-video-wra ...
- HashMap源码解析 非原创
Stack过时的类,使用Deque重新实现. HashCode和equals的关系 HashCode为hash码,用于散列数组中的存储时HashMap进行散列映射. equals方法适用于比较两个对象 ...
- HashMap源码解析和设计解读
HashMap源码解析 想要理解HashMap底层数据的存储形式,底层原理,最好的形式就是读它的源码,但是说实话,源码的注释说明全是英文,英文不是非常好的朋友读起来真的非常吃力,我基本上看了差不多 ...
- 详解HashMap源码解析(下)
上文详解HashMap源码解析(上)介绍了HashMap整体介绍了一下数据结构,主要属性字段,获取数组的索引下标,以及几个构造方法.本文重点讲解元素的添加.查找.扩容等主要方法. 添加元素 put(K ...
- [源码解析] PyTorch 流水线并行实现 (5)--计算依赖
[源码解析] PyTorch 流水线并行实现 (5)--计算依赖 目录 [源码解析] PyTorch 流水线并行实现 (5)--计算依赖 0x00 摘要 0x01 前文回顾 0x02 计算依赖 0x0 ...
- [源码解析] TensorFlow 分布式之 MirroredStrategy 分发计算
[源码解析] TensorFlow 分布式之 MirroredStrategy 分发计算 目录 [源码解析] TensorFlow 分布式之 MirroredStrategy 分发计算 0x1. 运行 ...
- HashMap 源码解析
HashMap简介: HashMap在日常的开发中应用的非常之广泛,它是基于Hash表,实现了Map接口,以键值对(key-value)形式进行数据存储,HashMap在数据结构上使用的是数组+链表. ...
随机推荐
- 用JS实现判断iframe是否加载完成
本文出至:新太潮流网络博客 var iframe = document.createElement("iframe"); iframe.src = "blog.iinu. ...
- 如何扩展 Azure 资源组中虚拟机的 OS 驱动器
概述 在资源组中通过从 Azure 应用商店部署映像来创建新的虚拟机 (VM) 时,默认的 OS 驱动器空间为 127 GB. 尽管可以将数据磁盘添加到 VM(数量取决于所选择的 SKU),并且我们建 ...
- linux操作系统不重启添加raid0步骤
1.限制:本步骤仅适用于LSI芯片的raid卡,可以通过以下蓝色指令判断是否LSI芯片 [root@HKC-Lab-CDN ~]# lspci | grep -i lsi 03:00.0 RAID b ...
- python基础学习16----模块
模块(Module)的引入 import 模块名 调用模块中的函数的方式为 模块名.函数名 这种方式引入会相当于将模块中代码执行一遍,所以如果引入的模块中有输出语句,那么只写import 模块名,运行 ...
- Nginx+uWSGI+Django部署web服务器
目录 Nginx+uWSGI+Django部署web服务器 环境说明 前言 搭建项目 Django部署 编辑luffy/luffy/settings.py 编辑luffy/app01/views.py ...
- sourceTree跳过注册
sourceTree是一个很方便的git管理工具,但是现在一直无法注册,本文记录了跳过注册的方法. 将下面的代码赋值到地址栏 %LocalAppData%\Atlassian\SourceTree\ ...
- BZOJ1011:[HNOI2008]遥远的行星(乱搞)
Description 直线上N颗行星,X=i处有行星i,行星J受到行星I的作用力,当且仅当i<=AJ.此时J受到作用力的大小为 Fi->j=Mi*Mj/(j-i) 其中A为很小的常量, ...
- Flume学习之路 (二)Flume的Source类型
一.概述 官方文档介绍:http://flume.apache.org/FlumeUserGuide.html#flume-sources 二.Flume Sources 描述 2.1 Avro So ...
- 2.3.1 TextView(文本框)详解
http://www.runoob.com/w3cnote/android-tutorial-textview.html 1.基础属性详解: 通过下面这个简单的界面,我们来了解几个最基本的属性: 布局 ...
- dubbo接口访问控制
微服务背景下,一个web应用都可能不再service依赖,而是通过RPC调用远端服务器上的服务.这些服务里,就包括了一些不能轻易暴露的后台功能接口.暴露出去的dubbo接口注册到某一个zk上后,该du ...