省略红黑树版本的简易HashMap
手写HashMap的一些基本参数
HashMap的初始容量是16,这个是长久积累得出的经验之谈,并且和他本身扩容是需要2的幂次有关的。
- 初始容量
- 最大容量
- 默认负载因子
- 负载因子
- size大小
- 存放元素的数组
为什么HashMap扩容是2的幂次呢?
hash算法简单实现的话就是key与数组长度length求余得出当前元素存放的位置。but求余操作性能十分低下。那么源码是怎么做的呢,是按位与进行位运算(table.length - 1) & hash,我们知道位运算的速度是远高于求余运算。那么这样按位与后得到的结果就是元素应当在数组中的位置。2的幂次只有一位是1,其余位是0,减一则高位为0,其余为1。
最大容量为什么是2的30次方不是2的31次方?
由于int类型的长度为4个字节共32个二进制位,按理说可以向左移动31位即2的31次方。但是事实上由于二进制数字中最高的一位也就是最左边的一位是符号位,用来表示正负之分(0为正,1为负),所以只能向左移动30位,而不能移动到处在最高位的符号位。
//初始容量大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量大小
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
final float loadFactor;
int initialCapacity;
transient int entrySize;
transient Entry<K,V>[] table = null;
Map接口
我们需要一个Map接口来定义一些行为 提供基本的get、put和remove
interface Map_<K, V>{
int size();
V get(Object key);
V put(K key, V value);
V remove(Object key);
interface Entry<K, V>{
K getKey();
V getValue();
}
}
初始化
在我们创建map的时候需要执行构造方法,将我们设置的容量或者负载因子等信息设置进去,所以需要多个构造方法来支持这些操作。
public HashMap_() {
this(DEFAULT_INITIAL_CAPACITY,DEFAULT_INITIAL_CAPACITY);
}
public HashMap_(int initialCapacity){
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap_(int initialCapacity, float loadFactor) {
// 容量小于0的话,直接抛出异常
if (initialCapacity < 0){
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
}
// 另外容量也不能超过最大限制
if (initialCapacity > MAXIMUM_CAPACITY){
this.initialCapacity = MAXIMUM_CAPACITY;
}
// 负载因子的校验
if (loadFactor <= 0 || Float.isNaN(loadFactor)){
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
}
// 其他属性的初始化
this.loadFactor = loadFactor;
this.initialCapacity = initialCapacity;
table = new Entry[initialCapacity];
}
这里提供了三个基本的构造方法,支持无参构造、容量设置以及容量和负载因子的设置。 另外,Entry数组的定义我们也可以仿照源码整个静态内部类出来。
Entry数组
static class Entry<K,V> implements Map_.Entry<K,V>{
final K key;
V value;
Entry<K,V> next;
public Entry(K key, V value, Entry<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;
}
}
哈希函数
没啥好说的,这个照搬了XD
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
容量
@Override
public int size() {
return entrySize;
}
获取元素
我们获取元素呢,需要根据哈希计算出具体下标位置。如果获取不到,则寻求在链表中寻找。 这里不是取模运算,而是操作二进制位进行与运算,我们都知道移位操作比取模的性能要高不知多少倍。 2的幂次在二进制中是开头为1后面都是0,减1后就变成全为1。这样去进行与运算就能得出我们具体index下标位置。
@Override
public V get(Object key) {
// 通过之前的hash函数计算出数组中的位置
int index = hash(key) & (initialCapacity - 1);
Entry<K,V> entry = table[index];
while (entry != null){
if (entry.getKey() == key || entry.getKey().equals(key)){
return entry.value;
}
entry = entry.next;
}
return null;
}
扩容
在put元素的时候呢,我们是需要进行容量校验的,不能无脑put,所以put前置条件扩容方法这里给出。 扩容我们需要new一个新的容量更大的数组,entrySize为0(新的空数组),然后进行重新hash,将数组里的元素重新定位。
private void resize(int newSize){
Entry[] newtable = new Entry[newSize];
entrySize = 0;
initialCapacity = newSize;
rehash(newtable);
}
private void rehash(Entry<K,V>[] newtable){
// 先将旧的entry放到集合中暂存
List<Entry<K,V>> entryList = new ArrayList<>();
for (Entry<K,V> entry : table){
while (entry != null){
entryList.add(entry);
entry = entry.next;
}
}
// 用新的空的newtable覆盖旧的table
if (newtable.length > 0){
table = newtable;
}
// 重新hash -> 重新put entry到hashmap
for (Entry<K,V> entry : entryList){
put(entry.getKey(), entry.getValue());
}
}
放置元素
我们在放置元素的时候需要考虑到
- 返回旧值
- 是否需要扩容 2倍扩
- 计算下标
- 是否需要存放在链表 另外放到链表中是覆盖原值呢还是追加呢,都是需要考虑的点 这里插入链表操作选用了1.7版本的简单的头插法(毕竟简易版本嘛)
@Override
public V put(K key, V value) {
// 返回旧值
V oldValue = null;
// 是否需要扩容
if(entrySize > this.loadFactor * initialCapacity){
resize(2 * initialCapacity);
}
// 计算出数组中的位置,依旧按照与操作
int index = hash(key) & (initialCapacity - 1);
// 为空说明当前位置没有元素,直接插入即可
if (table[index] == null){
table[index] = new Entry<>(key,value,null);
} else {
// hash相同,插入到链表中
// 需要遍历单链表找到那个值
Entry<K,V> head = table[index];
Entry<K,V> curr = head;
while (curr != null){
// key相同
if (curr.getKey() == key || curr.getKey().equals(key)){
oldValue = current.getValue();
// 替换新值
curr.value = value;
// 这里一定要return,避免执行下面代码
return oldValue;
}
// key不同,hash相同
curr = curr.next;
}
// 插入到头部
table[index] = new Entry<>(key, value, head);
}
// 增加size
++entrySize;
return oldValue;
}
移除元素
移除元素依然返回旧值,所以提前定义出来oldValue,依旧通过hash定位具体index下标。记得容量要减1,不然size()方法返回结果就错了,size方法基于entrySize字段的。
@Override
public V remove(Object key) {
V oldValue = null;
// 计算出数组中的位置
int index = hash(key) & (initialCapacity - 1);
Entry<K,V> prev = table[index];
Entry<K,V> curr = prev;
while (curr != null){
Entry<K,V> next = curr.next;
if (curr.getKey() == key || curr.getKey().equals(key)){
oldValue = curr.getValue();
// 记得容量要减1
--entrySize;
// 如果移除头节点
if (prev == curr){
// 舍弃当前头节点直接将下一个节点引用赋给table[index]
table[index] = next;
} else {
// 前一个节点指向当前节点的下一个节点即达到了删除
prev.next = next;
}
return oldValue;
}
prev = curr;
curr = next;
}
return oldValue;
}
测试
最后我们做一下测试。
Map_<String,String> map = new HashMap_();
map.put("a","aa");
map.put("b","bb");
String put = map.put("a", "aaa");
System.out.println(put);
System.out.println(map.size());
System.out.println(map.get("a"));
System.out.println(map.get("b"));
String a = map.remove("a");
System.out.println(a);
System.out.println(map.size());
System.out.println(map.get("a"));
输出:
aa
2
aaa
bb
aaa
1
null