手写HashMap
介绍
HashMap是一个存储键值对的一个数据结构,它集合了数组、链表和红黑树等多种数据结构。HashMap常用的就是get和put两种操作。
下面是一个miniHashMap的实现,先对于HashMap有一个基本的结构认识,再去研究JDK1.7和1.8HashMap的源码。
初始化
HashMap初始化时,需要两个重要的参数initialCapacity容量和loadFactroy负载因子。在初始化HashMap时,建议设置initialCapacity,防止因为map容量太大,造成多次rehash(扩容)而影响性能。
默认加载因子为0.75,这是在时间和空间成本上寻求一种折衷。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认容量16
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
//容量
private int initialCapacity;
//负载因子
private float loadFactor;
//元素表
private Node[] table;
//元素数量
private int size;
public MyHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public MyHashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("初始化容量异常");
}
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new IllegalArgumentException("初始化加载因子异常");
}
this.initialCapacity = initialCapacity;
this.loadFactor = loadFactor;
table = new Node[initialCapacity];
}
Node节点
Node节点是HashMap中存储数据一个节点,它被实现成一个Node对象,并存放于数组中。
class Node {
private int hash;//哈希码
private K key;
private V value;
private Node next;//下一个哈希重合的对象
public Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
hash函数
hash函数也称为扰动函数。在初始化时我们生成了一个元素表用于存放Node节点的一个table。通过计算key的值最大可能的均匀把Node节点分配到table上,为了尽大可能的分散元素,必须用到这个hash函数
private int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
key.hashCode产生的是一个-232到232范围中的一个数,通过与自己无符号右移异或,产生一个扰动的参数。
右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
put方法
put方法实质就是通过key值,进行hash计算出数组下标,并对该数组下标的元素进行判断,插入或者更新value值。注意的是这里出现位置冲突时,使用了头插法。
public V put(K k, V v) {
V oldValue = null;
if (size >= initialCapacity * loadFactor) {//默认超过容量的75%,扩容
resize();
}
int hashcode = hash(k);//通过hash函数得到的扰乱值
int index = hashcode & (initialCapacity - 1); //计算该元素在数组的位置
if (table[index] == null) {//如果节点为空,插入数据
table[index] = new Node<>(hashcode, k, v, null);
size++;
} else {
Node nextNode = table[index];//获取该节点的数据
while (nextNode != null) {//通过循环判断是否是最后一个节点
if (nextNode.hash == hashcode && (k == nextNode.getKey()) || k.equals(nextNode.getKey())) {//如果hashcode和equals全部相等,覆盖这个值
oldValue = nextNode.getValue();
nextNode.value = v;
return oldValue;//返回修改后的值
}
nextNode = nextNode.next;//如果不相等继续循环
}
//如果插入的节点不在数组中,但是index相等,直接插入原有节点的子节点中
//头插法,最后进的元素在头部
table[index].next = new Node<>(hashcode, k, v, nextNode);
size++;
}
return oldValue;
}
get方法
get方法实质是通过计算key值在元素表中的位置,获取value值。
当出现位置冲突时,使用key的hashcode数组和equals方法进行验证。
public V get(K k) {
int hashCode = hash(k);
int index = hash(k) & (initialCapacity - 1);//获取数组下标
if (table[index] == null) {
return null;
} else {
Node node = table[index];
while (node != null) {
//查找到hashcode或者是key值相等的节点
if (node.hash == hashCode && (k == node.getKey() || k.equals(node.getKey()))) {
return node.value;
} else {
node = node.next;
}
}
}
return null;
}
扩容方法
新生成一个长度为原来的2倍的元素表和一个能存储所有元素的链表。
并再用put方法,把链表中的元素重新put到新的元素表。
这里的扩容方法时间复杂度为n^2,
private void resize() {
//产生新的元素表,长度为原来的2倍
int newSize = initialCapacity << 1;
Node[] newTable = new Node[newSize];
initialCapacity = newSize;
//进行扩容
reHash(newTable);
}
private void reHash(Node[] newTable) {
//生成一个链表
List> entryList = new ArrayList<>();
for (Node 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 node : entryList) {
//重新进行put操作
put(node.getKey(), node.getValue());
}
}
关于hashcode和equals方法
hashCode主要用于提升查询效率提高哈希表性能,来确定在散列结构中对象的存储地址
重写equals()必须重写hashCode()
哈希存储结构中,添加元素重复性校验的标准就是先检查hashCode值,后判断equals()
两个对象equals()相等,hashcode()必定相等
两个对象hashcode()不等,equals()必定也不等
两个对象hashcode()相等,对象不一定相等,需要通过equals()进一步判断。
如果传入HashMap一个自定义对象作为key值,必须要重写该对象的hashcode和equals方法,并且符合上面的规则。
为什么hashmap推荐使用String作为key?
因为String对象hashcode和equals方法完全符合上面的规则。
而且,String中缓存有个hash变量,它可以缓存hashCode,避免重复计算hashCode。例如,你有一个 student1对象作为map的key值,你第二次使用student1作为key值放入map中时,会再次计算student1的hashCode。
总结
实现hashmap最重要的是减少元素冲突,在JDK源码中,可以看到用到了大量的位运算,目的就是提高性能。使用hashmap的时候必须初始化容量和重写key对象的hashcode和equals方法。本次手写的miniHashMap仅实现了主要的功能,在线程安全方面和数据性能方面有明显的不足。