hashCode 和 equals 我觉得你真不一定懂

hashCode 和 equals 我觉得你真不一定懂首先我告诉你hashCode equals要覆盖的话一定要同时覆盖,否则会出现很多奇怪的问题,还有hashCode用不好会导致内存泄漏哦,请先记

欢迎大家来到IT世界,在知识的湖畔探索吧!

首先我告诉你hashCode equals要覆盖的话一定要同时覆盖,否则会出现很多奇怪的问题,还有hashCode用不好会导致内存泄漏哦,请先记下这些结论,下面我们慢慢分析。

假如你想查找一个集合中是否包含某个对象,那么代码应该怎么写呢?

通常的做法是逐一取出每个元素与要查找的对象一一比较,当发现两者进行equals比较结果相等时,则停止查找并返回true,否则,返回false。但是这个做法的一个缺点是当集合中的元素很多时,譬如有十万个元素,那么逐一的比较效率势必下降很快。

于是有人发明了一种哈希算法来提高从该集合中查找元素的效率,这种方式将集合分成若干个存储区域(可以看成一个个桶),每个对象可以计算出一个哈希码,拥有不同哈希码的对象被放入到不同的区域里面,如下图所示:

hashCode 和 equals 我觉得你真不一定懂

假设现在全部的对象都已经根据自己的hashCode值存储在不同的存储区域中了,那么现在查找某个对象,不需要遍历整个集合了,现在只需要计算要查找对象的的hashCode,然后找到该hashCode对应的存储区域,在该存储区域中来查找就可以了,这样效率也就提升了很多(这个地方如果更改了与计算hashcode相关元素的值,会存在内存泄露问题,后边会有专门的例子)。说了这么多相信你对hashCode的作用有了一定的了解,下面就来看看hashCode和equals的区别和联系。

hashCode 和 equals 的区别?

JDK对equals(Object obj)和hashCode()这两个方法的定义和规范:在Java中任何一个对象都具备equals(Object obj)和hashCode()这两个方法,因为他们是在Object类中定义的。

equals(Object obj)方法用来判断两个对象是否“相同”,如果“相同”则返回true,否则返回false。

hashCode()方法返回一个int数,在Object类中的默认实现是“将该对象的内部地址转换成一个整数返回”。

为什么重写了equals还需要重写hashCode呢?

一般来说涉及到对象之间的比较大小就需要重写equals方法,但是为什么重写了equals还需要重写hashCode呢?

其实如果不这样做代码也可以执行,只不过会隐藏bug。

类的对象如果会存储在HashTable,HashSet, HashMap等散列存储结构中,那么重写equals后一定也要重写hashCode否则会导致存储数据的不唯一性(会存储两个equals相等的数据)。而如果确定不会存储在这些散列结构中,则可以不重写hashCode。

但是,谁能保证后期不会存储在这些结构中呢,况且重写了hashCode也不会降低性能,因为在线性结构(如ArrayList)中是不会调用hashCode,所以为了保险起见一定要同时重写equals和hashCode。

不同时重写equals和hashCode会有什么问题?

下面我们以HashSet为例为大家讲解, HashTable HashMap等其他散列结构都会存在同样的问题

我先说下将对象放入到HashSet的大体流程:

hashCode 和 equals 我觉得你真不一定懂

从上图我们可以清晰的看到,想要判断一个对象是否存在于HashSet需要两个条件

  • 对象与HashSet中某一个对象的hashcode相等
  • 对象与HashSet中某一个对象equals相等

我们可以看一下具体的代码实现,HashSet 增加元素的add方法最终调用的是HashMap的put方法,所以我们把他们关键代码都贴出来

HashSet

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

欢迎大家来到IT世界,在知识的湖畔探索吧!

HashMap

欢迎大家来到IT世界,在知识的湖畔探索吧!  public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //这里就是判断元素是否和集合中元素相等的地方
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

下面演示具体的问题:

  • 覆盖equals(Object obj)但不覆盖hashCode(),导致数据不唯一性
class Point {
  private int x;
  private int y;

  public Point(int x, int y) {
    super();
    this.x = x;
    this.y = y;
  }
 //只是覆盖了equals没有覆盖hashCode
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Point other = (Point) obj;
    if (x != other.x)
      return false;
    if (y != other.y)
      return false;
    return true;
  }

  @Override
  public String toString() {
    return "x:" + x + ",y:" + y;
  }

}


public class HashCodeTest {
  public static void main(String[] args) {
    Collection set = new HashSet();
    //业务上我们会认为p1 p2是一样的,而且他们的equals也相等
    Point p1 = new Point(1, 1);
    Point p2 = new Point(1, 1);

    System.out.println(p1.equals(p2));
    set.add(p1);   //(1)
    set.add(p2);   //(2)

    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
      Object object = iterator.next();
      System.out.println(object);
    }
  }
}

结果p1 和 p2 都被放入了 HashSet, 是不是和你以前的理解不一样呢

欢迎大家来到IT世界,在知识的湖畔探索吧!true
x:1,y:1

原因分析:最核心的原因就是两个对象的hashCode不一致

  1. 当执行set.add(p1)时(1),集合为空,直接存入集合;
  2. 当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,因为没有覆盖hashCode方法,所以jdk使用默认Object的hashCode方法,返回内存地址转换后的整数,因为不同对象的地址值不同,所以这里不存在与p2相同hashCode值的对象,所以直接存入集合。
  • 覆盖hashCode方法,但不覆盖equals方法,仍然会导致数据的不唯一性

class Point {
  private int x;
  private int y;
 
  public Point(int x, int y) {
    super();
    this.x = x;
    this.y = y;
  }
 
  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + x;
    result = prime * result + y;
    return result;
  }
 
  @Override
  public String toString() {
    return "x:" + x + ",y:" + y;
  }
 
}
public class HashCodeTest {
  public static void main(String[] args) {
    Collection set = new HashSet();
    //业务上我们会认位p1 p2是一样的,而且他们的equals也相等
    Point p1 = new Point(1, 1);
    Point p2 = new Point(1, 1);

    System.out.println(p1.equals(p2));
    set.add(p1);   //(1)
    set.add(p2);   //(2)

    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
      Object object = iterator.next();
      System.out.println(object);
    }
  }
}

结果还是p1 和 p2 都被放入了 HashSet

原因分析:最核心的原因就是两个对象的equals不一致

  1. 当执行set.add(p1)时(1),集合为空,直接存入集合;
  2. 当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,这里覆盖了hashCode方法,p1和p2的hashCode相等,所以继续判断equals是否相等,因为这里没有覆盖equals,使用Object 的equals,即比较两个对象的内存地址,所以这里返回false,所以也将p2存入集合。

综合上述两个测试,要想保证元素的唯一性,必须同时覆盖hashCode和equals才行。

更改与计算hashCode有关变量的值会存在内存泄漏问题

class Point {
  private int x;
  private int y;
 
  public Point(int x, int y) {
    super();
    this.x = x;
    this.y = y;
  }
 
 
  public int getX() {
    return x;
  }
 
 
  public void setX(int x) {
    this.x = x;
  }
 
 
  public int getY() {
    return y;
  }
 
 
  public void setY(int y) {
    this.y = y;
  }
 
 //hashcode的生成和 x y 都有关系
  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + x;
    result = prime * result + y;
    return result;
  }
 
 
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Point other = (Point) obj;
    if (x != other.x)
      return false;
    if (y != other.y)
      return false;
    return true;
  }
 
 
  @Override
  public String toString() {
    return "x:" + x + ",y:" + y;
  }
 
}

public class HashCodeTest {
  public static void main(String[] args) {
    Collection set = new HashSet();
    Point p1 = new Point(1, 1);
    Point p2 = new Point(1, 2);
 
    set.add(p1);
    set.add(p2);
    
    //更改了与计算hashcode 有关的x y 的值
    p2.setX(10);
    p2.setY(10);
    
    set.remove(p2);
 
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
      Object object = iterator.next();
      System.out.println(object);
    }
  }
}

结果是p2并没有被删除,是不是很神奇

x:1,y:1
x:10,y:10

原因分析:hashcode变了,导致查找的区域错误

假设p1的hashCode为1,p2的hashCode为2,在存储时p1被分配在1号桶中,p2被分配在2号桶中。

这时修改了p2中与计算hashCode有关的信息(x和y),当调用remove(Object obj)时,首先会查找该hashCode值的对象是否在集合中。

假设修改后的hashCode值为10, 这时可能会去3号桶中查找,当然查找结果空,所以jdk认为该对象不在集合中,所以不会进行删除操作。

然而用户以为该对象已经被删除,导致该对象长时间不能被释放,造成内存泄露。

解决方法:

尽量不要在执行期间修改与hashCode值有关的对象信息,如果非要修改,则必须先从集合中删除该元素,修改完成后后再加入集合中。

总结

  1. 在散列结构中判断对象相等的条件是 hashcode相等以及equals 相等
  2. 覆盖equals时一定要覆盖hashCode
  3. 尽量不要修改与计算hashcode相关属性的值,非要修改的话先删除该元素,修改完再加入集合

喜欢我的文章给个关注吧,也可关注 WX “Java大讲堂” 获取最新干货, 私信笔者送一套60G Java学习资料

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/35359.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们YX

mu99908888

在线咨询: 微信交谈

邮件:itzsgw@126.com

工作时间:时刻准备着!

关注微信