pythonhashmap函数_理解HashMap
HashMap源码分析
基于JDK7的HashMap源码分析
类的介绍
下⾯的类介绍是从源码的英⽂翻译来的
HashMap是基于哈希表实现的Map接⼝实现类。这个实现提供所有的map相关的操作,允许使⽤null的键和null的值。(HashMap与Hashtable⼤致是⼀样的,只是HashMap是不同步的,且它允许你null的键和值。);另外,HashMap内部元素排列是⽆序的。
假设哈希函数能将元素合理地分散在各个哈希桶中,那么HashMap的put、get等基础操作的效率会很⾼(时间复杂度是常数级别O(n))。HashMap的迭代所有元素的时间与它的实例的容量(哈希桶的数量)及⼤⼩(键值对的数量)之和成正⽐。因此,如果你很在意HashMap的迭代性能,就不应该初始容量设置得很⾼,或者把负载因⼦设置得很低。
⼀个HashMap的实例有两个参数会影响到它的性能:初始容量和负载因⼦。容量是指哈希表中桶的数量,
初始容量就是哈希表创建时指定的初始⼤⼩。负载因⼦是⼀个度量,⽤来衡量当哈希表的容量满到什么程度时,哈希表就应该⾃动扩容。到哈希表中元素的数量超过负载因⼦和当前容量的乘积时,哈希表会重新计算哈希(rehashed)(即重建内部数据结构),哈希表桶的数量⼤约会变成原来的两倍。
⼀般来说,默认把负载因⼦值设置成0.75,在时间成本和空间成本之间是⽐较好的权衡。该值再⾼⼀点能减少空间开销,但会增加查成本(表现在HashMap类的⼤多数操作中,包括get和put)。所以我们在设置初始化容量时,应该合理考虑预期装载的元素数量以及负载因⼦,从⽽减少rehash的操作次数。如果初始容量⼤于最⼤条⽬数除以加载因⼦(initial capacity > max entries / load factor),则不会发⽣重新加载操作。
如果HashMap的实例需要存储很多元素(键值对),创建HashMap时指定⾜够⼤的容量可以令它的存储效率⽐⾃动扩容⾼很多。
请注意如果很多的键使⽤的hashCode()⽅法结果都相同,那么哈希表的性能会很慢。为了改善影响,当键是Comparable时,HashMap 会⽤这些键的排序来提升效率。
请注意,HashMap是不同步的。如果多条线程同时访问⼀个HashMap,且⾄少有⼀条线程发⽣了结构性改动,那么它必须在外部进⾏同步。(结构性改动是指任何增加或删除键值对的操作,在源码中具体体现是导致modCount属性改动的操作,仅仅修改⼀个键对应的值则不属于结构性改动)。外部同步通
常通过同步⼀个封装了这个map的对象完成。
如果没有这样的对象,那么可以使⽤Collections.synchronizedMap把⼀个map转换成同步的map,这个动作最好在创建的时候完成,避免在转换前意外访问到不同步的map。
Map m = Collections.synchronizedMap(new HashMap(...));
HashMap的迭代器所有集合相关的⽅法都是快速失败的(fail-fast):如果创建迭代器后,除了迭代器⾃⾝的remove⽅法之外,map发⽣了结构性改动,迭代器会抛出ConcurrentModificationException。因此,⾯对并发的修改,迭代吗快速、⼲净利落地失败,⽽不会冒任何风险。
请注意,迭代器快速失败的特性在不同步的并发修改时,是不能作出硬性保证的。快速失败的迭代器会尽最⼤努⼒抛出ConcurrentModificationException。因此,编写依赖于此异常的程序以确保其正确性是错误的:迭代器的快速失败⾏为应该仅⽤于检测错误。
构造函数
HashMap的构造函数⼀共有四种:
⽆参构造,初始容量默认16,负载因⼦默认0.75
指定初始容量,负载因⼦默认0.75
指定初始容量和负载因⼦
通过传⼊的map构造
其中1、2、4都会调⽤第3种构造函数,第4种只是⽤已有的Map构造⼀个HashMap的便捷⽅法,所以这⾥重点看3、4两种构造函数的实现。
public class HashMap
extends AbstractMap
implements Map, Cloneable, Serializable {
//......
// 空表
static final Entry,?>[] EMPTY_TABLE = {};
// 哈希表
transient Entry[] table = (Entry[]) EMPTY_TABLE;
// 容器扩容阈值,当容器⼤⼩(size)达到此值时,容器就会扩容。
// size = 容量 * 负载因⼦
// 如果table == EMPTY_TABLE,那么就会⽤这个值作为初始容量,创建新的哈希表int threshold;
// 负载因⼦
final float loadFactor;
// 构造函数3:指定初始容量和负载因⼦
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();
}
// 构造函数4:⽤传⼊的map构造⼀个新的HashMap
public HashMap(Map extends K, ? extends V> m) {
this(Math.max((int) ( m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
// 分配哈希表空间
inflateTable(threshold);
putAllForCreate(m);
}
//......
}
上⾯的源码中,需要注意⼏点:
扩容阈值默认等于初始容量,16。当哈希表为空表时,HashMap会在内部以该阈值作为初始容量建哈希表,哈希表实质是⼀个数组
inflateTable⽅法就是建⽴哈希表,分配表内存空间的操作(inflate翻译为“膨胀”的意思,后⾯会详述)。但是指定初始容量和负载因⼦的构造⽅法并没有马上调⽤inflateTable。查源码中全部调⽤inflateTable的地⽅有:
graph LR
HashMap构造函数-Map为参数 --> inflateTable
put --> inflateTable
putAll --> inflateTable
clone --> inflateTable
readObject --> inflateTable
初步看上去,只有参数列表是Map的构造函数调⽤了inflateTable,但HashMap(Map map)构造函数内部的逻辑是先调⽤⼀下HashMap(int initialCapacity, float loadFactor)构造函数初始化完了容量和负载因⼦后,再调⽤inflateTable的。所以⼩结⼀点:HashMap在初始化阶段不会马上创建哈希表。
调⽤逻辑
为了更好理解代码的调⽤,下图列出⼀些⽅法之间的调⽤关系:
内部数据结构
HashMap内部维护的数据结构是数组+链表,每个键值对都存储在HashMap的静态内部类Entry中,结构如下图:
put的实现
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);
for (Entry 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;
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果容器⼤⼩⼤于等于阈值,且⽬标桶的entry不等于null
if ((size >= threshold) && (null != table[bucketIndex])) {
// 容器扩容: 哈希表原长度 * 2
resize(2 * table.length);
// 重新计算键的哈希值
hash = (null != key) ? hash(key) : 0;
// 重新计算哈希值对应存储的哈希表的位置
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
在put⽅法内部,会先判断哈希表是不是空表,如果是空表就建⽴哈希表(上⾯提到的内部数据结构中的数组),建好表后,就有空间可以存放键值对了。
要存放键值对,需要先根据key计算哈希码(hash),哈希码返回是⼀个int类型的数值,再根据哈希码计算出在固定长度的数组中存放的位置(下标)
得到下标后,就要在哈希表中到存储的位置。HashMap会先加载指定下标中存放的Entry对象,如果Entry不为空,就⽐较该Entry的hash和key(⽐较key的时候,⽤==和equals来⽐较)。如果跟put进来的hash、key匹配,就覆盖该Entry上的value,然后直接返回旧的value;否则,就该Entry指向的下⼀个Entry,直到最后⼀个Entry为⽌。
如果HashMap加载指定下标中存放的Entry对象是null,⼜或者是完整条Entry链表都没有匹配的hash和key。那么就调⽤addEntry新增⼀个Entry
addEntry⽅法中会做⼀些前置处理。HashMap会判断容器当前存放的键值对数量是否达到了设定的扩容阈值,如果达到了就扩容2倍。扩容后重新计算哈希码,并根据新哈希码和新数组长度重新计算存储位置。做好潜质处理后,就调⽤createEntry新增⼀个Entry。
由于上⾯已经做了前置的处理,createEntry⽅法就不⽤担⼼扩容的问题,放⼼存Entry即可。该⽅法会在给定的下标为⽌存放put进来的key,value,当然这个key,value是包装在Entry中的,让后将Entry指向旧的Entry。
python虚拟机建哈希表的逻辑(inflateTable)
建哈希表是在inflateTable⽅法中实现的:
/**
* 将⼀个数换算成2的n次幂
* @param number
* @return
*/
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
// 理解 Integer.highestOneBit((number - 1) << 1)
// ⽐如 number = 23,23 - 1 = 22,⼆进制是:10110
// 22 左移⼀位(右边补1个0),结果是:101100
// Integer.highestOneBit() 函数的作⽤是取左边最⾼⼀位,其余位取0,
// 即:101100 -> 100000,换成⼗进制就是 32
}
/
**
* inflate有“膨胀”、“充⽓”的意思。
* 理解为初始化哈希表,分配哈希表内存空间
*/
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
// 出⼤于等于toSize的2的n次幂,作为哈希表的容量
int capacity = roundUpToPowerOf2(toSize);
// 计算新的扩容阈值: 容量 * 负载因⼦
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // 指定容量建哈希表
table = new Entry[capacity];
/
/ 根据容量判断是否需要初始化hashSeed
initHashSeedAsNeeded(capacity);
}
理解⼀下roundUpToPowerOf2⽅法:
roundUpToPowerOf2部分计算结果:
roundUpToPowerOf2(0) = 1
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论