转载

Java的equals和hashCode方法浅谈

equalshashCode 作为Java基础经常在面试中提到,比如下面几个问题:

1、 equals== 有什么区别?

2、 equalshashCode 有什么关系?

3、 equalshashCode 如何编写?

对于第一个问题不少人只停留在字符串 equals 比较的是内容, == 比较的是内存地址,而对 equals 的本质极少过问。第二个问题,大多数都知道答案,也有不少记反了,但是更进一步为什么是那样的关系,就不知道了。对于第三个问题,大部分人一上手就把方法签名写错了,就别谈正确的写出实现了。带着这些问题,接下来谈谈自己的一点理解。

二、equals方法

先来看见 equals 方法的签名,

public boolean equals(Object obj) {
  return (this == obj);
}

可以看到入参是 Object ,很多人没有注意到这一点,上来就写错了。equals方法顾名思义就判断对象的相等性,默认实现就是 == ,那么说到二者的区别,个人理解, equals 方法是一种用户定义的“逻辑等”,而 == 是一种“物理等”,用俗语解释就是, equals 判断是否相同, == 判断是否一样。

equals 方法在编写的时候需要遵循以下原则:

  • 自反性
  • 对称性
  • 传递性
  • 一致性

下面展开说一下,

1、自反性的意思是,对于一个非 null 的对象x, x.equals(x) 一定为 true ,这是显而易见的,无须赘述。

2、对称性,对于非 null 对象x、y, x.equals(y) == true ,当且仅当 y.equals(x) == true 。来看一个来自《Effective Java》的例子,

// Broken - violates symmetry!
public final class CaseInsensitiveString {
  private final String s;

  public CaseInsensitiveString(String s) {
    this.s = Objects.requireNonNull(s);
  }

  // Broken - violates symmetry!
  @Override public boolean equals(Object o) {
    if (o instanceof CaseInsensitiveString)
      return s.equalsIgnoreCase(
          ((CaseInsensitiveString) o).s);
    if (o instanceof String)  // One-way interoperability!
      return s.equalsIgnoreCase((String) o);
    return false;
  }
  ...  // Remainder omitted
}

CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
// true or false
list.contains(s);
`

在JDK8运行 list.contains(s) 返回 false ,但是有的JDK可能会返回 true ,甚至直接崩溃,所以如果违反了对称性,程序的行为是不可预测的。

3、传递性,对于非 null 对象x、y、z,如果 x.equals(y) == truey.equals(z) == true ,那么 x.equals(z) == true

public class Point {
  private final int x;
  private final int y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  @Override public boolean equals(Object o) {
    if (!(o instanceof Point))
      return false;
    Point p = (Point)o;
    return p.x == x && p.y == y;
  }

  ...  // Remainder omitted
} 

public class ColorPoint extends Point {
  private final Color color;

  public ColorPoint(int x, int y, Color color) {
    super(x, y);
    this.color = color;
  }

  // Broken - violates transitivity!
  @Override public boolean equals(Object o) {
    if (!(o instanceof Point))
      return false;

    // If o is a normal Point, do a color-blind comparison
    if (!(o instanceof ColorPoint))
      return o.equals(this);

    // o is a ColorPoint; do a full comparison
    return super.equals(o) && ((ColorPoint) o).color == color;
  } 

  ...  // Remainder omitted
} 

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

显然这个实现违反了传递性, p1.equals(p2) == p2.equals(p3) != p1.equals(p3) 。假如 Point 有两个子类 ColorPointSmellPointcolorPoint.equals(smellPoint) 将会导致无限递归,最终导致内存耗尽。引用《Effective Java》的说法,

There is no way to extend an instantiable class and add a value component while preserving the equals contract, unless you’re willing to forgo the benefits of object-oriented abstraction. 

这句话的大意是如果你继承扩展一个类,就没法再保持 equals 的原则了,除非放弃使用继承。放弃继承?这不是让我们因噎废食嘛,咦,别说,还真能放弃继承,那就是组合,因为本文的重点是 equalshashCode 就不展开了。

4、一致性,对于非 null 对象x、y,多次调用 x.equals(y) 返回一致。一致性意味着 equals 方法不要依赖不可靠的变量,这里“可靠”的意思不光意味着“不该变时不变”,还意味着“想获取时能获取到”,比如 java.net.URLequals 实现依赖了ip地址,而网络故障时无法获取ip,这是一个不好的实现。

说了那么多,有人可能会说,哎呀这么多原则顾头不顾尾,都要满足,太难了吧,下面列出实现 equals 的一些tips,照着做实现起来就易如反掌,

1、使用 == 判断是否为 nullthis ,如果是前者返回 false ,后者就返回 true

2、使用 instanceof 检测是否是正确的类型,如果不是直接返回 false ,如果是,强制转换为正确的类型,然后比较与“逻辑等”相关的变量。

三、hashCode方法

hashCode 主要用来在Java中哈希数据结构 HashMapHashSet 生成哈希值, hashCode 的方法签名,

public native int hashCode();

默认实现会将对象的内存地址转化为一个整数,因此只有同一个对象 hashCode 才一样,即使两个 equals 返回 true 的对象 hashCode 也不一样,如果不进行重写。

equals 一样, hashCode 也需要满足一些原则,

1、一致性,和 equals 相关的变量没有变化, hashCode 返回值也不能变化。

2、两个对象 equals 返回 truehashCode 返回值应该相等。由上面得知, hashCode 默认实现不满足这一条件,因此任何类如果实现了 equals 就必须实现 hashCode ,确保二者的步调一致,下面来看一个反例,

public class Person {
  private int age;
  private String name;

  public Person(int age, String name) {
    this.age = age;
    this.name = name;
  }

  @Override
    public boolean equals(Object obj) {
      if (obj == null) {
        return false;
      }
      if (obj == this) {
        return true;
      }

      if (obj instanceof Person) {
        Person that = (Person) obj;
        return age == that.age && Objects.equals(name, that.name);
      }

      return false;
    }
}

Map<Person, Integer> map = new HashMap<>();
map.put(new Person(10, "小明"), 1);
map.get(new Person(10, "小明"));

初学者可能觉得最后一条语句会返回1,事实上返回的是 null ,为什么会这样呢?明明将数据放进去了,而数据却像被黑洞吞噬一样,要解释得从 HashMap 的数据结构说起, HashMap 是由数组和链表组成的一种组合结构,如下图,往里存放时, hashCode 决定数组的下标,而 equals 用于查找值是否已存在,存在的话替换,否则插入;往外取时,先用 hashCode 找到对应数组下标,然后用 equals 挨个比较直到链表的尾部,找到返回相应值,找不到返回null。再回过头看刚才的问题,先放进去一个 new Person(10, "小明") ,然后取的时候又 new Person(10, "小明") ,由于没有重写 hashCode ,这两个对象的 hashCode 是不一样的,存和取的数组下标也就不一样,自然取不出来了。

Java的equals和hashCode方法浅谈

3、两个对象 equals 返回 falsehashCode 返回值可以相等,但是如果不等的话,可以改进哈希数据结构的性能。这条原则也可以用 HashMap 的数据结构解释,举一个极端的例子,假如 Person 所有对象的 hashCode 都一样,那么 HashMap 内部数组的下标都一样,数据就会进到同一张链表里,这张链表比正常情况下要长的多,而遍历链表是一项耗时的工作,性能也就下来了。

那么如何写一个好的 hashCode 呢?

1、声明一个变量 int 的变量 result ,将第一个和 equals 相关的实例变量的 hashCode 赋值给它。

2、然后依次计算剩下的实例变量的 hashCodec

(a) 如果是null,设置一个常数,通常为0

(b) 如果是原始类型,使用 Type.hashCode(f)Type 为它们的装箱类型

(c) 如果是数组,如果每一个元素都是相关的,可以使用 Arrays.hashCode ;否则将相关元素看作独立的变量计算

3、使用 result = 31 * result + c 的形式将每个变量的哈希值组合起来,最后返回 result

参考资料:《Effective Java》

原文  https://pingao777.github.io/2018/07/02/Java的equals和hashCode方法浅谈/
正文到此结束
Loading...