手写一个HashMap

编程

大家都知道JDK7前HashMap底层是数组+链表,JDK8以后当链表长度>8的时候会自动变为红黑树。本文基于数+链表实现一个简化版的HashMap,有助于理解HashMap的底层原理。在此之前,结合下图,先了解一下相关的基础知识

 

Put的过程

  1. 对key的hashCode()做hash运算,计算元素存放在数组的位置;

  2. 如果该位置没有元素,则放在该位置; 如果已存在元素,即发生Hash碰撞,以链表的形式存在于该元素后,当然若key已经存在就替换旧值(保证key的唯一性);如果碰撞导致链表过长(>=8),就把链表转换成红黑树(JDK1.8中的改动);

  3. 如果元素的个数太多(条件后面再说),需要扩容

 

get的过程

对key的hashCode()做hash运算,计算位置; 如果在数组里的第一个节点里直接命中,则直接返回; 如果有冲突,则遍历链表去寻找;

 

何时扩容?

hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容。loadFactor默认0.75,hashmap默认长度16,也就是说第一次扩容发生在16*0.75=12的时候,将扩容一倍,也就是说32

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。 当元素小于8个当时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于8个的时候,此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

 

当链表转为红黑树后,什么时候退化为链表?

为6的时候退转为链表。中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

 

当两个对象的hashcode相同会发生什么?

因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。

 

 

 

 

直接上代码

/**

* @author HeyS1

* @date 2020/6/4

* @description

*/

public class MyHashMap<K, V> {

@Data

static class Entry<K, V> {

private Entry<K, V> next;

private K key;

private V value;

Entry(Entry<K, V> next, K key, V value) {

this.next = next;

this.key = key;

this.value = value;

}

}

//数组默认长度

private static final int DEFAULT_INITIAL_CAPACITY = 16;

//默认阈值比例

private static final float DEFAULT_LOAD_FACTOR = 0.75f;

//数组长度

private int arraySize;

//阈值比例

private float loadFactor;

//一共存储了多少个entry

private int entryUseSize;

//存储数组

private Entry<K, V>[] table;

public MyHashMap() {

this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);

}

public MyHashMap(int arraySize, float loadFactor) {

this.arraySize = arraySize;

this.loadFactor = loadFactor;

table = new Entry[this.arraySize];

}

public void put(K k, V v) {

//获取entry在数组中存放的位置

int index = getIndex(k);

Entry<K, V> t = table[index];

if (t == null) {

//如果该位置不存在元素,则放入即可

table[index] = createEntry(k, v);

}

//若已存在元素

else {

if (keyEquals(t, k)) {

//若该位置的元素key与放入元素的key是一致的,则替换value

t.value = v;

} else {

//遍历链表

while (t != null) {

if (t.next == null) {

t.next = createEntry(k, v);

break;

}

if (keyEquals(t.next, k)) {

t.next.value = v;

break;

}

t = t.next;

}

}

}

//若entry数量 >= 数组长度 * 阈值,则进行扩容

if (entryUseSize >= arraySize * loadFactor) {

resize(2 * entryUseSize);

}

}

public V get(K k) {

int index = getIndex(k);

Entry<K, V> t = table[index];

if (keyEquals(t, k)) {

return t.value;

}

//遍历链表寻找元素

while (t.next != null) {

if (keyEquals(t.next, k)) {

return t.next.value;

}

t = t.next;

}

return null;

}

/**

* 扩容 (即将所有元素重新hash放入一个更大的数组)

*

* @param i

*/

private void resize(int i) {

arraySize = i;//重置数组长度为i

entryUseSize = 0;//重置记录元素长度为0

//将所有元素都放入List

List<Entry<K, V>> list = new ArrayList<>();

for (int j = 0; j < table.length; j++) {

if (table[j] == null) {

continue;

}

list.add(table[j]);

while (table[j].next != null) {

list.add(table[j].next);

table[j] = table[j].next;

}

}

//新建一个容量更大的数组并且将所有元素put进去

table = new Entry[i];

for (Entry<K, V> kvEntry : list) {

put(kvEntry.getKey(), kvEntry.getValue());

}

}

private Entry<K, V> createEntry(K k, V v) {

entryUseSize++;//记录元素数量,每次创建一个Entry数量都要自增1,用于计算是否需要扩容

return new Entry<>(null, k, v);

}

/**

* 判断Key是否一致

*

* @param entry 某元素

* @param key key

* @return

*/

private boolean keyEquals(Entry entry, K key) {

return entry.getKey() == key || (entry.getKey() != null && entry.getKey().equals(key));

}

/**

* 根据hash获取元素在数组的位置

*

* @param k

* @return

*/

private int getIndex(K k) {

return hash(k) & (arraySize - 1);

}

/**

* 获取Key 的hash

*

* @param key

* @return

*/

private int hash(K key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

public void getInfo() {

System.out.println("当前数组长度:" + arraySize);

System.out.println("当前元素个数:" + entryUseSize);

}

}

测试:

public class Test {

public static void main(String[] args) {

MyHashMap<Integer, String> myMap = new MyHashMap<>();

for (int i = 0; i < 500; i++) {

myMap.put(i, i + "");

System.out.println(myMap.get(i));

}

//重复添加

for (int i = 0; i < 500; i++) {

myMap.put(i, i + "-覆盖");

System.out.println(myMap.get(i));

}

myMap.getInfo();

}

}

 

以上是 手写一个HashMap 的全部内容, 来源链接: utcz.com/z/517735.html

回到顶部