当equals()使用相似性度量时,覆盖hashCode()以与equals()一致

时间:2015-02-09 00:49:09

标签: java equals hashcode

假设我有一个带有颜色和模型字段的汽车。我需要将汽车存放在一个集合中,我将不会重复(没有2辆相同的汽车)。在下面的示例中,我使用的是HashMap。

根据Java文档,如果我们有2个Car对象car1和car2,那么car1.equals(car2) == true,那么它也必须保持car1.hashCode() == car2.hashCode()。所以在这个例子中,如果我只是想用它们的颜色来比较汽车,那么我只会使用equals()hashCode()中的颜色字段,就像我在我的代码中所做的那样,它的工作原理非常好。

public class Car {
String color;
String model;

@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;
    Car other = (Car) obj;
    if (color == null) {
        if (other.color != null)
            return false;
    } else if (!color.equals(other.color))
        return false;
    return true;
}

public Car(String color, String model) {
    super();
    this.color = color;
    this.model = model;
}

@Override
public String toString() {
    return color + "\t" + model;
}

public static void main(String[] args) {
    Map<Car, Car> cars = new HashMap<Car, Car>();
    Car a = new Car("red", "audi");
    Car b = new Car("red", "bmw");
    Car c = new Car("blue", "audi");
    cars.put(a, a);
    cars.put(b, b);
    cars.put(c, c);
    for(Car car : cars.keySet()) {
        System.out.println(cars.get(car));
    }

}

}

  

输出结果为:

     
      
  • red bmw
  •   
  • blue audi
  •   

正如所料。

到目前为止一切都很好。现在,我正在尝试比较2辆汽车的其他方法。我提供了测量2辆车之间相似性的功能。为了论证,我假设我有一个方法double similarity(Car car1, Car car2),它在区间[0,1]中返回一个double值。如果它们的相似函数返回大于0.5的值,我认为2辆汽车是相同的。然后,我重写equals方法:

@Override
public boolean equals(Object obj) {
    Car other = (Car) obj;
    return similarity(this, other) > 0.5;
}

现在,我不知道如何覆盖hashCode()以确保始终保持hashCode - equals合约,例如2个相等的对象总是具有相同的hashCodes。

我一直在考虑使用TreeMap而不是HashMap,只是为了避免覆盖hashCode,因为我不知道如何正确地执行它。但是,我不需要任何排序,所以我发现在这个问题中使用TreeMap是不合适的,而且我认为在复杂性方面它会更昂贵。

如果你可以建议我,那将是非常有帮助的:一种覆盖hashCode的方法,或者是一种更适合我的问题的不同结构的替代方法。

提前谢谢!

6 个答案:

答案 0 :(得分:4)

虽然短跑运动员已经涵盖了您的策略的一些问题,但您的方法存在更多基于合同的问题。根据Javadoc,

  

[equals]是传递性的:对于任何非空参考值x,y和z,if   x.equals(y)返回true,y.equals(z)返回true,然后   x.equals(z)应该返回true

但是,x可能与y相似,而y可能与z类似,x与z相距太远,因此相似您的equals方法不起作用。

答案 1 :(得分:4)

您不应以这种方式篡改equalshashcode方法。 Collection数据结构依赖于这些方法,并以非标准方式使用它们会产生意外行为。

我建议您创建一个Comparator实施方案,该实施方案将比较两辆车或实施Comparable界面,您可以使用下面的similarity方法。

答案 2 :(得分:3)

这里有几点要做。

首先,这是equals的不寻常用法。通常,equals被解释为意味着这些是同一对象的两个实例;一个人可以在没有影响的情

第二点是a.equals(b)暗示a.hashCode() == b.hashCode()但不反过来。事实上,让所有对象返回相同的哈希码是完全合法的(尽管毫无意义)。因此,在您的情况下,只要所有足够similar汽车返回相同的哈希码,各种集合就会正常运行。

我怀疑你应该有一个单独的课来代表你的类似的&#39;概念。然后,您可以测试相似性的相等性或类似于汽车列表的地图。这可能是对概念的更好表示,而不是为汽车重载equals

答案 3 :(得分:3)

hashCode()只是一个&#34;捷径&#34;为equals()。确保您正在努力实施的方案对equals有意义,这一点非常重要。考虑汽车abc,其中similarity(a, b) == 0.3similarity(b, c) == 0.3

但如果similarity(a, c) == 0.6怎么办?然后,您处于a.equals(b)b.equals(c),但神秘a.equals(c)为假的情况。

这违反了Object.equals()的一般合同。发生这种情况时,HashMapTreeMap等标准库的某些部分会突然开始表现得非常奇怪。

如果您对插入不同的排序方案感兴趣,那么使用每个实现您的方案的不同Comparator<Car>会更好。虽然同样的限制适用于Comparator API 1 ,但它可以让您表示小于和大于,它听起来像您真正的追求,哪些不能通过Object.equals()完成。

[1]如果compare(a,b) == compare(b,c) == 0,则compare(a,c)也必须为0

答案 4 :(得分:2)

正如其他人所说,.equals()的后一种实施违反了合同。你根本无法以这种方式实现它。如果你停下来思考它,那就有意义了,因为.equals()的实现并不意味着当两个对象实际上相等时返回true,但当它们相似时足够。但足够相似 相等不一样,无论是在Java还是其他任何地方。

检查.equals() javadocs,您会发现任何实现它的对象都必须遵守合同:

  

equals方法在非null对象引用上实现等价关系:

     
      
  • 它是自反的:对于任何非空引用值x,x.equals(x)应该返回true。

  •   
  • 它是对称的:对于任何非空引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)才应返回true。

  •   
  • 它是传递性的:对于任何非空引用值x,y和z,如果x.equals(y)返回true而y.equals(z)返回true,则x.equals(z)应该返回true。

  •   
  • 它是一致的:对于任何非空引用值x和y,x.equals(y)的多次调用始终返回true或始终返回false,前提是没有修改对象的equals比较中使用的信息

  •   
  • 对于任何非空引用值x,x.equals(null)应返回false。

  •   

.equals()的实施不符合此合同:

  • 根据您double similarity(Car car1, Car car2)的实施情况,它可能不是对称的
  • 它显然不具有传递性(在以前的答案中有详细解释)
  • 可能不一致:

考虑一个与您在评论中提供的示例略有不同的示例:

&#39;钴&#39;等于&#39; blue&#39;虽然&#39; red&#39;与&#39; blue&#39;

不同

如果您使用某些外部来源来计算相似度,例如字典,并且如果有一天钴&#39;没有被发现作为一个条目,你可能会返回接近0.0的相似性,所以汽车不会相等。但是,第二天你就意识到了钴的问题。是一种特殊的蓝色&#39;所以你把它添加到字典中,这次,当你比较相同的两辆车时,相似性非常高(或接近1.0),所以它们相等。这将是不一致。我不知道您的相似度函数是如何工作的,但如果它取决于您要比较的两个对象中包含的数据之外的任何内容,那么您可能也违反了.equals()一致性约束。 / p>

关于使用TreeMap<Car, Whatever>,我不知道如何提供任何帮助。来自TreeMap javadocs

  

... Map接口是根据equals操作定义的,但是有序映射使用compareTo(或compare)方法执行所有键比较,因此从这个方法看,两个被认为相等的键是相同的排序后的地图,等于。

换句话说,在TreeMap<Car, Whatever> map中,map.containsKey(car1)会返回true iff car1.compareTo(car2)确切地0返回属于的某些car2 map。但是,如果比较没有返回0map.containsKey(car1)可能会返回false,尽管car1car2在相似性方面非常相似功能。这是因为.compareTo()用于排序,而不是用于相似性。

因此,关键点在于您不能单独使用Map来满足您的用例,因为它只是错误的结构。实际上,您不能单独使用任何依赖于.hashCode().equals()的Java结构,因为您永远找不到与您的密钥匹配的对象。


现在,如果您确实希望通过similarity()功能找到与给定汽车最相似的汽车,我建议您使用Guava's HashBasedTable structure构建相似度表你的每一辆车之间的系数(或你喜欢的任何其他花哨的名字)。

这种方法需要Car像往常一样实施.hashCode().equals() (即不仅仅按颜色检查,当然也不需要调用{{1}功能)。例如,您可以通过新的牌号 similarity()属性进行检查。

我们的想法是在每辆车之间设置一个存储相似度的表格,其对角线干净,因为我们已经知道汽车与自身相似(实际上,它是<&#39; s < em>等于自身)。例如,对于以下汽车:

Car

表格如下所示:

Car a = new Car("red", "audi", "plate1");
Car b = new Car("red", "bmw", "plate2");
Car c = new Car("light red", "audi", "plate3");

对于相似度值,我假设同一品牌和同一颜色系列的汽车比同色但不同品牌的汽车更相似,不同品牌和不同颜色的汽车更不相似

您可能已经注意到该表是对称。如果需要空间优化,我们只能存储一半的单元格。但是,根据文档, a b c a ---- 0.60 0.95 b 0.60 ---- 0.45 c 0.95 0.45 ---- 已经过优化,可以通过行键访问,因此请让它保持简单,并将进一步优化作为练习。

找到与给定汽车最相似的汽车的算法可以如下绘制:

  1. 检索指定车辆的行
  2. 在返回的行中返回与给定汽车最相似的汽车,即相似系数最高的行的汽车
  3. 这里有一些代码显示了一般的想法:

    HashBasedTable

    关于复杂性...初始化表需要 O(n2),而搜索最相似的汽车需要 O(n)。我很确定这可以改进,也就是说为什么把汽车放在已知相似的表中? (我们只能放置相似系数高于给定阈值的汽车),或者,当我们找到相似系数高于另一个给定阈值的汽车时,我们可以停止搜索,而不是找到具有最高相似系数的汽车,等

答案 5 :(得分:0)

根据我对您的similarity()方法的理解,我认为最好保持hashCode()函数大致相同,但不要使用color.hashCode(),而是创建一个帮助方法,将生成一个类似的颜色&#34;,并使用该hashCode:

public int getSimilarColor(String color) {
    if(color == "blue" || color == "light blue" || color == "dark blue" /* add more blue colors*/) {
        return "blue";
    } else if(color == "red" || color == "light red" || color == "dark red" /* add more red colors*/) {
        return "red";
    }
    /*
    else if(yellow...)
    else if(etc...)
    */
    else {
        return color;
    }
}

然后在你的hashCode方法中使用它:

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

此辅助方法在similarity()中也可能有用。如果您不熟悉将类似颜色硬编码到您的方法中,您可以使用其他方法生成它们,例如模式匹配。