天天看点

为什么使用HashSet储存对象时必须重写equals和hashcode方法

对于 Set 接口的实现类 HashSet,它是按照哈希算法来存取集合中的对象,并且因为其继承了 Set 接口,所以不允许插入相同的数据。但是当我们在储存自定义的类的时候会出现相同的对象,我们来查看下面一个示例。

示例一:

User.java

/**
 * Created by MGL on 2017/4/22.
 */
public class User {
  private String number;//学号
  private String name;   //姓名
  private Integer age;    //年龄

  public User() {
  }

  public User(String number, String name, Integer age) {

      this.number = number;
      this.name = name;
      this.age = age;
  }


  public String getNumber() {
      return number;
  }

  public void setNumber(String number) {
      this.number = number;
  }

  public String getName() {
      return name;
  }

  public void setName(String name) {
      this.name = name;
  }

  public Integer getAge() {
      return age;
  }


  public void setAge(Integer age) {
      this.age = age;
  }

  @Override
  public String toString() {
      return "User{" +
              "number='" + number + '\'' +
              ", name='" + name + '\'' +
              ", age=" + age +
              '}';
  }
}
           

测试方法:

Test.java

import java.util.HashMap;
import java.util.HashSet;

/**
 * Created by MGL on 2017/4/21.
 */
public class Test {
    public static void main(String[] args) {
        HashSet<User> set = new HashSet<>();
        User user1 = new User("123", "zhangsan", );
        User user2 = new User("123", "zhangsan", );
        set.add(user1);
        set.add(user2);
        System.out.println(set.size());
    }
}
           

此时我们可以看到如下的输出结果:

为什么使用HashSet储存对象时必须重写equals和hashcode方法

Set 集合有去重的功能,但是在向 Set 集合中添加自定义的对象时无法去重,我们重写一下 User 类的 equals 和 hashCode 方法(此处的 equals 和 hashCode 为 IDEA 自动生成的)。

/**
 * Created by MGL on 2017/4/22.
 */
public class User {
     private String number;//学号
     private String name;   //姓名
     private Integer age;    //年龄

    //无参构造函数    
     public User() {
     }

    //3个参数的构造函数
     public User(String number, String name, Integer age) {
         this.number = number;
         this.name = name;
         this.age = age;
     }


     public String getNumber() {
         return number;
     }

     public void setNumber(String number) {
         this.number = number;
     }

     public String getName() {
         return name;
     }

     public void setName(String name) {
         this.name = name;
     }

     public Integer getAge() {
         return age;
     }


     public void setAge(Integer age) {
         this.age = age;
     }

     @Override
     public boolean equals(Object o) {
         //判断传入的是否为同一个对象
         if (this == o) return true;
         //判断传入的对象是否为空,是不是相同的类
         if (o == null || getClass() != o.getClass()) return false;
        //强转
         User user = (User) o;
        //判断学号是否相同
         if (number != null ? !number.equals(user.number) : user.number != null) return false;
         //判断姓名是否相同
         if (name != null ? !name.equals(user.name) : user.name != null) return false;
         //判断年龄是否相同
         return age != null ? age.equals(user.age) : user.age == null;
     }

    /**
     * Hash值的获取方式
     * @return
     */
     @Override
     public int hashCode() {
         //获取学号、姓名、年龄的 hashCode 分别乘以 31
         int result = number != null ? number.hashCode() : ;
         result =  * result + (name != null ? name.hashCode() : );
         result =  * result + (age != null ? age.hashCode() : );
         return result;
     }

     @Override
     public String toString() {
         return "User{" +
                 "number='" + number + '\'' +
                 ", name='" + name + '\'' +
                 ", age=" + age +
                 '}';
     }
}
           

实例二:

Test02.java:

import java.util.HashSet;

/**
 * Created by MGL on 2017/5/4.
 */
public class Test02 {
    public static void main(String[] args) {

        HashSet<User> set = new HashSet<>();
        User user1 = new User("123", "zhangsan", );
        User user2 = new User("123", "zhangsan", );
        set.add(user1);
        set.add(user2);
        System.out.println(set.size());
    }
}
           
为什么使用HashSet储存对象时必须重写equals和hashcode方法

这次存入了一个对象了,这是为什么呢??

在上一篇的文章 HashSet解析 中我们已经知道了 HashSet的内部储存原理其实是 HashMap 虽然在前面的几篇博客中已经介绍了 HashMap 的内部实现,但是还是感觉到了自己的生疏和不足,我们再来贴部分源码来观察一下。

HashSet的 add 方法实际上调用的是 HashMap 的put 方法,我们来看看 HashMap 的 put 方法。

HashMap put 方法:

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

HashMap hash方法:

static final int hash(Object key) {
        int h;
        return (key == null) ?  : (h = key.hashCode()) ^ (h >>> );
    }
           

调用 put 方法时,会调用相应的自定义对象的 hashCode 方法,来获取该对象在 Node 数组中的坐标。因此我们在 User 对象中重写 hashCode 方法是确保相同的对象在同一个 Node 数组中会有相同的坐标。

在储存对象时当发生冲突的时候会经过下面一段代码:

if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
           

这段代码首先比较两个对象是不是同一个对象,然后通过 equals 方法判断对象的属性值是否相同,来确认是否为同一个对象。

看看下面的测试用例:

Test03:

import java.util.HashSet;

/**
 * Created by MGL on 2017/5/4.
 */
public class Test03 {
    public static void main(String[] args) {

        HashSet<User> set = new HashSet<>();
        User user1 = new User("123", "zhangsan", );
        set.add(user1);
        set.add(user1);
        System.out.println(set.size());
    }
}
           

其实这两段代码的运行结果是一样的,但是它们的内部运行原理是不相同的,在 Test02 中向 Set 集合中添加了两个不同对象,两个对象的属性值相同,在 Test03 中也是向 Set 集合中添加了两个对象,但是是同一个对象。到底有什么不同,我们在 User 类的 equals 方法出打个断点,调试一下,我们可以发现一个有趣的现象,Test02 可以调用到 equals 而 Test03 不会调用到 equals 这个方法,这到底发生了什么???

其实问题的关键还是在于这一行代码:

if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
           

当我们储存同一个对象的时候 (k = p.key) == key 计算结果为 true ,运用双或 的特点,直接就返回了 true ,而 Test03 不是传入的同一个对象,因此会调用 key.equals(k) 这段代码,调用自定义类的 equals 方法。

总结

1.我们在使用 HashSet 对自定义类进行去重的时候,一定要覆盖自定义类的 equals 和 hashCode 方法,hashCode 方法是找到当前对象在 Node 数组重的位置,而 equals 是比较当前对象与对应坐标链表中的对象是否相同。

2.在使用Set对象储存自定义对象的时候,每次都会调用自定义对象的 hashCode 方法,但是 equals 方法并不是每次都会被调用到,不会被调用到的情形有下:

  1. 传入同一个对象;
  2. 当前坐标为空;

3.要根据自己要实现的功能,合理的重写 hashCode 和 equals 方法来达到去重的目的。

帅照:

为什么使用HashSet储存对象时必须重写equals和hashcode方法

继续阅读