- 做过开发的应该都知道hashmap它是java日常开发过程中一种常用的数据结构。但是很多人确还没去了解过它的底层是怎样实现的,今天就让我们一起来看一下hashmap它到底是怎样实现的,它是怎样存储数据的。
首先我们一起来看看hashmap类继承结构
- 从图中我们可以看到Hashmap继承了AbstractMap还分别实现了Map、Cloneable、Serializable接口这也就是说hashmap拥有可序列化和可被克隆的功能,接下来我们再来看hashmap中定义了那些属性,它们分别代表什么意思。
/**
* The default initial capacity
-
MUST be a power of two.
* 默认的初始容量
*/
static
final
int
DEFAULT_INITIAL_CAPACITY
= 1 << 4;
//
aka
16
/**
* 最大的容量
*/
static
final
int
MAXIMUM_CAPACITY
= 1 << 30;
/**
* 加载因子
*/
static
final
float
DEFAULT_LOAD_FACTOR
= 0.75f;
/**
*/
static
final
Entry<?,?>[]
EMPTY_TABLE
= {};
/**
*/
transient
Entry<K,V>[]
table
= (Entry<K,V>[])
EMPTY_TABLE;
/**
* map中的数据数量
*/
transient
int
size;
/**
* The next size value at which to resize (capacity * load factor).
*
@serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
int
threshold;
/**
* The load factor for the hash table.
*
*
@serial
*/
final
float
loadFactor;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views
of
* the HashMap fail-fast.
(See ConcurrentModificationException).
*/
transient
int
modCount;
/**
* The default threshold of map capacity above which alternative hashing is
* used for String keys. Alternative hashing reduces the incidence of
* collisions due to weak hash code calculation for String keys.
* <p/>
* This value may be overridden by defining the system property
* {@code jdk.map.althashing.threshold}. A property value of {@code 1}
* forces alternative hashing to be used at all times whereas
* {@code -1} value ensures that alternative hashing is never used.
*/
static
final
int
ALTERNATIVE_HASHING_THRESHOLD_DEFAULT
= Integer.MAX_VALUE;
- 这里先说一下 DEFAULT_INITIAL_CAPACITY这个是hashmap的默认容量大小,也就是说当我们没有为hashmap指定容量时hashmap默认就是这个大小这里开发hashmap的人员用了一个位移运算,其实就是2^4=16.。接着我们来看DEFAULT_LOAD_FACTOR这个是hashmap的一个加载因子,它的作用主要是在扩容时使用,这个0.75可以说是一个经验值很多人对这个0.75感到疑惑,不理解0.75的意思,这里我就举个例子帮助大家理解:假如我们把一分成四等份,那么每份就是0.25。再拿0.25*3是不是就是等于0.75也就是说这个0.75就是4分之3的意思。再来看一下table这个table也就是我们map存储值的集合。现在我们来看一下这个Entry是个什么鬼。它又是如何定义的:
static
class
Entry<K,V>
implements
Map.Entry<K,V> {
final
K
key;
V
value;
Entry<K,V>
next;
int
hash;
/**
* Creates new entry.
*/
Entry(int
h, K
k, V
v, Entry<K,V>
n) {
value
=
v;
next =
n;
key =
k;
hash =
h;
}
}
- 从Entry的定义不难看出,这个entry是以key value的进行存储的并且是一个单向链表结构。这个Entry也就是存储我们put的值对象,通过这里我们就可以得出hashmap的存储结构其实是数组加链表。
- 我们再来看一下hashmap是如果put数据的:
先来看第一行如果table==EMPTY_TABLE就会调用inflateTable()方法初始化table,如果table==EMPTY_TABLE不成立那么就会看key==null如果成立会调用putForNullkey()方法并返回,如果这个条件也不成立就会调用hash(key)方法算一个hash值根据这个hash值通过indexFor(hash,
table.leng) 方法去算一个table的存储位置也就是下标。接下来会根据这个下标来table数组中找看看有没有如果有会看一下hash值是否相等并且key是否也是相同的,如果是说明已经存在相同的key就会用新值覆盖老值并返回拍老值,如果table没有这个下标的数据那么就直接调用addEntr方法添加。并返回null.
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<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;
}
接着我们看看addEntry方法看看它是怎么添加的首先会判断是否是否需要扩容这里threshold就是阀值这个 threshold是根据容量capacity * loadFactor(加载因子)来算出来的,这里可以看到就算是size >= threshold也不一定一定就会扩容这里还有一个条件就是新增值的下标是不是在table数组中已经存在如果已经存在就会进行扩容。这里可以看到扩容的新容量是原有容量的两倍。扩容后还会重新计算一下hash和下标。然后调用createEntry方法。
void
addEntry(int
hash, K
key, V
value,
int
bucketIndex) {
if
((size
>=
threshold) && (null
!=
table[bucketIndex]))
{
resize(2 *
table.length);
hash = (null
!=
key) ? hash(key)
: 0;
bucketIndex
= indexFor(hash,
table.length);
}
createEntry(hash,
key,
value,
bucketIndex);
}
我们来看一下createEntry方法,这个方法看起来代码很少但是确隐含的了个很巧妙的逻辑,解决hash碰撞。我们来看一下第一行先根据bucketIndex获取了一下table中的数据如果这个数据不为空就说明发生了hash碰撞这时候这个发生碰撞的值就会被当做我们当前添加值的下一个值也就是Entry中的next来存储,也就是说当发生hash碰撞时hashmap是通过链表的方式进行处理的。
void
createEntry(int
hash, K
key, V
value,
int
bucketIndex) {
Entry<K,V>
e =
table[bucketIndex];
table[bucketIndex]
=
new
Entry<>(hash,
key,
value,
e);
size++;
}
我们先来看一下inflateTable方法,这里其实就是为了table数组进行赋值初始化
private
void
inflateTable(int
toSize) {
// Find a power of 2 >= toSize
//这是个取到一个偶数,加入这个toSize=3那么会根据3计算出一个偶数
int
capacity
= roundUpToPowerOf2(toSize);
//这是的threshold是扩容的一个阀值
threshold
= (int) Math.min(capacity
*
loadFactor,
MAXIMUM_CAPACITY
+ 1);
table
=
new
Entry[capacity];
initHashSeedAsNeeded(capacity);
}
方法中再来看putForNullKey方法,首先看看table[0]有没有数据如果有就再看看key是不是==null如果是说明已经有一个为null的key数据存在,就直接用新值覆盖老值并返回老值。如果table[0]中没有数据就调用addEntry方法添加。
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;
}
}
modCount++;
addEntry(0,
null,
value, 0);
return
null;
}
最后我们在来看看扩容方法resize, 首先判断一下当前的容量是否已经等于最大的容量限制,如果已经等于就没有必要在扩容了直接返回。如果不等于就new 一个新长度的Entry数组并且调用trasfer方法对新数据进行赋值(就是将原有table中的数据转移到新数组中的过程),然后把newTable赋值给table这个table最后算一下扩容的一下阀值。
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,
initHashSeedAsNeeded(newCapacity));
table
=
newTable;
threshold
= (int)Math.min(newCapacity
*
loadFactor,
MAXIMUM_CAPACITY
+ 1);
}