直接寻址表:
直接寻址是汇编里面的概念:即直接给出指定数据在内存中的地址
上图中的U指的是定义了一个关键字的全域(即所有的关键字都在里面) k为实际的关键字(实际使用到的)
T为一个数组(T即为一个直接寻址表),其中每个位置都对应U里面的一个关键字(和直接寻址的概念相同)
问题:
U很大时,保存对应的T不合实际
实际使用的k相对于U很小时,则分配给T的空间大部分都浪费
Hash表:由一个直接寻址表和hash函数组成
常用的Hash函数:
除法散列函数(mysql),乘法散列函数,全域散列
上图T极为一个Hash表,就是将K1,k2...等通过Hash函数获取到Hash值(HashCode,int类型)存放在Hash表(T)中
(是将K1,K2,K3等关键字的值通过Hash函数计算,而不是K1,K2,K3等关键字的内存地址通过Hash函数计算)
所以HashCode表示的就是关键字在Hash表中的位置
HashCode的作用:查找的快捷性
eg:有一个能存放1000个数这样大的内存中,在其中要存放1000个不一样的数字,用最笨的方法,就是存一个数字,就遍历一遍,看有没有相同得数,当存了900个数字,开始存901个数字的时候,就需要和前面已经存的900个数字进行对比。
假设hash表中有1、2、3、4、5、6、7、8个位置,存第一个数,hashcode为1,该数就放在hash表中1的位置,存到100个数字,hash表中8个位置会有很多数字了,1中可能有20个数字,存101个数字时,他先查hashcode值对应的位置,假设为1,那么就有20个数字和他的hashcode相同,他只需要跟这20个数字相比较(equals),如果都不相同,那么就放在1这个位置。
Hash碰撞(Hash冲突):
关键字K是无限的,而Hash表T是有限的,所以会存在存储位置冲突的情况(就是上面的例子中,hash表中位置1里面有20个数字了,这种情况就是Hash冲突)
解决方案:
- 方式一:开放寻址法,如果哈希函数返回的位置已经有值,则继续向后探寻新的位置来存储这个值。而继续向下探寻位置的方法有
- 线性探查,如果位置i被占用,则向下探查i+1,i+2 ……直至空位置
- 二次探查,如果位置i被占用,则依次探查i+1^2,i+2^2……直至空位置
- 二度哈希:有n个哈希函数,等使用第一个哈希函数h1,发生冲突时,依次尝试使用h2,h3……
- 方式二:链表法(mqsql),哈希表每个位置都链接一个链表,当冲突发生时,冲突的元素被加到该位置链表后面
equals()和HashCode()关系:
- equals(): 用来判断两个对象是否相同,在Object类中是通过比较对象的内存地址
- hashCode(): 获取哈希码,也称为散列码,返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置
一个是对象的内存地址,一个是哈希表的索引位置,所以此时来看,是没有关系的
两个对象的hashCode()返回值相同,两对象不一定相同,但俩个对象equals()相同,则hashCode()必定相同:
其实就是上面的俩点:同一个对象在哈希表的索引位置必然相同,不同对象在哈希表的索引位置也可能相同
为啥重写equals()和hash():
equals()比较的是俩个对象的内存地址,所以需要重写(实际中也直接比较id字符串来代替比较俩个对象)
equals()重写了的话,若不重写hash(),则会出现equals()比较结果相同,但hash()比较结果不同的情况,所以要重写hash()
按照上面的说法,则只重写equals()不重写hashCode()也不会有问题
但对象创建HashSet和hashMap等集合时:
eg:
public class HashEqualsDemo {
static class Person {
private String age;
Person(String age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" + "age='" + age + '\'' + '}';
}
}
}
public static void main(String[] args) {
HashSet set1 = new HashSet();
set1.add("1");
set1.add("1");
for (Object a : set1) {
System.out.println(a);
}
HashSet set2 = new HashSet();
Person p1 = new Person("1");
Person p2 = new Person("1");
set2.add(p1);
set2.add(p2);
for (Object a : set2) {
System.out.println(a);
}
}
}
结果:
1
Person{age='1'}
Person{age='1'}
分析:
set1.add(“1”);:set1集合为空,找到hashCode对应在哈希表中的存储区,直接存入字符串1
set1.add(“1”);:首先判断该字符串1的hashCode值对应哈希表中所在的存储区域是否有相同的hashCode,此处调用String类中的hashCode()
,显然两次返回了相同的hashCode,接着进行equals()方法的比较,此处调用String类中的equals()
,由于两个字符串指向的常量池中的同一个字符串1,所以两个String对象相同,字符串1重复,不进行存储。
set2.add(p1);:set2集合为空,找到对象p1的hashCode对应在哈希表中的存储区,直接存入对象p1
set2.add(p2);:首先判断该对象p2的hashCode值对应哈希表中所在的存储区域是否有相同的hashCode,Person中未重写hashCode()此处调用Object类中的hashCode()
,所以jdk使用默认Object的hashCode方法,返回内存地址转换后的整数,因为p1、p2为不同对象,地址值不同,所以这里不存在与p2相同hashCode值的对象,直接存入对象p2
所以有使用到对象的哈希集合等操作时,必须重写hashCode()和equals()
修改:
public class HashEqualsDemo {
static class Person {
private String age;
Person(String age) {
this.age = age;
}
//重写equals()
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof Person)) {
return false;
}
//地址相同必相等
if (obj == this) {
return true;
}
Person person = (Person) obj;
//地址不同比较值是否相同
return person.age.equals(this.age);
}
//重写hashCode()
@Override
public int hashCode() {
return Objects.hash(age);
}
@Override
public String toString() {
return "Person{" + "age='" + age + '\'' + '}';
}
}
}
public static void main(String[] args) {
HashSet set1 = new HashSet();
set1.add("1");
set1.add("1");
for (Object a : set1) {
System.out.println(a);
}
HashSet set2 = new HashSet();
Person p1 = new Person("1");
Person p2 = new Person("1");
set2.add(p1);
set2.add(p2);
for (Object a : set2) {
System.out.println(a);
}
}
}
结果:
1
Person{age='1'}
另:
java对象的hashCode()方法:
java native int hashcode();
返回的值JVM内部实现,把java对象的内存地址转换成一个整数返回