HashMap基于hashing原理,通过put(key,value)和get(key)方法存储和获取对象,当我们将键值对传递给put()方法时,它调用见对象的hashcode方法来计算hashcode值,然后找到bucket存储位置来存储值对象;获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象,HashMap使用链表来解决碰撞问题,当发生碰撞,对象将会以键值对的方式存储在LinkedList链表的节点中,链表节点中存储方式是以键值对的方式存储的。
当两个不同的键对象,它的hashcode相同时,会发生什么? 它们会存储在同一个bucket位置的LinkedList链表中,键对象的equals方法用来找到键值对对象,下边通过源码来查看一下:
首先:1、初始化HashMap,判断设置的容量和负载因子是否合理,2、设置实参传进来的负载因子,临界值等等
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);
// 设置负载因子,临界值此时为容量大小,后面第一次put时由inflateTable(int toSize)方法计算设置
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
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);
}
然后是put方法,1、监测table是不是一个空的map表,是则初始化,2、判断key是否为空,是则将此键值对添加到table[0]位置,遍历该链表,如果有key为null,替换其value,无则创建新的Entry对象,由此可知,在table[0]的位置上是无法形成链表的,最多只有一个Entry对象,key为null的Entry对象存储在这里,3、当key不为null的时候,用hash()获取key的哈希值,然后指定hash值在对应table上的bucket位置索引,在bucket哈希桶中遍历LinkedList,判断当前key是否存在,如果存在(这就是hash碰撞,如果发生了hash冲突,用key对象的hash值和equals判断LinkedList中有无完全相同的对象)就用当前value替换原有的,如果key是一个全新的,就addEntry键值对对象,
public V put(K key, V value) {
// 如果table引用指向成员变量EMPTY_TABLE,那么初始化HashMap(设置容量、临界值,新的Entry数组引用)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 若“key为null”,则将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头
// 所以table[0]的位置上,永远最多存储1个Entry对象,形成不了链表。key为null的Entry存在这里
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值
int hash = hash(key);
// 搜索指定hash值在对应table中的索引
int i = indexFor(hash, table.length);
// 循环遍历table数组上的Entry对象,判断该位置上key是否已存在
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))) {
// 如果这个key对应的键值对已经存在,就用新的value代替老的value,然后退出!
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 修改次数+1
modCount++;
// table数组中没有key对应的键值对,就将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
接下来了解一下addEntry()方法,也就是怎么样去形成一个LinkedList单链表,将Entry对象添加到数组bucketIndex位置对于的哈希桶中,并判断数组是否需要扩容,2、在链表中添加一个新的Entry对象,放到表头,这个链表是一个先进后出的规则
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
/** 指向下一个元素的引用 */
Entry<K,V> next;
int hash;
/**
* 构造方法为Entry赋值
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
...
...
}
/**
* 将Entry添加到数组bucketIndex位置对应的哈希桶中,并判断数组是否需要扩容
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果数组长度大于等于容量×负载因子,并且要添加的位置为null
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);
}
/**
* 在链表中添加一个新的Entry对象在链表的表头
*/
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++;
}
get方法
如果两个不同的key的hashcode相同,两个值对象储存在同一个bucket位置,要获取value,我们调用get()方法,HashMap会使用key的hashcode找到bucket位置,因为HashMap在链表中存储的是Entry键值对,所以找到bucket位置之后,会调用key的equals()方法,按顺序遍历链表的每个 Entry,直到找到想获取的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那HashMap必须循环到最后才能找到该元素。
get()方法源码如下:
public V get(Object key) {
// 若key为null,遍历table[0]处的链表(实际上要么没有元素,要么只有一个Entry对象),取出key为null的value
if (key == null)
return getForNullKey();
// 若key不为null,用key获取Entry对象
Entry<K,V> entry = getEntry(key);
// 若链表中找到的Entry不为null,返回该Entry中的value
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
// 计算key的hash值
int hash = (key == null) ? 0 : hash(key);
// 计算key在数组中对应位置,遍历该位置的链表
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 若key完全相同,返回链表中对应的Entry对象
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
// 链表中没找到对应的key,返回null
return null;
}
hash算法
我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。
源码分析:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
做一个HashMap的小demo
package cn.lzrabbit.structure;
/**
* Created by rabbit on 14-5-4.
*/
public class MyHashMap {
//默认初始化大小 16
private static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认负载因子 0.75
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
//临界值
private int threshold;
//元素个数
private int size;
//扩容次数
private int resize;
private HashEntry[] table;
public MyHashMap() {
table = new HashEntry[DEFAULT_INITIAL_CAPACITY];
threshold = (int) (DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
size = 0;
}
private int index(Object key) {
//根据key的hashcode和table长度取模计算key在table中的位置
return key.hashCode() % table.length;
}
public void put(Object key, Object value) {
//key为null时需要特殊处理,为简化实现忽略null值
if (key == null) return;
int index = index(key);
//遍历index位置的entry,若找到重复key则更新对应entry的值,然后返回
HashEntry entry = table[index];
while (entry != null) {
if (entry.getKey().hashCode() == key.hashCode() && (entry.getKey() == key || entry.getKey().equals(key))) {
entry.setValue(value);
return;
}
entry = entry.getNext();
}
//若index位置没有entry或者未找到重复的key,则将新key添加到table的index位置
add(index, key, value);
}
private void add(int index, Object key, Object value) {
//将新的entry放到table的index位置第一个,若原来有值则以链表形式存放
HashEntry entry = new HashEntry(key, value, table[index]);
table[index] = entry;
//判断size是否达到临界值,若已达到则进行扩容,将table的capacicy翻倍
if (size++ >= threshold) {
resize(table.length * 2);
}
}
private void resize(int capacity) {
if (capacity <= table.length) return;
HashEntry[] newTable = new HashEntry[capacity];
//遍历原table,将每个entry都重新计算hash放入newTable中
for (int i = 0; i < table.length; i++) {
HashEntry old = table[i];
while (old != null) {
HashEntry next = old.getNext();
int index = index(old.getKey());
old.setNext(newTable[index]);
newTable[index] = old;
old = next;
}
}
//用newTable替table
table = newTable;
//修改临界值
threshold = (int) (table.length * DEFAULT_LOAD_FACTOR);
resize++;
}
public Object get(Object key) {
//这里简化处理,忽略null值
if (key == null) return null;
HashEntry entry = getEntry(key);
return entry == null ? null : entry.getValue();
}
public HashEntry getEntry(Object key) {
HashEntry entry = table[index(key)];
while (entry != null) {
if (entry.getKey().hashCode() == key.hashCode() && (entry.getKey() == key || entry.getKey().equals(key))) {
return entry;
}
entry = entry.getNext();
}
return null;
}
public void remove(Object key) {
if (key == null) return;
int index = index(key);
HashEntry pre = null;
HashEntry entry = table[index];
while (entry != null) {
if (entry.getKey().hashCode() == key.hashCode() && (entry.getKey() == key || entry.getKey().equals(key))) {
if (pre == null) table[index] = entry.getNext();
else pre.setNext(entry.getNext());
//如果成功找到并删除,修改size
size--;
return;
}
pre = entry;
entry = entry.getNext();
}
}
public boolean containsKey(Object key) {
if (key == null) return false;
return getEntry(key) != null;
}
public int size() {
return this.size;
}
public void clear() {
for (int i = 0; i < table.length; i++) {
table[i] = null;
}
this.size = 0;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("size:%s capacity:%s resize:%s\n\n", size, table.length, resize));
for (HashEntry entry : table) {
while (entry != null) {
sb.append(entry.getKey() + ":" + entry.getValue() + "\n");
entry = entry.getNext();
}
}
return sb.toString();
}
}
class HashEntry {
private final Object key;
private Object value;
private HashEntry next;
public HashEntry(Object key, Object value, HashEntry next) {
this.key = key;
this.value = value;
this.next = next;
}
public Object getKey() {
return key;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
public HashEntry getNext() {
return next;
}
public void setNext(HashEntry next) {
this.next = next;
}
}
MyHashMap