仿写HashMap,思路清晰

本文通过阅读HashMap源码,详细介绍了HashMap的工作原理,包括容量、负载因子、数组和链表的使用。作者尝试仿写HashMap,实现了put、get和resize方法,探讨了Hash值计算、数组扩容及冲突解决策略。同时,文章还讨论了Java中泛型数组的实例化问题以及为何使用&(capacity-1)来计算索引。
摘要由CSDN通过智能技术生成

最近在看HashMap的源码,了解了HashMap的原理和方法实现的过程,觉得如果自己仿写可以简单仿写一个HashMap可能就会更加清楚了,说干就干。

说明一下,我主要仿写了put、get和扩容resize方法,其他方法其实大致原理差不多。

源码已上传到GitHub上:仿写HashMap

准备工作

首先,HashMap的底层是一个数组,数组中的元素Node有四个属性,Key,Value,Key的Hash值和为了解决产生Hash冲突而设置的Next结点,如下:

class Node<K,V>{
    K key;
    V value;
    int hash;
    Node<K,V> next;

    public Node(K key, V value, int hash) {
        this.key = key;
        this.value = value;
        this.hash = hash;
    }
}

在HashMap中,大体上有HashMap容量capacity,负载因子loadFactor,元素数量size(还没put元素,肯定是0)和Node泛型数组这四个属性。

public class MyHashMap<K,V> {
    private int size = 0;
    private int capacity;
    private float loadFactor;
    private Node<K,V>[] table;
}

对于构造函数,HashMap默认容量是16,默认负载因子是0.75,还要创建一个大小为16的Node泛型数组,这里我一开始是这样写的

public class MyHashMap<K,V> {
    private int size = 0;
    private int capacity;
    private float loadFactor;
    private Node<K,V>[] table;
    
    public MyHashMap() {
        this.capacity = 16;
        this.loadFactor = 0.75f;
        table = new Node<K,V>[capacity];
    }  
}

发现new Node<K,V>[capacity];报错了,在网上搜索才知道Java是不支持实例化泛型数组的,因为Java认为数组是不可变的,而泛型数组中的元素类型是会变化的,后来查了一些资料,发现了一个解决方法,这样利用反射就可以实例化一个固定大小的泛型数组了,如下:

public class MyHashMap<K,V> {
    private int size = 0;
    private int capacity;
    private float loadFactor;
    private Node<K,V>[] table;
    
    public MyHashMap() {
        this.capacity = 16;
        this.loadFactor = 0.75f;
        table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
    }  
}

HashMap源码中还给出了一个可以自定义容量和负载因子的构造函数,我们也模仿着写一个,注意参数的合法性(其实HashMap源码中还限制了容量的最大值,为1<<30,也可以限制一下)

public class MyHashMap<K,V> {
    private int size = 0;
    private int capacity;
    private float loadFactor;
    private Node<K,V>[] table;
    
    public MyHashMap() {
        this.capacity = 16;
        this.loadFactor = 0.75f;
        table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
    }  
    
    //可以自定义容量和负载因子的构造器
    public MyHashMap(int initialCapacity,float loadFactor) {
        //自定义容量不能小于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("不合法容量: " +
                    capacity);
        //自定义负载因子不能小于等于0并且不能是NaN值
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("不合法负载因子: " +
                    loadFactor);
        //自定义容量向上取2的n次方
        this.capacity = tableSizeFor(initialCapacity);
        this.loadFactor = loadFactor;
        table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
    }
}

在HashMap中,容量必须为2的n次方,这个和生成Hash值有关,后面我会讲一下,而如果自定义的容量不为2的n次方,就会向上取一个最小的2的n次方的数,比如自定义容量为9,HahsMap就会自动将容量变为16,所以我这里调用了一个tableSizeFor方法来实现,参考了HashMap源码中的tableSizeFor方法,如下:

    //向上取2的n次方
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : n + 1;
    }

在写put和get方法的之前,我们还要写一个Hash方法,目的是为了计算Key的Hash值以得到它在数组中的索引位置,后面会讲一下原理,如下:

    //计算Hash值
    int Hash(K key) {
        return Objects.hashCode(key) & (capacity - 1);
    }

这里和HashMap源码中的Hash方法一样,也用HashCode值与capacity-1,那么为什么要这样做呢,& capacity不是也可以得到索引的位置吗?
这里就涉及到了一道面试题,就是为什么要&(capacity - 1),原因就在于:HashMap规定数组的容量必需是2的n次方,不是的话向上取最小的2的n次方,当容量为2的n次方的时候,length-1会把二进制中1以后的0全部变为1,当hashcode值和length-1做与时,生成的hash值都是按照hashcode值生成的,减少了hash冲突。
如果length不为2的n次方的话,length-1就会在某些位上出现0,做与后就是0,浪费了空间,生成的hash值很可能出现重复,发生hash冲突的几率提高
如果直接&capacity,由于capacity是2的n次方,后果可想而知
所以HashMap既限制了capacity是2的n次方,又使用&(capacity - 1),是减少Hash冲突最有效的方法。

至此,准备工作就做完了,可以开始写我们的put、get和resize方法了,先把之前写的整理一下:

public class MyHashMap<K,V> {
    private int size = 0;
    private int capacity;
    private float loadFactor;
    private Node<K,V>[] table;

    public MyHashMap() {
        this.capacity = 16;
        this.loadFactor = 0.75f;
        table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
    }

    public MyHashMap(int initialCapacity,float loadFactor) {
        //自定义容量不能小于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("不合法容量: " +
                    capacity);
        //自定义负载因子不能小于等于0并且不能是NaN值
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("不合法负载因子: " +
                    loadFactor);
        //自定义容量向上取2的n次方
        this.capacity = tableSizeFor(initialCapacity);
        this.loadFactor = loadFactor;
        table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
    }

    //计算Hash值
    int Hash(K key) {
        return Objects.hashCode(key) & (capacity - 1);
    }

    //向上取2的n次方
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : n + 1;
    }
	
	//put、get、resize
	
}
class Node<K,V>{
    K key;
    V value;
    int hash;
    Node<K,V> next;

    public Node(K key, V value, int hash) {
        this.key = key;
        this.value = value;
        this.hash = hash;
    }
}

put方法

先写put方法吧,我这里就不考虑jdk1.8新加的红黑树了,使用的是数组加链表,思考一下HashMap的put方法的大致流程:

  • 调用Hash方法计算得到key的Hash值,也就是索引位置
  • 判断索引位置处是否有元素,如果没有元素,直接加进去,如果有元素,就要遍历链表了
  • 遍历链表,判断每个结点的key是不是等于要加进去结点的key,如果有等于的,直接此结点覆盖value,如果没有等于的,利用头插法插到链表的第一个结点之前(这里也可以使用尾插法,注意要让最后一个结点的next指向新结点)
  • 判断一下是否发生覆盖,如果没有发生覆盖,size++
  • 判断size是否超过负载因子*容量,超过则扩容,调用resize

以上是大致思路,头插法和尾插法都有写,用一个就行,下面是具体代码实现:

    void put(K key,V value){
        int hash = Hash(key);
        //判断是否发生覆盖的标识
        boolean flag = true;
        Node<K,V> node = new Node<>(key,value,hash);
        //如果索引位置没有元素,直接加
        if(this.table[hash] == null){
            this.table[hash] = node;
        }else{
            Node<K,V> head = this.table[hash];
            //发生hash冲突,搜索链表
            while (head != null){
                //如果Key相等,覆盖Value
                if(key == head.key){
                    head.value = value;
                    flag = false;
                    break;
                }
                
//                //如果hash冲突没有发生覆盖,使用尾插法,让最后一个结点的next指向新结点
//                if(head.next == null){
//                    head.next = node;
//                    break;
//                }

                head = head.next;
            }
            
            //如果hash冲突没有发生覆盖,使用头插法,插到第一个结点之前
            if(flag){
                head = this.table[hash];
                this.table[hash] = node;
                node.next = head;
            }
            
        }
        //如果没有发生覆盖,元素数量size++
        if(flag) this.size++;
        //如果元素数量超过负载因子*容量,扩容
        if(size > capacity * loadFactor) resize();
    }

get方法

接下来写get方法,也是先思考一下大致流程:

  • 调用Hash方法计算得到key的Hash值,也就是索引位置
  • 如果索引位置没有元素,直接返回null
  • 如果索引位置有元素,遍历索引位置处的所有结点,找到key一样的就取得value,找不到返回null
    V get(K key){
        V value = null;
        int hash = Hash(key);
        //如果索引位置没有元素
        if(this.table[hash] == null){
            return value;
        }else{
            Node<K,V> head = this.table[hash];
            //遍历所有结点
            while (head != null){
                if(head.key == key){
                    value = head.value;
                    break;
                }
                head = head.next;
            }
            return value;
        }
    }

resize方法

最后是resize方法,这个方法是用来扩容的,HashMap扩容的时机为元素数量size大于负载因子loadFactor*容量capacity时,在jdk1.8中,如果链表元素超过8并且元素数量小于64时也会发生扩容,后者先不实现,先实现前者,也是思考一下大致流程:

  • 拷贝一下老数组,实例化一个是原数组容量2倍的新数组作为HashMap的数组,并把capacity乘以2。
  • 老数组中有所有元素,遍历老数组,如果某个索引处不为null,就遍历该索引位置的所有结点,每个结点都调用put方法加到新数组中,直到遍历完老数组为止。
    void resize(){
        //老的table
        Node<K,V>[] oldTable = this.table;
        //新的table,容量2倍
        this.table = (Node<K, V>[]) Array.newInstance(Node.class,2 * capacity);
        this.capacity *= 2;
        //遍历老table,把键值对都put到新table中
        for (int i = 0;i < oldTable.length; i++){
            if(oldTable[i] != null){
                Node<K,V> head = oldTable[i];
                while (head != null) {
                    put(head.key,head.value);
                    head = head.next;
                }
            }
        }
    }

至此,我自己仿写的HashMap在基础功能上就做完了,完整代码如下:

public class MyHashMap<K,V> {
    private int size = 0;
    private int capacity;
    private float loadFactor;
    private Node<K,V>[] table;

    public MyHashMap() {
        this.capacity = 16;
        this.loadFactor = 0.75f;
        table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
    }

    public MyHashMap(int initialCapacity,float loadFactor) {
        //自定义容量不能小于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("不合法容量: " +
                    capacity);
        //自定义负载因子不能小于等于0并且不能是NaN值
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("不合法负载因子: " +
                    loadFactor);
        //自定义容量向上取2的n次方
        this.capacity = tableSizeFor(initialCapacity);
        this.loadFactor = loadFactor;
        table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
    }

    //计算Hash值
    int Hash(K key) {
        return Objects.hashCode(key) & (capacity - 1);
    }


    void put(K key,V value){
        int hash = Hash(key);
        //判断是否发生覆盖
        boolean flag = true;
        Node<K,V> node = new Node<>(key,value,hash);
        if(this.table[hash] == null){
            this.table[hash] = node;
        }else{
            Node<K,V> head = this.table[hash];
            //发生hash冲突,搜索链表
            while (head != null){
                //如果Key相等,覆盖Value
                if(key == head.key){
                    head.value = value;
                    flag = false;
                    break;
                }
                //如果hash冲突没有发生覆盖,使用尾插法,让最后一个结点的next指向新结点
                if(head.next == null){
                    head.next = node;
                    break;
                }
                head = head.next;
            }
//            //如果hash冲突没有发生覆盖,使用头插法,插到第一个结点之前
//            if(flag){
//                head = this.table[hash];
//                this.table[hash] = node;
//                node.next = head;
//            }
        }
        if(flag) this.size++;
        //如果元素数量超过负载因子*容量,扩容
        if(size > capacity * loadFactor) resize();
    }

    V get(K key){
        V value = null;
        int hash = Hash(key);
        if(this.table[hash] == null){
            return value;
        }else{
            Node<K,V> head = this.table[hash];
            while (head != null){
                if(head.key == key){
                    value = head.value;
                    break;
                }
                head = head.next;
            }
            return value;
        }
    }

    void resize(){
        //老的table
        Node<K,V>[] oldTable = this.table;
        //新的table,容量2倍
        this.table = (Node<K, V>[]) Array.newInstance(Node.class,2 * capacity);
        this.capacity *= 2;
        //遍历老table,把键值对都put到新table中
        for (int i = 0;i < oldTable.length; i++){
            if(oldTable[i] != null){
                Node<K,V> head = oldTable[i];
                while (head != null) {
                    put(head.key,head.value);
                    head = head.next;
                }
            }
        }
    }

    //向上取2的n次方
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : n + 1;
    }

}
class Node<K,V>{
    K key;
    V value;
    int hash;
    Node<K,V> next;

    public Node(K key, V value, int hash) {
        this.key = key;
        this.value = value;
        this.hash = hash;
    }
}

自己也只是按照流程仿写了一下,许多HashMap中的方法都没有写,目的只是为了加深自己对HashMap的认识,其中可能也有不足的地方需要指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值