从HashMap中取数据过程get(key)
我们需要通过key对象获得“键值对”对象,进而返回value对象。
- 获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数组的位置。
- 在链表上挨个比较key对象。 调用equals()方法,将key对象和链表上所有节点的key对象进行比较,这里比较的是key对象的hashcode,直到碰到返回true的节点对象为止。
- 返回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的节点,扩容方法等等,实现起来比较麻烦,留作后期再实现。
下面记录一些关于泛型的内容
至此,我已经见过几种泛型的使用方法,但是一不小心就容易弄混淆,搞不清楚泛型到底怎么用,现在总结几种我已经见过的泛型的用法。
- 在链表节点的类处添加泛型,指定存入数据类型,对Node进行实例化的时候就只能添加 E 类型的数据了。
- 把Node类当做一个创建节点的工具,在实现类处添加泛型,此时实现类的方法就只能添加 E 类型的数据了。
- 前两种结合依然可以正确使用泛型。