HashMap和HashTable源码分析
HashMap
HashMap是一个实现了Map接口的Hash表。提供所有Map的操作,并且允许null
key和null value。HashMap几乎等同于HashTable,只不过HashMap不是线程安全的。
如果hash函数将元素在buckets之间合适的散列的话,HashMap能提供常量时间的get和put操作性能。迭代操作所需时间与HashMap的capacity成正比,因此在需要快速迭代的场合,就不应该将初始capacity设置的很大。
有两个参数会影响HashMap的性能:初始capacity,加载因子factor。capacity是hash表的buckets的数目,factor指定hash表达到怎样的容量时capacity会自动增加。当hash表的entries数目超过了hash表的factor和capacity,hash表将会被rehashed,内部数据结构会被重建,hash表会增加至将近buckets的两倍。
一般情况下,基于时间和空间上的考虑,factor=0.75,值太大会减少空间但会增加查找时间。如果有大量的映射需要存储到HashMap中去,创建一个具有较大值的capacity的HashMap在性能上优于HashMap自己自动增长Hash表。
当多个key具有相同的hashCode(),也会影响性能。当key是Comparable时,为了解决冲突,会对这些key使用比较方法。
哈希表
数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。 哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。
哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法—— 拉链法,我们可以理解为“链表的数组” ,如图:
从上图我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。
首先HashMap里面实现一个静态内部类Entry,其重要的属性有
key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
transient Entry[] table;
HashMap的存取实现
既然是线性数组,为什么能随机存取?这里HashMap用了一个小算法,大致是这样实现:
int hash
= key.hashCode(); // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值
int index
= hash % Entry[].length;
Entry[index] = value;
// 取值时:
int hash
= key.hashCode();
int index
= hash % Entry[].length;
return Entry[index];
put
这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方,
第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] =
A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现,我们应该已经清楚了。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value); //null总是放在数组的第一个链表中
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;
}
void int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //如果size超过threshold,则扩充table大小。再散列
if (size++ >= threshold)
resize(2 * table.length);
}
当然HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个index的链就会很长,会不会影响性能?HashMap里面设置一个因子,随着map的size越来越大,Entry[]会以一定的规则加长长度。
get
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) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
null key的存取
null key总是存放在Entry[]数组的第一个元素。
private V putForNullKey(V value) {
for (Entry<K,V> e = null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
确定数组index:hashcode % table.length取模
HashMap存取时,都需要计算当前key应该对应Entry[]数组哪个元素,即计算数组下标;算法如下:
/**
* Returns index for hash code h.
*/
static int int h, int length) {
return h & (length-1);
}
table初始大小
public HashMap(int initialCapacity, float loadFactor) {
..... // Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
new Entry[capacity];
init();
}
注意table初始大小并不是构造函数中的initialCapacity!!而是 >= initialCapacity的2的n次幂!!!!
————为什么这么设计呢?——
解决hash冲突的办法
- 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
- 再哈希法
- 链地址法
- 建立一个公共溢出区
java中hashmap的解决办法就是采用的链地址法。
再散列rehash过程
当哈希表的容量超过默认容量时,必须调整table的大小。当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,需要创建一张新表,将原表的映射到新表中。
/**
* 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.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
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);
}
/**
* Transfers all entries from current table to newTable.
*/
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;
//重新计算index
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
Hashtable
功能几乎等同于HashMap,主要有以下三个区别:
1.Hashtable继承于Dictionary,HashMap继承于AbstractMap,两个类都实现了Map<K,V>, Cloneable, Serializable方法
2.Hashtable是线程安全的,HashMap不是线程安全的
3.HashMap允许null key和null value,Hashtable不允许
HashMap和HashTable源码分析的更多相关文章
- 并发-HashMap和HashTable源码分析
HashMap和HashTable源码分析 参考: https://blog.csdn.net/luanlouis/article/details/41576373 http://www.cnblog ...
- Java入门系列之集合Hashtable源码分析(十一)
前言 上一节我们实现了散列算法并对冲突解决我们使用了开放地址法和链地址法两种方式,本节我们来详细分析源码,看看源码中对于冲突是使用的哪一种方式以及对比我们所实现的,有哪些可以进行改造的地方. Hash ...
- 史上最简单的的HashTable源码分析
HashTable源码分析 1.前言 Hashtable 一个元老级的集合类,早在 JDK 1.0 就诞生了 1.1.摘要 在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap.Link ...
- HashMap原理及源码分析
HashMap 原理及源码分析 1. 存储结构 HashMap 内部是由 Node 类型的数组实现的.Node 包含着键值对,内部有四个字段,从 next 字段我们可以看出,Node 是一个链表.即数 ...
- HashMap与TreeMap源码分析
1. 引言 在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...
- (转)HashMap和HashTable源码
转自: http://www.cnblogs.com/ITtangtang/p/3948406.html http://frankfan915.iteye.com/blog/1152091 一.Has ...
- HashMap:从源码分析到面试题
1 HashMap简介 HashMap是实现map接口的一个重要实现类,在我们无论是日常还是面试,以及工作中都是一个经常用到角色.它的结构如下: 它的底层是用我们的哈希表和红黑树组成的.所以我们在学习 ...
- JDK1.8 HashMap中put源码分析
一.存储结构 在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构.它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置.Hash ...
- JAVA的HashTable源码分析
Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长.Hashtable ...
随机推荐
- 实验吧 Fair Play
知识点:playfair密码以前没见过 Playfair密码(英文:Playfair cipher 或 Playfair square)是一种使用一个关键词方格来加密字符对的加密法,1854年由查尔斯 ...
- MFC 中的 Value 和 Control
一.變量類型不同:control 型变量是这个控件所属类的一个实例(对象),控制對象的變量.即變量代表對象本身.代表這個人!value 是用来传递数据,不能对控件进行其它的操作.向變量傳遞數據.代表這 ...
- 【Hadoop】Hadoop 中 RPC框架原理、代码示例
0.内容 1.hadoop中的RPC框架封装思想 2.Hadoop RPC 实现方法 3.服务调用动态转发和负载均衡的实现思考 4.协议代码: package com.ares.hadoop.rpc; ...
- SSM-CRUD入门项目——修改与PUT请求
修改 分析: 点击编辑,弹出用户修改的模态框, 模态框中显示用户的信息, 点击更新完成修改! 第一步先复制添加员工的模态框进行修改调整,完成修改员工的模态框的创建:(当然,相应的生成员工数 ...
- day5 二值化
1.otsu二值化 # coding=utf-8 import cv2 import numpy as np from matplotlib import pyplot as plt #1.读入图像 ...
- 「PKUWC2018」Minimax
题面 题解 强势安利一波巨佬的$blog$ 线段树合并吼题啊 合并的时候要记一下$A$点权值小于$l$的概率和$A$点权值大于$r$的概率,对$B$点同样做 时空复杂度$\text O(nlogw)$ ...
- 1135: [POI2009]Lyz
1135: [POI2009]Lyz https://lydsy.com/JudgeOnline/problem.php?id=1135 分析: hall定理+线段树连续区间的最大的和. 首先转化为二 ...
- linux下,将一个目录中的图片文件合成为gif图片
# {} 为文件所在目录位置 # {} 为gif图片位置 convert -delay -depth -layers optimize -quality -loop {} {}
- 我们一起学习WCF 第四篇单通讯和双向通讯
前言:由于个人原因很久没有更新这个系列了,我会继续的更新这系列的文章.这一章是单向和双向通讯.所谓的单向就是只有发送却没有回复,双向是既有发送还有回复.就是有来无往代表单向,礼尚往来表示双向.下面我用 ...
- 记录阿里云ECS(Centos7.4)安装mysql 8.0.X服务
#*.rpm介绍 大多数二进制rpm包都包含在名称中倒数第二个字段中编译rpm的体系结构..rpm软件包有那么几种 *.src.rpm 源程序包,要先通过编译才能安装 *.noarch.rpm 该包适 ...