自定义HashMap【Java实现、2024最新】

我这里就不带大家学基础和看源码了,建议大家先去看看基础知识再来跟着我自定义一个
在这里插入图片描述

1.创建Node节点类

在hashmap中,所有的节点实际上是一个个的Node节点,下面我就直接用代码加注释的方法来解释了。

class Node<K,V>{
    //当前节点计算出来的hash值
    //通过hash值对数组的长度取模,可以获取该Node节点在数组中的位置
    int hash;   //hash值
    K k;    //键
    V v;    //值
    Node<K,V> next;//如果hash冲突,生成链表的话,则next指向下一个节点,没有下一个节点返回null

    /*构造方法*/
    public Node(int hash, K k, V v, Node<K, V> next) {
        this.hash = hash;
        this.k = k;
        this.v = v;
        this.next = next;
    }

    @Override
    public String toString() {
        return "Node{" +
                "hash=" + hash +
                ", k=" + k +
                ", v=" + v +
                ", next=" + next +
                '}';
    }
    //所有属性的set和get方法,不用多说
    public int getHash() {return hash;}
    public void setHash(int hash) {this.hash = hash;}
    public K getK() {return k;}
    public void setK(K k) {this.k = k;}
    public V getV() {return v;}
    public void setV(V v) {this.v = v;}
    public Node<K, V> getNext() {return next;}
    public void setNext(Node<K, V> next) {this.next = next;}
}
//最后这个Node类会作为我们后面定义的HashMap的内部类来使用

2.HashMap的参数设置

/*map的底层就是一个Node数组,每一个元素就是一个Node,如果这个元素有hash冲突,那么node转为链表*/
private  Node<K,V> [] table;
/*数组的初始大小*/
private final int DEFAULT_INITIAL_CAPACITY = 1<<4;  //1<<4  1左移四位   =   1000    就是16

private int size = 0;//table的实际长度(存了多少Node)
/**
 * 负载因子:减少生成链表的机会,因为一旦table存满了,则必会增加生成链表的机会
 * 所以在table还没有存满前,就要扩容
 * */
private static float DEFAULT_FACTOR = 0.75f; //实际上就是一个百分数75%


/**
 * 阈值:数组容量*负载因子
 * */
private int threshold;

/*构造方法:初始化一个Node数组和阈值*/
public MyHashMap(){
    table = new Node[DEFAULT_INITIAL_CAPACITY];
    threshold = (int) (DEFAULT_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
public int size(){
    return this.size;
}

这里有两个概念需要拿出来单独讲

2.1.位运算

​ 在我们的Java编程中,这种容器还有其他什么的框架,虽然会使得我们的编码过程变得十分快捷和方便。但是,代价就是太吃内存了,这是因为我们在封装或者使用泛型等操作时,涉及的运算实在是太多了。因此,位运算就是其中的一个解决办法之一。

位运算是一种直接操作二进制数字的运算方式,通过对数字的二进制表示进行操作来实现各种计算。位运算相比于算术运算具有许多优点,这里我们只需要掌握两点:1.速度快,2.节省空间。

2.2.负载因子和阈值

​ 在hashmap中,有一个重要的概念,那就是扩容。在不考虑红黑树的情况下,当添加节点时节点个数超过数组容量时,需要对数组扩容。

​ 而hashmap的整体设计思想就是要减少hash冲突,减少产生链表的机会。我们在不断添加节点的过程中,随着数组存储的节点实际数量越来越多,发生hash冲突的几率是越来越大的。所以,我们规定,当节点实际个数为数组容量大小的75%(负载因子)时,就对数组进行扩容。

​ 其实,简单点说就是,数组还没有存满前(达到阈值),就对其进行扩容,减少hash冲突的机会。

3.索引和hash值的计算

/**
 * 利用该函数完成对key生成hashcode的操作
 * Object中原来就有hashCode(),但是这种自带的函数生成的hash值比较相近
 * 比如 a,b,c,d,e的哈希值就是它们的ASCLL值:97,98,99,100,101
 * 解决办法:1.先调用hashCode()方法 2.hash值是32位整数,将它的高低16位进行混洗
 * @param key	
 * @return		节点hash值
 * 目的:使hash值更加离散
 */
static final int hash(Object key){
    int h = key.hashCode();
    return (key==null)?0:(h^(h>>16));//!!又是位运算
    //h ^ (h >>> 16):将原始的 h 与其右移16位的结果进行异或运算。
}

/**
 * @param n     就是数组的长度
 * @param key   索引就是key的hash值对数组长度取余
 * @return 返回在Node数组中的索引
 */
static int index(int n,Object key){
    //hash(key)%n;
    return (n-1)%hash(key);//等同于求余,此余数就是key在数组中的索引
}

4.hashmap的get方法

public V get(K k){
//        1.根据k获得数组下标
        int index = index(table.length,k);
//        2.根据下标到table表中取出Node
        Node<K,V> node = table[index];
//        3.Node==null,说明map中还没有这个key,直接返回空
        if (node ==null) return null;
//        4.Node!=null 说明数组中有这个key
//        4.1.如果Node.next==null,说明这不是链表,只有唯一的节点,返回节点V
        if (node.getNext()==null)  return node.getV();
//        4.2.如果Node.next!=null,说明是一个链表,迭代找到K相等的节点,返回节点的V
        else {
            if (node.getK()==k) return node.getV();
            Node<K,V> next = node.next;
            while(next!=null){
                if (next.getK()==k){
                    return  next.getV();
                }
                next = next.next;
            }
            return null;
        }
    }

5.hashmap的put方法

public void put(K k,V v){
        //1.想根据K计算数组索引
        int index = index(table.length,k);
        //2.取出这个索引的node
        Node<K,V> node = table[index];
        //3.如果node==null,说明没有冲突,即这个索引位置没有存东西
        if (node ==null){
            table[index] = new Node<>(hash(k),k,v,null);
            size++;

        }
        //4.如果node!=null,有冲突:
        else {
            // 4. 如果 node != null,有冲突:
            Node<K, V> current = node;
            while (current != null) {//这里相当于遍历索引()
                //4.1如果k值相同,则替换value值即可
                if (current.getK().equals(k)) {
                    current.setV(v);
                    return;
                }
                if (current.next == null) {//说明索引处只有一个节点,没有产生链表
                    // 4.2. k 与 current 中的 k 不同,表示不同 key 但是 hash 值相同,生成链表新的节点
                    current.next = new Node<>(hash(k), k, v, null);
                    size++;
                    break;
                }
                current = current.next;//遍历下一个链表节点
            }
        }
//        System.out.println("元素个数:"+size+",阈值:"+threshold+",容量:"+table.length);
        if (size>threshold){
            resize();//扩容这个数组,要将这个数组中的原来的数据移动;
        }
    }

6.扩容方法

​ 为了解决JDK1.7中的死循环问题, 在jDK1.8中新增加了红黑树,即在数组长度大于64,同时链表长度大于8的情况下,链表将转化为红黑树。我们这里就先不考虑这个问题。

private void resize(){
    Node [] tableOld = table;
    Node[] tableNew = new Node[tableOld.length<<1];//扩容,原来的容量长度左移一位,相当于乘以2
    for (int i =0;i<tableOld.length;i++){
        if (tableOld[i]!=null){
            //原来node的hash值,对新数组计算一次索引下标(因此,扩容后,节点的索引可能会与在旧数组的位置不一样)
            int newIndex = index(tableNew.length,tableOld[i].hash);
            tableNew[newIndex] = tableOld[i];
        }
    }
    table = tableNew;//更新数组
    //更新阈值
    threshold = (int) (table.length*DEFAULT_FACTOR);
}

7.最后的完整代码

package 自定义HashMap;

import java.util.HashMap;

public class MyHashMap<K,V> {

    /*map的底层就是一个Node数组,每一个元素就是一个Node,如果这个元素有hash冲突,那么node转为链表*/
    private  Node<K,V> [] table;
    /*数组的初始大小*/
    private final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  //1<<4  1左移四位   =   1000    就是16

    private int size = 0;//table的实际长度(存了多少Node)
    /**
     * 负载因子:减少生成链表的机会,因为一旦table存满了,,则必会增加生成链表的机会
     * 所以在table还没有存满前,就要扩容
     * */
    private static final float DEFAULT_FACTOR = 0.75f; //实际上就是一个百分数75%


    /**
     * 阈值:数组容量+负载因子
     * */
    private int threshold;

    /*构造方法:初始化一个Node数组和阈值*/
    public MyHashMap(){
        table = new Node[DEFAULT_INITIAL_CAPACITY];
        threshold = (int) (DEFAULT_FACTOR * table.length);
    }
    public int size(){
        return this.size;
    }

    /**
     * 根据Key取值
     * @param k
     * @return
     */
    public V get(K k){
//        1.根据k获得数组下标
        int index = index(table.length,k);
//        2.根据下标到table表中取出Node
        Node<K,V> node = table[index];
//        3.Node==null,说明map中还没有这个key,直接返回空
        if (node ==null) return null;
//        4.Node!=null 说明数组中有这个key
//        4.1.如果Node.next==null,说明这不是链表,只有唯一的节点,返回节点V
        if (node.getNext()==null)  return node.getV();
//        4.2.如果Node.next!=null,说明是一个链表,迭代找到K相等的节点,返回节点的V
        else {
            if (node.getK()==k) return node.getV();
            Node<K,V> next = node.next;
            while(next!=null){
                if (next.getK()==k){
                    return  next.getV();
                }
                next = next.next;
            }
            return null;
        }
    }

    /**
     * 存
     * @param k
     * @param v
     */
    public void put(K k,V v){
        //1.想根据K计算数组索引
        int index = index(table.length,k);
        //2.取出这个索引的node
        Node<K,V> node = table[index];
        //3.如果node==null,说明没有冲突,即这个索引位置没有存东西
        if (node ==null){
            table[index] = new Node<>(hash(k),k,v,null);
            size++;

        }
        //4.如果node!=null,有冲突:
        else {
            // 4. 如果 node != null,有冲突:
            Node<K, V> current = node;
            while (current != null) {//这里相当于遍历索引()
                //4.1如果k值相同,则替换value值即可
                if (current.getK().equals(k)) {
                    current.setV(v);
                    return;
                }
                if (current.next == null) {//说明索引处只有一个节点,没有产生链表
                    // 4.2. k 与 current 中的 k 不同,表示不同 key 但是 hash 值相同,生成链表新的节点
                    current.next = new Node<>(hash(k), k, v, null);
                    size++;
                    break;
                }
                current = current.next;//遍历下一个链表节点
            }
        }
//        System.out.println("元素个数:"+size+",阈值:"+threshold+",容量:"+table.length);
        if (size>threshold){
            resize();//扩容这个数组,要将这个数组中的原来的数据移动;
        }
    }

    /**
     * 数组扩容函数
     * */
    private void resize(){
        //扩容,原来的容量长度左移一位,相当于乘以2
        Node [] tableOld = table;
        Node[] tableNew = new Node[tableOld.length<<1];
        for (int i =0;i<tableOld.length;i++){
            if (tableOld[i]!=null){
                //原来node的hash值,对新数组计算一次索引下标(因此,扩容后,节点的索引可能会与在久数组的位置不一样)
                int newIndex = index(tableNew.length,tableOld[i].k);
                tableNew[newIndex] = tableOld[i];
            }
        }
        table = tableNew;//更新数组
        //更新阈值
        threshold = (int) (table.length*DEFAULT_FACTOR);
    }
    /**
     * 利用该函数完成对key生成hashcode的操作
     * Object中原来就有hashCode(),但是这种自带的函数生成的hash值比较相近
     * 比如 a,b,c,d,e的哈希值就是它们的ASCLL值:97,98,99,100,101
     * 解决办法:1.先调用hashCode()方法 2.hash值是32位整数,将它的高低16位进行混洗
     * @param key
     * @return
     * 目的:使hash值更加离散
     */
    static final int hash(Object key){
        int h = key.hashCode();
        return (key==null)?0:(h^(h>>16));
    }

    /**
     * @param n     就是数组的长度
     * @param key   索引就是key的hash值对数组长度取余
     * @return 返回在Node数组中的索引
     */
    static int index(int n,Object key){
        //hash(key)%n;
        return (n-1)%hash(key);//等同于求余,此余数就是key在数组中的索引
    }

    /**
     * 每一个node就是数组里的一个节点
     * @param <K>
     * @param <V>
     */
    class Node<K,V>{
        //当前节点计算出来的hash值
        //通过hash值模数组的长度,可以获取该Node节点在数组中的位置
        int hash;   //hash值
        K k;    //键
        V v;    //值
        Node<K,V> next;//如果hash冲突,生辰链表的话,则next指向下一个节点,没有下一个节点返回null

        /*构造方法*/
        public Node(int hash, K k, V v, Node<K, V> next) {
            this.hash = hash;
            this.k = k;
            this.v = v;
            this.next = next;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "hash=" + hash +
                    ", k=" + k +
                    ", v=" + v +
                    ", next=" + next +
                    '}';
        }
//所有属性的set和get方法,不用多说
    public int getHash() {return hash;}
    public void setHash(int hash) {this.hash = hash;}
    public K getK() {return k;}
    public void setK(K k) {this.k = k;}
    public V getV() {return v;}
    public void setV(V v) {this.v = v;}
    public Node<K, V> getNext() {return next;}
    public void setNext(Node<K, V> next) {this.next = next;}
    }
}

8.测试

public static void main(String[] args) {

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

    hashMap.put(1,"张1");
    hashMap.put(2,"李2");
    hashMap.put(3,"王3");
    hashMap.put(1,"赵1");
    hashMap.put(1,"陈1");

    System.out.println(hashMap.size());

    System.out.println(hashMap.get(1));
    System.out.println(hashMap.get(2));
    System.out.println(hashMap.get(3));


}
测试结果:
3123

9.小结

​ 这里,我只是对基础的set和get方法进行了自定义,还有只使用了链表进行扩容,没有运用红黑树的知识。实际上的hashmap还有更多的方法,这就需要我们细读源码了,在后续我有时间的话会更新的。当然,官方源码肯定比我写的详细且规范。如有错误和其他意见,还请在评论区留言指正哦。

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值