概述

本文将探讨如何在Eclipse IDE中自动生成equals()hashCode()方法。我们将展示Eclipse的强大且便利的代码自动生成功能,并强调尽管如此,对代码的精心测试仍然是必要的。

规则

在Java中,equals()用于检查两个对象是否等价。测试这个方法的一个好方法是确保对象满足对称性、反射性和传递性。对于三个非空对象abc

  • 对称性 - 如果a.equals(b)`,则b.equals(a)*
  • 反射性 - a.equals(a)
  • 传递性 - 如果a.equals(b)并且b.equals(c),则a.equals(c)

hashCode()必须遵守一个规则:

  • 对于equals()为真的两个对象,它们的hashCode()值必须相同

包含基本类型的类

让我们考虑一个只包含基本成员变量的Java类:

public class PrimitiveClass {

    private boolean primitiveBoolean;
    private int primitiveInt;

    // constructor, getters and setters
}

使用Eclipse IDE,我们可以通过“源”>“生成hashCode()equals()”来添加这些方法。Eclipse会显示以下对话框:

选择“全选”可以确保包含所有成员变量。注意,在插入点下方列出的选项会影响生成代码的风格。这里我们不选择任何选项,点击“确定”,方法就会被添加到我们的类中:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + (primitiveBoolean ? 1231 : 1237);
    result = prime * result + primitiveInt;
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null) return false;
    if (getClass() != obj.getClass()) return false;
    PrimitiveClass other = (PrimitiveClass) obj;
    if (primitiveBoolean != other.primitiveBoolean) return false;
    if (primitiveInt != other.primitiveInt) return false;
    return true;
}

生成的hashCode()方法首先声明一个质数(31),对基本对象执行各种操作,然后根据对象的状态返回结果。

equals()首先检查两个对象是否是同一个实例(==),如果是,则返回true。

接下来,它检查比较对象是否非空,且两个对象属于同一类,如果不符合,则返回false。

最后,equals()检查每个成员变量的相等性,如果有任何一个不相等,就返回false。

我们可以编写简单的测试用例:

PrimitiveClass aObject = new PrimitiveClass(false, 2);
PrimitiveClass bObject = new PrimitiveClass(false, 2);
PrimitiveClass dObject = new PrimitiveClass(true, 2);

assertTrue(aObject.equals(bObject) && bObject.equals(aObject));
assertTrue(aObject.hashCode() == bObject.hashCode());

assertFalse(aObject.equals(dObject));
assertFalse(aObject.hashCode() == dObject.hashCode());

包含集合和泛型的类

现在,考虑一个更复杂的Java类,带有集合和泛型:

public class ComplexClass {

    private List<?> genericList;
    private Set<Integer> integerSet;

    // constructor, getters and setters
}

再次使用Eclipse的“源”>“生成hashCode()equals()”。注意到hashCode()使用instanceOf来比较类对象,因为我们已经在对话框的Eclipse选项中选择了“使用instanceof比较类型”。我们得到的结果如下:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((genericList == null)
      ? 0 : genericList.hashCode());
    result = prime * result + ((integerSet == null)
      ? 0 : integerSet.hashCode());
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null) return false;
    if (!(obj instanceof ComplexClass)) return false;
    ComplexClass other = (ComplexClass) obj;
    if (genericList == null) {
        if (other.genericList != null)
            return false;
    } else if (!genericList.equals(other.genericList))
        return false;
    if (integerSet == null) {
        if (other.integerSet != null)
            return false;
    } else if (!integerSet.equals(other.integerSet))
        return false;
    return true;
}

生成的hashCode()方法依赖于Java核心库中的AbstractList.hashCode()AbstractSet.hashCode()方法。这些方法会遍历集合,计算每个项目的hashCode()值并返回结果。

同样,生成的equals()方法使用AbstractList.equals()AbstractSet.equals(),通过比较它们的字段来判断集合是否相等。

我们可以通过一些示例来验证其健壮性:

ArrayList<String> strArrayList = new ArrayList<String>();
strArrayList.add("abc");
strArrayList.add("def");
ComplexClass aObject = new ComplexClass(strArrayList, new HashSet<Integer>(45,67));
ComplexClass bObject = new ComplexClass(strArrayList, new HashSet<Integer>(45,67));
        
ArrayList<String> strArrayListD = new ArrayList<String>();
strArrayListD.add("lmn");
strArrayListD.add("pqr");
ComplexClass dObject = new ComplexClass(strArrayListD, new HashSet<Integer>(45,67));
        
assertTrue(aObject.equals(bObject) && bObject.equals(aObject));
assertTrue(aObject.hashCode() == bObject.hashCode());

assertFalse(aObject.equals(dObject));
assertFalse(aObject.hashCode() == dObject.hashCode());

继承

考虑使用继承的Java类:

public abstract class Shape {
    public abstract double area();

    public abstract double perimeter();
}

public class Rectangle extends Shape {
    private double width;
    private double length;
   
    @Override
    public double area() {
        return width * length;
    }

    @Override
    public double perimeter() {
        return 2 * (width + length);
    }
    // constructor, getters and setters
}

public class Square extends Rectangle {
    Color color;
    // constructor, getters and setters
}

如果我们尝试在Square类上使用“源”>“生成hashCode()equals()”,Eclipse会警告我们:“超类Rectangle没有重新声明equals()hashCode():生成的代码可能无法正常工作”。

类似地,当我们尝试在Rectangle类上生成hashCode()equals()时,也会收到关于超类Shape的警告。

尽管有警告,Eclipse仍然允许我们继续。在Rectangle的情况下,它扩展了一个不能实现hashCode()equals()的抽象Shape类,因为没有具体的成员变量。在这种情况下,我们可以忽略Eclipse的警告。

然而,Square类继承了Rectanglewidthlength成员变量,以及它自己的颜色变量。在Square类中直接创建hashCode()equals(),而不先为Rectangle做同样的事情,意味着equals()hashCode()中只使用color

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((color == null) ? 0 : color.hashCode());
    return result;
}
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null) return false;
    if (getClass() != obj.getClass()) return false;
    Square other = (Square) obj;
    if (color == null) {
        if (other.color != null)
            return false;
    } else if (!color.equals(other.color))
        return false;
    return true;
}

快速测试表明,如果仅width不同,Squareequals()hashCode()是不够的,因为width没有包含在equals()hashCode()的计算中:

Square aObject = new Square(10, Color.BLUE);     
Square dObject = new Square(20, Color.BLUE);

Assert.assertFalse(aObject.equals(dObject));
Assert.assertFalse(aObject.hashCode() == dObject.hashCode());

让我们修复这个问题,使用Eclipse为Rectangle类生成equals()hashCode()

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    long temp;
    temp = Double.doubleToLongBits(length);
    result = prime * result + (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(width);
    result = prime * result + (int) (temp ^ (temp >>> 32));
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null) return false;
    if (getClass() != obj.getClass()) return false;
    Rectangle other = (Rectangle) obj;
    if (Double.doubleToLongBits(length)
      != Double.doubleToLongBits(other.length)) return false;
    if (Double.doubleToLongBits(width)
      != Double.doubleToLongBits(other.width)) return false;
    return true;
}

我们需要重新生成Square类的equals()hashCode(),以便调用Rectangle的版本。在这个生成的代码中,我们在Eclipse对话框中选择了所有选项,所以可以看到注释、instanceOf比较和if块:

@Override
public int hashCode() {
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + ((color == null) ? 0 : color.hashCode());
    return result;
}


@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (!super.equals(obj)) {
        return false;
    }
    if (!(obj instanceof Square)) {
        return false;
    }
    Square other = (Square) obj;
    if (color == null) {
        if (other.color != null) {
            return false;
       }
    } else if (!color.equals(other.color)) {
        return false;
    }
    return true;
}

重新运行上面的测试,我们现在通过了,因为SquarehashCode()equals()计算正确。

结论

Eclipse IDE非常强大,能自动生成模板代码,如getter和setter、各种构造函数、equals()hashCode()。理解Eclipse的运作方式可以帮助我们减少这类编码任务的时间。但仍然需要谨慎,并通过测试确保处理了所有预期情况。

代码片段如往常一样可以在GitHub上找到。