03Java的HashMap底层原理(下)之手写低配版HashMap

从HashMap中取数据过程get(key)

我们需要通过key对象获得“键值对”对象,进而返回value对象。

  1. 获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数组的位置。
  2. 在链表上挨个比较key对象。 调用equals()方法,将key对象和链表上所有节点的key对象进行比较,这里比较的是key对象的hashcode,直到碰到返回true的节点对象为止。
  3. 返回equals()为true的节点对象的value对象。

注意:Java中规定,两个内容相同(equals()为true)的对象必须具有相等的hashcode。因为如果equals()为true而两个对象的hashcode不同;那在整个存储过程中就发生了悖论。

下面介绍一下如何实现向HashMap中添加数据:根据HashMap的底层原理可知,每次存储一个数据节点时,首先计算这个数据节点的hash值(注意是hash值)然后存到对应索引的数组中,当下次要存入一个相同hash值的数据节点时,就追加到上一个节点的后面形成单链表结构,如果后来存入的节点的hash值与之前存入的数据的hash值没有相同的,那就直接存入数组对应索引即可。

下面是我写的实现向HashMap中put数据的程序:

package myhashmap;

/**
 * 自定义实现HashMap
 *
 * @author 发达的范
 * @date 2020/11/13 21:41
 */
public class TestHashMap03 {
    
    
    Node0[] table;//位桶数组
    int size;

    public TestHashMap03() {
    
    
        table = new Node0[16];
    }

    public void put(Object key, Object value) {
    
    
        Node0 newNode = new Node0();
        newNode.hash = myHash(key.hashCode(), table.length);//hashCode是系统生成的,hash值是可以自己计算的
        newNode.key = key;
        newNode.value = value;
        newNode.next = null;
        //需要先进行判断
        Node0 temp = table[newNode.hash];
        if (temp == null) {
    
    //该索引初次放入数据
            table[newNode.hash] = newNode;
        } else {
    
    
            while (temp.next != null) {
    
    
                if (temp.key == newNode.key) {
    
    
                    temp.value = newNode.value;
                }
                temp = temp.next;
            }
            temp.next = newNode;
        }
    }

    public int myHash(int v, int length) {
    
    //计算hash值
        return v & (length - 1);
    }

    public static void main(String[] args) {
    
    
        TestHashMap03 testHashMap03 = new TestHashMap03();
        testHashMap03.put(01, "发达的范");
        testHashMap03.put(17, "你好");
        testHashMap03.put(01, "fadadefan");
        testHashMap03.put(02, "奥特曼");
        testHashMap03.put(15, "小怪兽");
        testHashMap03.put(06, "123打");
    }
}
package myhashmap;

/**
 * 服务于自定义的HashMap的节点类
 *
 * @author 发达的范
 * @date 2020/11/13 21:42
 */
public class Node0 {
    int hash;
    Object key;
    Object value;
    Node0 next;

    public Node0() {
    }
}

直接在testHashMap03.put(15, “小怪兽”);处设置断点,从debug视图中看这几个数据是否正常存储到HashMap中:
在这里插入图片描述

可以看到,断点之前的数据节点已经正确存入到HashMap中,但是当我把这句testHashMap03.put(17, “你好”); 注释掉,再次进行debug时,发现了问题:向HashMap中添加键相同的节点,后一节点没有覆盖前一节点!这严重违反了Java的规定啊!首先肯定问题是出在put方法上,经过仔细分析之后发现问题所在。

下面看图片:
在这里插入图片描述

在添加第一个节点时,满足if(temp==null)条件语句,把第一个节点放入到指定索引位置的数组中,当添加第二个节点时,不满足if条件,直接进入else的代码块,注意此时的索引位置newNode.hash只有一个节点数据,也就是说对象temp!=null,但是temp.next==null,也就是说这个while循环的条件任何时候都不会满足,不会执行里面的判别节点的key值是否重复的程序,而是直接执行temp.next==newNode,把新的节点追加到了上一节点上形成单链表。如果不注释testHashMap03.put(17, “你好”);这句,程序会先把键key为17(hash值为1)的节点先追加到上一节点,然后执行testHashMap03.put(01, “fadadefan”);的时候,while循环的条件满足,判断有重复的键key就把之前的覆盖掉,内存图如下:

在这里插入图片描述

查找到问题所在,下面重新完成put方法:

public void put(Object key, Object value) {
    
    
    Node0 newNode = new Node0();
    newNode.hash = myHash(key.hashCode(), table.length);//hashCode是系统生成的,hash值是可以自己计算的
    newNode.key = key;
    newNode.value = value;
    newNode.next = null;
    //需要先进行判断
    Node0 temp = table[newNode.hash];
    Node0 tempLast = null;
    boolean isRepeat=false;//这个变量是标记有没有重复键key
    if (temp == null) {
    
    
        table[newNode.hash] = newNode;
    } else {
    
    
        while (temp != null) {
    
    
            if (temp.key.equals(newNode.key)) {
    
    
                System.out.println("key重复了");
                temp.value = newNode.value;
                isRepeat = true;
                break;
            } else {
    
    
                tempLast = temp;
                temp = temp.next;
            }
        }
        if (!isRepeat) {
    
    //新节点的key与HashMap中的key没有重复的
            tempLast.next = newNode;
        }
    }
}

至此,这个问题已经解决,读者可自行验证。

下面是完善版本,添加get方法,重写toString方法,添加泛型:

package myhashmap;

/**
 * 自定义实现HashMap,添加get方法,重写toString方法,添加泛型
 *
 * @author 发达的范
 * @date 2020/11/14 16:34
 */
public class TestHashMap04<K,V> {
    
    
    Node0[] table;//位桶数组
    int size;

    public TestHashMap04() {
    
    
        table = new Node0[16];
    }

    public void put(K key, V value) {
    
    
        Node0 newNode = new Node0();
        newNode.hash = myHash(key.hashCode(), table.length);//hashCode是系统生成的,hash值是可以自己计算的
        newNode.key = key;
        newNode.value = value;
        newNode.next = null;
        //需要先进行判断
        Node0 temp = table[newNode.hash];
        Node0 tempLast = null;
        boolean isRepeat = false;//这个变量是标记有没有重复键key
        if (temp == null) {
    
    
            table[newNode.hash] = newNode;
        } else {
    
    
            while (temp != null) {
    
    
                if (temp.key.equals(newNode.key)) {
    
    
//                    System.out.println("key重复了");
                    temp.value = newNode.value;
                    isRepeat = true;
                    size--;
                    break;
                } else {
    
    
                    tempLast = temp;
                    temp = temp.next;
                }
            }
            if (!isRepeat) {
    
    //新节点的key与HashMap中的key没有重复的
                tempLast.next = newNode;
            }
        }
        size++;
    }

    public void isRight(int key) {
    
    
        if (key < 0 || key > table.length) {
    
    
            throw new RuntimeException("数组索引越界!");
        }
        if (table[myHash(key, table.length)] == null) {
    
    
            throw new RuntimeException("HashMap中不存在该键值对:" + key);
        }
    }

    public Object get(K key) {
    
    //打印特定键值对的内容
        int hash = myHash(key.hashCode(), table.length);
        isRight(hash);
        Object value = null;
        Node0 temp = table[hash];
        while (temp != null) {
    
    
            if (temp.key == key) {
    
    
                value = temp.value;
                break;
            }
            temp = temp.next;
        }
        return value;
    }

    @Override
    public String toString() {
    
    
        StringBuilder stringBuilder = new StringBuilder("{");
        for (int i = 0; i < table.length; i++) {
    
    
            Node0 temp = table[i];
            while (temp != null) {
    
    
                stringBuilder.append(temp.key + ":" + temp.value + "  ");
                temp = temp.next;
            }
        }
        stringBuilder.setCharAt(stringBuilder.length() - 1, '}');
        return stringBuilder.toString();
    }

    public int myHash(int v, int length) {
    
    
        return v & (length - 1);
    }

    public static void main(String[] args) {
    
    
        TestHashMap04<Integer,Object> testHashMap04 = new TestHashMap04();
        testHashMap04.put(01, "发达的范");
        testHashMap04.put(17, "你好");
        testHashMap04.put(01, "孙悟空");//添加一个键重复的节点
        testHashMap04.put(33, "唐三藏");//添加想听hash值的节点
        testHashMap04.put(02, "奥特曼");
        testHashMap04.put(15, "小怪兽");
        testHashMap04.put(06, "123打");//乱序添加
        System.out.println(testHashMap04.get(17));
        System.out.println(testHashMap04);
    }
}

运行结果:在这里插入图片描述

可以看到,键key相同后一节点覆盖前一节点,可正确输出。

另外,可以添加一些其他方法,比如移除特定键key的节点,扩容方法等等,实现起来比较麻烦,留作后期再实现。


下面记录一些关于泛型的内容

至此,我已经见过几种泛型的使用方法,但是一不小心就容易弄混淆,搞不清楚泛型到底怎么用,现在总结几种我已经见过的泛型的用法。

  1. 在链表节点的类处添加泛型,指定存入数据类型,对Node进行实例化的时候就只能添加 E 类型的数据了。

在这里插入图片描述

  1. 把Node类当做一个创建节点的工具,在实现类处添加泛型,此时实现类的方法就只能添加 E 类型的数据了。

在这里插入图片描述

  1. 前两种结合依然可以正确使用泛型。

猜你喜欢

转载自blog.csdn.net/fada_is_in_the_way/article/details/109726027