1.顺序表的问题

查找和去重效率较低

对于这样的顺序表来说,如果需要查找元素,就需要从第一个元素逐个检查,进行查找。对于需要去重的存储来说,每次存入一个元素之前,就得将列表中的每个元素都比对一遍,效率相当低。

1.1.解决思路

我们注意到在这里的顺序表中列表中的每个元素都有一个与之对应的索引,不过这里的索引只是与元素所在的位置有对应关系,也就是说:

索引与顺序表中的位置有一一对应关系,但是与位置中的元素没有必然的联系。如果有某种方式能够建立这样的一个关系:

如图中所示,也就是说每个元素与其在顺序表中的索引,以及位置都有一个必然的对应关系,如此一来,每个元素在被存入数据结构之前,其位置就已经被确定了。这样无论是要查找,还是去重都会机器方便。

2.散列表

2.1.哈希函数的作用

如前面提出的这种解决思路,给每一个元素在表中找一个唯一确定的位置的这种解决方案被称为散列表。

那么,问题又来了,如何确定要存入数组的元素在表中的位置呢?这就是哈希函数的作用。

哈希函数的作用就是利用要存入表的元素的属性信息,生成一个唯一的整型值,这个值被称为哈希值,利用哈希值在表中确定一个固定的位置,用来存储这个元素。

2.2.字符串转整型的问题

一般来说存储元素的属性无外乎就两种类型,一种是数值型的,要转成整型没什么好说的;另外就主要是字符串型的,对于字符串如何将其转成整型呢?

我们知道字符串是由一个个的字符组成的,而我们可以根据ASCII码表将字符转换成对应的整型编码,这样只需要将字符串中的每个字符转成整型,然后进行相应的计算即可。

2.3.BKDR哈希算法

哈希算法有很多种,此处我们介绍一种比较常用的哈希算法,下面是这种算法的C语言实现版本。

// BKDR Hash Function
unsigned int BKDRHash(char *str)
{
unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
unsigned int hash = 0; while (*str)
{
hash = hash * seed + (*str++);
} return (hash & 0x7FFFFFFF);
}

观察这个函数,其实其内部的逻辑就是遍历一个字符数组,将每个元素对应的ASCII码值乘以一个数,然后累加起来的结果,可以转换成如下表示的一个结果:

3.重写hashCode()

3.1.重写hashCode()的原因

public class Student {

    private String num;
private String name; public Student(String num, String name) {
this.num = num;
this.name = name;
} public static void main(String[] args) {
Student stu1 = new Student("10001", "赤骥");
Student stu2 = new Student("10001", "赤骥");
Student stu3 = new Student("10002", "白义"); System.out.println("赤骥的HashCode:" + stu1.hashCode());
System.out.println("赤骥的HashCode:" + stu2.hashCode());
System.out.println("白义的HashCode:" + stu3.hashCode());
}
}

这段代码执行的结果是:

赤骥的HashCode:366712642
赤骥的HashCode:1829164700
白义的HashCode:2018699554

这段代码中,我们打印出三个对象的哈希值,我们看到Student这个类中并没有hashCode()方法,因为在Java的继承体系中,Object类是所有类的超类,也就是说实际上Student类是继承了Object类的,因此这里没有写hashCode()方法,那么调用的就是Object类的hashCode()方法了。

而根据我们之前对哈希函数的定义,这个Object类中继承的hashCode()方法显然不适用于这个Student类。因为stu1和stu2这两个对象的属性值是完全一样的,那么从业务角度来说,这两个对象应该就是重复的,那么他们生成的哈希值也应该是一致的,而现在显然并不一致,因此我们需要为这个Student类重写hashCode()方法。

3.2.如何重写hashCode()方法

@Override
public int hashCode() {
StringBuilder sb = new StringBuilder();
sb.append(num);
sb.append(name);
char[] charArr = sb.toString().toCharArray();
int hash = 0; for(char c : charArr) {
hash = hash * 131 + c;
}
return hash;
}

将所有需要参与计算的属性值都合并成一个字符串,然后转换成一个字符数组:

char[] charArr = sb.toString().toCharArray();

然后遍历这个字符数组进行计算。

4.Java中常用的哈希表

4.1.hashCode()在HashSet和HashMap中的作用

现在以及编写好了hashCode()方法,我们到实际的案例中去使用一下。在Java中常用的哈希表有HashSet和HashMap。

此处我们先以HashSet为例:

import java.util.HashSet;
import java.util.Set; public class Student { private String num;
private String name; public Student(String num, String name) {
this.num = num;
this.name = name;
} @Override
public int hashCode() {
StringBuilder sb = new StringBuilder();
sb.append(num);
sb.append(name);
char[] charArr = sb.toString().toCharArray();
int hash = 0;
for(char c : charArr) {
hash = hash * 131 + c;
}
return hash;
} public static void main(String[] args) {
Student stu1 = new Student("10001", "赤骥");
Student stu2 = new Student("10001", "赤骥");
Student stu3 = new Student("10002", "白义"); Set<Student> students = new HashSet<>();
students.add(stu1);
students.add(stu2);
students.add(stu3);
System.out.println(students.size());
}
}

观察这段代码,根据我们之前重写的哈希函数,stu1和stu2应该是在相同位置的,并且他们的值是一样的,那么应该是只能够存放其中一个到这个set中,因此最终打印输出的结果应该是2。

而实际测试的结果为3。这是为什么?

我们来查看一下HashSet的源码:

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

我们找到HashSet中的这个add方法,看它是怎么实现的,可以看到这里调用了一个map对象的put方法来存放元素,可以才想到,实际上HashSet真正的实现是另外一个类,这个HashSet只是对其的一个封装,我们找到这个map,看看它到底是哪个类:

private transient HashMap<E,Object> map;

在HashSet前面的属性声明中可以看到这样一行代码,根据这个我们看出来实际上Java中的HashSet是依托于HashMap的实现的。那么接下来到HashMap中去找这个添加元素的方法看看:

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

这里真正存放元素的逻辑是在putVal()这个方法中,这里面代码较多就不贴上来了,这里简述一下其中的关键逻辑。它会调用存入元素的hashCode()方法,计算出元素所对应在表中的位置,然后判断这个位置上是否已经有内容了。如果这个位置上以及有了一个元素,那么就调用传入元素的equals()方法与已有的元素进行对比,以此来判断两个元素是否相同,如果不相同,就将这个元素也存入表中。

4.2.equals()方法的作用

也就是说,使用hashCode()方法确定元素在数据结构中存放的位置。而使用equals()来确认当两个元素存放的位置发生冲突时,是应该将两个元素都存入数据结构,还是说只需要存放其中一个。

如果equals()方法判断两个元素是一样的,那么当然只需要存放其中一个既可;但如果equals()方法判断两个对象是不同的,那么当然两个都需要存放到数据结构中。

5.重写equals()方法

重写equals()从逻辑上来说就比较简单了,先看下实例:

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} if (obj instanceof Student) {
if (((Student) obj).num.equals(this.num)
&& ((Student) obj).name.equals(this.name)) {
return true;
}
} return false;
}

首先判断是否是自己和自己比较,如果是那么肯定是相同的,因为是同一个对象。

然后再逐个比较对象的属性,如果属性值都相同,那么说明就是相同的对象。

在重写了equals()方法之后,重写再进行之前的测试,就可以发现结果是正确的了,在该集合中三个对象只能放入其中的两个,还有一个因为重复而无法放入。

6.String类的hashCode()方法和equals()方法

在前面的一系列介绍过程中,我们都是介绍自己定义的类的hashCode()方法和equals()方法。除了这些自定义的类,我们在平时编写代码过程中经常会用到一个系统的类,并且这个类也经常被用在HashMap中作为key来使用,那就是String类,我们可以看看这个类的hashCode()方法和equals()方法是如何编写的。

6.1.hashCode()方法

public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value; for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

观察这个hashCode()方法,基本上我们实现哈希函数的思路与这个是一致的。

6.2.equals()方法

public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

观察代码,可以发现对于String类来说,如果要判断两个String的实例相同,需要逐一判断这两个字符串中的字符是否相同。

Java——重写hashCode()和euqals()方法的更多相关文章

  1. Java 重写 hashCode() 和 equals() 方法

    1. hashCode 1.1 基本概念 hashCode 是 JDK 根据对象的地址算出来的一个 int 数字(对象的哈希码值),代表了该对象再内存中的存储位置. hashCode() 方法是超级类 ...

  2. 为什么要重写hashcode和equals方法?初级程序员在面试中很少能说清楚。

    我在面试 Java初级开发的时候,经常会问:你有没有重写过hashcode方法?不少候选人直接说没写过.我就想,或许真的没写过,于是就再通过一个问题确认:你在用HashMap的时候,键(Key)部分, ...

  3. (转)为什么要重写 hashcode 和 equals 方法?

    作者丨hsm_computer cnblogs.com/JavaArchitect/p/10474448.html 我在面试Java初级开发的时候,经常会问:你有没有重写过hashcode方法?不少候 ...

  4. HashMap中使用自定义类作为Key时,为何要重写HashCode和Equals方法

    之前一直不是很理解为什么要重写HashCode和Equals方法,才只能作为键值存储在HashMap中.通过下文,可以一探究竟. 首先,如果我们直接用以下的Person类作为键,存入HashMap中, ...

  5. Hibernate中用到联合主键的使用方法,为何要序列化,为何要重写hashcode 和 equals 方法

    联合主键用Hibernate注解映射方式主要有三种: 第一.将联合主键的字段单独放在一个类中,该类需要实现java.io.Serializable接口并重写equals和hascode,再将该类注解为 ...

  6. 为什么要重写hashcode和equals方法

    我在面试 Java初级开发的时候,经常会问:你有没有重写过hashcode方法?不少候选人直接说没写过.我就想,或许真的没写过,于是就再通过一个问题确认:你在用HashMap的时候,键(Key)部分, ...

  7. 重写hashcode和equals方法

    重写hashcode和equals方法 简乐君 2019-05-07 21:55:43 35481 收藏 191分类专栏: Java 文章标签: equals() hashcode()版权 一.前言我 ...

  8. Java 重写hashCode 方法和equals方法

    package Container; import java.util.HashSet; import java.util.Iterator; /* Set 元素是无序的(存入和取出的顺序不一定一致) ...

  9. java集合框架(hashSet自定义元素是否相同,重写hashCode和equals方法)

    /*HashSet 基本操作 * --set:元素是无序的,存入和取出顺序不一致,元素不可以重复 * (通过哈希值来判断是否是同一个对象) * ----HashSet:底层数据结构是哈希表, * 保证 ...

随机推荐

  1. iframe标签(页面嵌套)

    本文链接:https://blog.csdn.net/weixin_44540236/article/details/92760494 两个不同的页面但是它们的基本框架都是一样,每点击一次左边的导航栏 ...

  2. 三、Nginx支持php

    一.php fastcgi配置(分析宝塔的) 因为只是配置PHP,所以只需要改动server部分 关于phpmyadmin这个站点的信息: 查找该文件:

  3. MySQL之concat、concat_ws、group_concat

    concat(str1, str2, ...)  返回结果为连接一起的字符串. concat_ws(separator, str1, str2, ...) 同concat,但是可以指定连接符,sepa ...

  4. Centos7 更改网卡名称

    cd /etc/sysconfig/network-scripts/ 将要改名的网卡配置文件重命名,例如 mv ifcfg-eth1 ifcfg-eth0 vim ifcfg-eth0 修改devic ...

  5. 第八章 watch监听 83 名称案例-使用watch监听文本框数据的变化

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8&quo ...

  6. Error response from daemon: manifest for elasticsearch:latest not found

            五孔    35个     三孔空调     3个     一开五孔    10个     一开双控    10个     两开双控    2个     一开多控    3个     ...

  7. 基于Kibana的可视化监控报警插件sentinl入门

    sentinl是什么 Kibi/Kibana Alert & Reporting App Watching your data, 24/7/365 sentinl是一个免费的kibana预警与 ...

  8. 实现一个可无限折叠的table

    前言 如何在table上实现一个可折叠展开子节点的table?先看下最终实现效果图: 其实这个项目在两个月以前就以上上传在github了,但当时没有写详细的实现过程.自己前几天发表的一篇技术贴当下拉列 ...

  9. Nuxt 服务端渲染

    前言 Nuxt.js 是一个基于 Vue.js 的通用应用框架. ssr 渲染服务端 可以看官方文档 https://zh.nuxtjs.org/guide/ nuxt 与平常 jsonp 不一样 , ...

  10. 9种纯CSS3人物信息卡片动态展示效果

    <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8&quo ...