本文基于JDK1.7
虽然很早之前看过hashMap源码,但是还是需要实战一下,手写了一个类似的HashMap,加深对集合类的理解。代码我加上了详细注释。
问题:
- 如何将元素放入数组对应位置? 即寻址过程
- 不同元素在数组位置相同,发生冲突如何解决?
- 数组容量不足,怎么扩容?
搞清楚这三个问题,我们就可以很轻松完成自己的HashMap:
自己写的Map,就简单点,不需要像源码那样考虑健壮性,只需要弄清大致流程即可。
先定义一个SelfMap抽象接口:
public interface SelfMap<K, V> {
V put(K k, V v);
V get(K k);
int size();
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
}
}
Node节点是存放在数组和链表的元素,是一个键值对。
下面开始实现上面Map:
定义常量:
//定义数组默认容量为16
private int capacity = 1 << 4;
//数组实际元素数量
private int size = 0;
//负载因子
private float loadFactor = 0.5f;
//数组,初始默认为空,第一次add时创建
Node<K, V>[] table = null;
定义四个常量,其中数组我们使用lazyInit,第一次添加的时候初始化数组
具体方法实现
需要弄清楚以下问题:
- 第一次放数据初始化数组
- 判断是否需要扩容
- 键为null需要放到数组index=0的位置
- 计算Index = hash & (array.length - 1)
- 何时put直接覆盖value(相同对象)?
- hash冲突使用头插法构造链表
- 扩容后同一链表元素在新数组位置相同吗?链表元素顺序还能保持吗?
@Override
public V put(K k, V v) {
// 判断table 数组大小是否为空(如果为空的情况下 ,做初始化操作)
if (table == null) {
//初始化数组 懒加载
table = new Node[capacity];
}
//首先判断是否需要扩容
if(size > loadFactor * capacity) {
resize();
}
// if k=null 需要插入到0位置(针对键为null情况)
if(k == null) {
//初始化Node
table = new Node[capacity];
Node<K, V> node = table[0];
//0位置本身有值,替换value
if(node != null) {
return node.setValue(v);
}
//0位置为空,直接放入
node = new Node<K, V>(null, v, null);
}
// 计算index
int index = getIndex(k, capacity);
//定位index位置元素
Node<K, V> node = table[index];
Node<K, V> newNode = node;
// 判断index是否有值(即是否发生Hash冲突),不冲突的话直接放入
if(node == null) {
//构建新节点直接放入
node = new Node<K, V>(k, v, null);
size++;
}
//冲突,使用头插法
while (newNode != null) {
//不为空先判断是否为同一对象
if(newNode.getKey() == k || newNode.getKey().equals(k)) { //保证hashCode相等,equals相等,但是不一定是一个对象,比如两个String和类型数据,覆盖value
return newNode.setValue(v);
}
//遍历到最后一个节点,没有发现相同节点,此时需要插入到头结点
if(newNode.next == null) {
node = new Node<>(k, v, node);
}
newNode = newNode.next;
}
table[index] = node;
return null;
}
/**
* 获取元素
**/
@Override
public V get(K k) {
if(k == null) {
//判断map集合是否为空
if(size == 0) {
return null;
}
return table[0].getValue();
}
int index = getIndex(k,table.length);
return table[index] == null? null : table[index].getValue();
}
@Override
public int size() {
return size;
}
/**
* 扩容方法
*/
public void resize() {
//数组长度变为原来的两倍
Node<K, V>[] newTable = new Node[capacity << 1];
//重新计算index
for (int i = 0; i < table.length; i++) {
Node<K, V> e = table[i];
while (e != null) {
//重新计算index
int position = getIndex(e.getKey(), table.length << 1);
Node<K, V> next = e.next;
//让当前节点的下一节点指向新数组指定位置链表的头
e.next = newTable[position];
//头插法插入
newTable[position] = e;
//移动到链表的下一位置
e = next;
}
}
table = newTable;
capacity = newTable.length;
//去掉引用,方便垃圾回收
newTable = null;
}
/**
* 定位元素数组位置
* @param k
* @param length
* @return
*/
public int getIndex(K k, int length) {
int hashCode = k.hashCode();
int index = hashCode & (length-1);
return index;
}
class Node<K, V> implements Entry<K, V> {
private K key;
private V value;
private Node<K, V> next;
public Node(K key, V value, Node<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
@Override
public K getKey() {
return this.key;
}
@Override
public V getValue() {
return this.value;
}
@Override
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
}
上面问题回答
- 何时put直接覆盖value(相同对象)?
注意,并不是相同对象才可以,对于String类型,不同对象相同值也会覆盖。jdk中有一个IdentityHashMap是判断是否为同一对象进行覆盖的
- 扩容后同一链表元素在新数组位置相同吗?链表元素顺序还能保持吗?
有可能相同也有可能不同,如果相同,生成的链表顺序会出现倒序,比如开始三个节点顺序为A->B->C,在新数组的顺序为C->B->A,但是在jdk1.8中顺序不会改变。