前言
HashMap是我们常见的集合类型,实现了Map接口,JDK1.8之前的底层结构为数据+链表实现,JDK1.8对其进行了优化,在原有的数组+链表的基础上增加了红黑树的结构,提升了数据的访问性能。接下来我们通过手写一个简单的HashMap来了解其实现原理。
自定义接口AndyMap,包含get(),put(),size()三个方法,对应与JDK中的Map的相应方法。同时定义内部接口Entry,包含用于获取Key和Value的方法。
public interface AndyMap<K, V> {
V get(K k);
V put(K k, V v);
int size();
interface Entry<K, V> {
K getKey();
V getValue();
}
}
定义AndyHashMap,实现AndyMap类,定义默认容量为16,(这里用位运算进行了操作,提升性能)。定义默认负载因子为0.75,当数组中的元素达到容量的75%时,需要进行扩容处理。如下所示:
public class AndyHashMap<K, V> implements AndyMap<K, V> {
//默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//默认负载因子,阈值比例
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//容量
private int initialCapacity;
//负载因子
private float loadFactor;
//元素表
private Node<K, V>[] table;
//元素数量
private int size;
public AndyHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public AndyHashMap(int defaultInitialCapacity, float defaultLoadFactor) {
if (defaultInitialCapacity < 0) {
throw new IllegalArgumentException("Illegal initial capacity: " +
defaultInitialCapacity);
}
if (defaultLoadFactor <= 0 || Float.isNaN(defaultLoadFactor)) {
throw new IllegalArgumentException("Illegal initial load factor: " +
defaultLoadFactor);
}
this.initialCapacity = defaultInitialCapacity;
this.loadFactor = defaultLoadFactor;
table = new Node[initialCapacity];
}
//剩余代码未贴出
.....
}
上面的代码中我们可以看到构造函数这里采用了外观模式,对外暴露了2个构造参数,实则只有一个。
接下来我们看一下Entry的结构定义,Entry中包含4个主要元素,哈希值hash,key,value以及下一个节点的元素,链表的结构就是通过这里来实现的。
/**
* Entry结构
*
* @param <K>
* @param <V>
*/
class Node<K, V> implements AndyMap.Entry<K, V> {
//key的哈希值
private int hash;
//key
private K key;
//value
private V value;
//下一个节点
private Node<K, V> next;
public Node(int hash, K key, V value, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
}
Hash算法,哈希算法需要尽可能的减少hash碰撞,使元素散列均匀,防止链表过长造成查询性能下降。此处的hash算法是在获取到key的hashCode后对其的高16位和低16位进行了异或操作之后再返回。
/**
* 获取hash值
*
* @param key
* @return
*/
private int hash(K key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
接下来我们看一下map中最常用的两个方法,put和get的实现。put方法传入key和value,返回旧的value,如果是新的key,则返回null。如下所示
/**
* put
*
* @param k
* @param v
* @return
*/
@Override
public V put(K k, V v) {
V oldValue = null;
if (size >= initialCapacity * loadFactor) {
//扩容
resize();
}
int hashCode = hash(k);
int index = hashCode & (initialCapacity - 1);
if (table[index] == null) {
table[index] = new Node<>(hashCode, k, v, null);
size++;
} else {
Node<K, V> node = table[index];
Node<K, V> nextNode = node;
while (nextNode != null) {
if (nextNode.hash == hashCode && (k == nextNode.getKey() || k.equals(nextNode.getKey()))) {
oldValue = nextNode.getValue();
nextNode.value = v;
return oldValue;
}
nextNode = nextNode.next;
}
table[index].next = new Node<>(hashCode, k, v, nextNode);
size++;
}
return oldValue;
}
在上述代码中我们可以看到,put时首先要进行扩容判断,当元素值达到阈值的时候要对数组进行扩容操作,之所以在插入数据前判断扩容是因为扩容操作比较耗时,如果放在插入后再判断扩容,当最后一次put时候正好达到了扩容条件,由于后续没有put操作,造成本次扩容浪费。
扩容完成后获取key在数组中的下标,这里通过hash值和容量进了取模运算,用来得到0到initialCapacity -1的一个数。
当数组对应位置为null时,对数组进行赋值后元素数加1。
当数组对应位置不为空时,需要对table[index]对应的链表进行遍历操作,分两种情况:
- 当key存在的时,新值覆盖旧值之后然后返回旧值。
- 当key不存在的时候,将新的Node对象插入到链表尾部,元素数加1。
扩容和reHash:
每次扩容都是在原来的容量基础上进行double扩容。扩容后要在新的数组上对原来的值重新hash,重新分配存储位置。本例中的重新hash仅是做了重新put操作。
/**
* 扩容
*
*/
private void resize() {
int newSize = initialCapacity << 1;
Node<K, V>[] newTable = new Node[newSize];
initialCapacity = newSize;
reHash(newTable);
}
/**
* 重新hash分配存储位置
*
* @param newTable
*/
private void reHash(Node<K, V>[] newTable) {
List<Node<K, V>> entryList = new ArrayList<>();
for (Node<K, V> node : table) {
if (node != null) {
entryList.add(node);
while (node.next != null) {
entryList.add(node.next);
node = node.next;
}
}
}
table = newTable;
size = 0;
for (Node<K, V> node : entryList) {
put(node.getKey(), node.getValue());
}
}
get操作:
get的操作相对简单,根据hash值获取数组下标,然后遍历链表获取对应的值即可。
/**
* get
*
* @param k
* @return
*/
@Override
public V get(K k) {
int hashCode = hash(k);
int index = hash(k) & (initialCapacity - 1);
if (table[index] == null) {
return null;
} else {
Node<K, V> node = table[index];
while (node != null) {
if (node.hash == hashCode && (k == node.getKey() || k.equals(node.getKey()))) {
return node.value;
} else {
node = node.next;
}
}
}
return null;
}
最后还有一个size方法,返回map中的元素数量即可。size在put时候会进行计算。
/**
* 元素数量
*
* @return
*/
@Override
public int size() {
return size;
}