如何为一个HashSet覆盖equals()、hashcode()和compareTo()方法

7

我试图重写HashSet中提到的方法:

Set<MyObject> myObjectSet = new HashSet<MyObject>();

MyObject:

public class MyObject implements Serializable {

  private static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  Long id;
  String name;
  int number;
  Map<String,String> myMap;

  public MyObject(String name, int number, Map<String,String> myMap) {
    this.name = name;
    this.number = number;
    this.myMap = myMap;
  }

  [...]
}

我该如何重写hashcode()、equals()和compareTo()方法呢?


目前我的代码如下:

public int hashCode () {
  return id.hashCode();
}

// override the equals method.
public boolean equals(MyObject s) {
  return id.equals(s.id);
}

// override compareTo
public int compareTo(MyObject s) {
  return id.compareTo(s.id);
}    

我看到说仅仅通过id比较是不够的,因为这个对象是数据库中的持久化实体(请参见这里)。

这种类型的所有对象中,名称和编号并不是唯一的。

那么我应该如何重写它呢?
我需要同时比较其中的hashMap吗?

我很困惑。这个对象唯一的特点就是在其生命周期后期填充的map myMap。

我该如何检查它们的相等性呢?

基于所有回复,我已将方法更改为以下内容:

 @Override
    public boolean equals(final Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    final MyComplexObj myComplexObj = (MyComplexObj) o;

    return myMap != null ? myMap.equals(myComplexObj.myMap) : myComplexObj.myMap == null;
    }

    @Override
    public int hashCode() {
    return myMap != null ? myMap.hashCode() : 0;
    }



    public int compareTo(MyComplexObj o) {
    return myMap.compareTo(o.getMyMap()));
    }

在compareTo方法中出现错误,"此方法对于类型Map未定义"


正如您所提到的,该对象唯一独特的是映射中的第一个条目,我会考虑添加一个额外的字段,并将该条目复制到其中。这样它就成为了真实世界的候选键,并可用于实现equals和hashcode方法。您能否详细说明此对象的生命周期,以及何时实际调用这些方法? - Hassan
2
我们没有足够的信息来回答你的问题。那个ID从哪里来?我在MyObject中看不到ID。你的实体在数据库中的主键是什么? - Guillaume F.
3
目前这个问题的表述非常不清晰。你用于覆盖的代码示例是错误的(例如,它们对equals()方法使用了错误的参数类型)。然后你还在使用一个未在MyObject类中定义的id字段?!与其设置赏金,你应该首先专注于改善问题本身。 - GhostCat
在类中添加了id字段。@GhostCat。当然这是错误的,我正在寻找正确的做法。 - user_mda
如果地图是唯一的东西,为什么不直接使用地图的 hashcode/equals/compareTo 方法呢?您也不希望在将对象添加到哈希集之后更改这些值。 - matt
显示剩余2条评论
4个回答

1
这是IntelliJ的默认选项。
import java.util.Map;

public class MyObject {
  String name;
  int number;
  Map<String,String> myMap;

  @Override
  public boolean equals(final Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    final MyObject myObject = (MyObject) o;

    if (number != myObject.number) return false;
    if (name != null ? !name.equals(myObject.name) : myObject.name != null) return false;
    return myMap != null ? myMap.equals(myObject.myMap) : myObject.myMap == null;
  }

  @Override
  public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + number;
    result = 31 * result + (myMap != null ? myMap.hashCode() : 0);
    return result;
  }
}

但是,既然你说:

该对象唯一的独特之处在于稍后在生命周期中填充的地图myMap。

我会保留myMap并跳过name和number(但这引出了一个问题,为什么要在集合的所有元素中包含冗余数据-名称和数字?)

那么它就变成了:

import java.util.Map;

public class MyObject {
  String name;
  int number;
  Map<String,String> myMap;

  @Override
  public boolean equals(final Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    final MyObject myObject = (MyObject) o;

    return myMap != null ? myMap.equals(myObject.myMap) : myObject.myMap == null;
  }

  @Override
  public int hashCode() {
    return myMap != null ? myMap.hashCode() : 0;
  }
}

记住,equals和hashcode方法也有其他的实现方式。例如,以下是IntelliJ在代码生成时提供的选项。

enter image description here

回答关于CompareTo的更多问题

与Equals和Hashcode不同,CompareTo与任何其他行为之间不存在契约。在你想要使用它进行排序等操作之前,你实际上不需要对CompareTo做任何事情。要了解更多关于CompareTo的信息,请阅读为什么Java类应该实现comparable?


谢谢,我会尝试的。那么compareTo方法呢? - user_mda
对象的唯一性在于其中的映射,而不仅仅是第一个条目。我已经进行了更改。 - user_mda
1
不用担心compareTo。hashCode和compareTo之间不存在任何契约。你很安全。只需要做equals和hashCode就可以了。 - so-random-dude
是的,正在验证更改。 - user_mda
@user_mda,你找到它了吗? - so-random-dude
显示剩余4条评论

1

compareTo() 方法与排序有关,但与 HashSetHashMap 无关。

对于基于哈希的集合,equals()hashCode() 的正常工作非常重要。请阅读 Object 的 Javadoc 中的规范。

也许实现这些方法的最终建议在 Joshua Bloch 的 Effective Java 中。我建议阅读相关章节——它很容易通过谷歌搜索获得。在这里尝试解释它们并没有意义。


有一件可能逃脱你注意的事情,就是你的字段myMap具有自己的有效equals()hashCode(),因此你不需要对其进行任何特殊处理。如果你可以保证没有字段为空,一个合理的hashCode()将会是(遵循Bloch的系统):

public int hashCode() {
     int result = 44; // arbitrarily chosen
     result = 31 * result + (int) (id ^ (id >>> 32));
     result = 31 * result + name.hashCode();
     result = 31 * result + number;
     result = 31 * result + myMap.hashCode();
     return result;
}

如果其中任何一个可能为空,则需要更多的代码。


几乎所有的IDE都会自动生成equals()hashcode()方法,使用类中的所有字段。它们将使用类似于Bloch建议的内容。在用户界面中寻找。你会找到它的。
另一个选择是使用Apache ReflectionUtils,它允许你简单地使用:
@Override
public int hashCode() {
    return HashCodeBuilder.reflectionHashCode(this);
}

@Override
public boolean equals(final Object obj) {
    return EqualsBuilder.reflectionEquals(this, obj);
}

该过程在运行时确定要使用哪些字段,并应用Bloch的方法。


1
这里的基本问题是“如何确定两个对象是否相等?”对于简单的对象,这是一个简单的问题。然而,即使是稍微复杂一些的对象,这个问题也变得越来越困难。
正如原问题所述:
“关于对象的唯一独特之处是稍后在生命周期中填充的地图myMap。”
给定两个类型为MyObject的实例,必须比较成员变量myMap是否相等。此映射的类型为Map。立即会有几个问题:
  • 键和值如何定义相等性?
    • (一个键=值对是否需要作为一个整体进行比较?)
    • (还是只需要将值彼此进行比较?)
  • 映射中键的顺序如何影响相等性?
    • (列表中的键是否应该排序,使得A-B-C等同于B-C-A?)
    • (或者1-2-3与3-2-1有不同的含义吗?)
  • 大小写对值的相等性有影响吗?
  • 这些对象是否会被存储在某种Java HashSetJava TreeSet中?
    • (您是否需要在同一集合中多次存储相同的对象?)
    • (还是只应该存储具有相等哈希码的对象一次?)
  • 这些对象是否需要作为列表或Java Collection的一部分进行排序?
  • 比较函数应如何排列列表中不相等的对象?
    • (键顺序如何决定对象在列表中先后顺序?)
    • (如果有几个值不同,值应如何确定顺序?)
这些问题的答案因应用程序而异。为了让内容对一般观众适用,我们做出以下假设:
  • 为保持确定性比较,键将被排序
  • 值将被视为区分大小写
  • 键和值是不可分割的,将作为一个单元进行比较
  • 地图将被展开成一个单一的字符串,以便轻松比较结果
使用equals()hashCode()compareTo()的美妙之处在于,一旦hashCode()被正确实现,其他函数可以基于hashCode()定义。
考虑到所有这些,我们有以下实现:
@Override
public boolean equals(final Object o)
{
    if (o instanceof MyObject)
    {
        return (0 == this.compareTo(((MyObject) o)));
    }
    return false;
}

@Override
public int hashCode()
{
    return getKeyValuePairs(this.myMap).hashCode();
}

// Return a negative integer, zero, or a positive integer
// if this object is less than, equal to, or greater than the other object
public int compareTo(final MyObject o)
{
    return this.hashCode() - o.hashCode();
}

// The Map is flattened into a single String for comparison
private static String getKeyValuePairs(final Map<String, String> m)
{
    final StringBuilder kvPairs = new StringBuilder();

    final String kvSeparator = "=";
    final String liSeparator = "^";

    if (null != m)
    {
        final List<String> keys = new ArrayList<>(m.keySet());
        Collections.sort(keys);

        for (final String key : keys)
        {
            final String value = m.get(key);
            kvPairs.append(liSeparator);
            kvPairs.append(key);
            kvPairs.append(kvSeparator);
            kvPairs.append(null == value ? "" : value);
        }
    }

    return 0 == kvPairs.length() ? "" : kvPairs.substring(liSeparator.length());
}

所有关键的工作都在hashCode()内完成。对于排序,compareTo()函数只需要返回一个负数/零/正数--一个简单的hashCode()差异。而equals()函数只需要返回true/false--一个简单的检查compareTo()是否等于零。

如果需要进一步阅读,可以参考刘易斯·卡罗尔关于逻辑基础的著名对话,其中涉及到平等的基本问题:

https://en.wikipedia.org/wiki/What_the_Tortoise_Said_to_Achilles

关于甚至是简单的语法结构,第6章“猪和辣椒”开头有两个“相等”的句子,来自《爱丽丝梦游仙境》:

鱼脚男仆首先从胳膊下面取出一封大信,递给另一个人,用庄重的口吻说:“送给公爵夫人。女王邀请参加槌球比赛。”青蛙脚男仆以同样庄重的口吻重复道:“女王的邀请函。邀请公爵夫人参加槌球比赛。”然后他们俩低头鞠躬,卷发纠缠在了一起。


0
如果你想让myMap实现可比较性,并且添加其他任何方法,可以创建一个装饰器来实现可比较接口,并将所有其他方法委托给封闭的myMap实例。
public class ComparableMap implements Map<String, String>, Comparable<Map<String, String>> {
    private final Map<String, String> map;

    public ComparableMap(Map<String, String> map) {
        this.map = map;
    }

    @Override
    public int compareTo(Map<String, String> o) {
        int result = 0;
        //your implementation based on values on map on you consider one map bigger, less or as same as another
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        return map.equals(obj);
    }

    @Override
    public int hashCode() {
        return map.hashCode();
    }

    // map implementation methods

    @Override
    public int size() {
        return map.size();
    }

    @Override
    public boolean isEmpty() {
        return map.isEmpty();
    }

    @Override
    public boolean containsKey(Object key) {
        return map.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        return map.containsValue(value);
    }

    @Override
    public String get(Object key) {
        return map.get(key);
    }

    @Override
    public String put(String key, String value) {
        return map.put(key, value);
    }

    @Override
    public String remove(Object key) {
        return map.remove(key);
    }

    @Override
    public void putAll(Map<? extends String, ? extends String> m) {
        map.putAll(m);
    }

    @Override
    public void clear() {
        map.clear();
    }

    @Override
    public Set<String> keySet() {
        return map.keySet();
    }

    @Override
    public Collection<String> values() {
        return map.values();
    }

    @Override
    public Set<Entry<String, String>> entrySet() {
        return map.entrySet();
    }

}

您可以在任何使用 myMap 的地方使用此映射表。

  public class MyObject implements Serializable {

        private static final long serialVersionUID = 1L;

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        Long id;
        String name;
        int number;
        ComparableMap myMap;

        public MyObject(String name, int number, Map<String, String> myMap) {
            this.name = name;
            this.number = number;
            this.myMap = new ComparablemyMap(myMap);
        }


        @Override
        public boolean equals(final Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            final MyComplexObj myComplexObj = (MyComplexObj) o;

            return myMap != null ? myMap.equals(myComplexObj.myMap) : myComplexObj.myMap == null;
        }

        @Override
        public int hashCode() {
            return myMap != null ? myMap.hashCode() : 0;
        }


        public int compareTo(MyComplexObj o) {
            return myMap.compareTo(o.getMyMap())); //now it works
        }

    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接