Preferred Equals() 方法的实现

3
这是一个关于如何在需要查找一个成员变量具有特定值的对象列表中实现equals方法的问题。这里是一份已实现equals方法的对象:
```html

I have an object where I've implemented equals:

```
class User {

    private String id;

    public User(id) {
        this.id = id;
    }

    public boolean equals(Object obj) {
        if (!(obj instanceof User)) {
            return false;
        }
        return ((User)obj).id.equals(this.id);
    }
}

现在,如果我想在列表中查找某些内容,我会像这样做:
public function userExists(String id) {
        List<Users> users = getAllUsers(); 
        return users.contains(new User(id));
}

但也许这个实现方案会更好?
class User {

    private String id;

    public boolean equals(Object obj) {
        if (!(obj instanceof User)) {
            return false;
        }
        if (obj instanceof String) {
            return ((String)obj).equals(this.id);
        }
        return ((User)obj).id.equals(this.id);
    }
}

使用这个替代方案:

public function userExists(String id) {
    List<Users> users = getAllUsers(); 
    return users.contains(id);
}

2
实际上,我遇到了一堆代码维护问题,这些代码都使用了“equals”与字符串相等的惯用法。像所有的错误一样,一开始并不太糟糕,但随着Collections的更加频繁地使用,越来越多的解决方法被打包进去,我认为第二种技术是一个__bug__而不是一个好处。 - Edwin Buck
5个回答

7

采用第二种方法是危险的,因为它破坏了等式的对称性。

Java期望实现equals()方法具有自反性、对称性和传递性。第二种实现破坏了对称性:如果您将User与表示其ID的String进行比较,则会得到true,但如果您将字符串与用户进行比较,则会得到false


我记得在Sierra/Bates的书中读到过对称性,但当时可能只是匆匆浏览了一下。现在我更清楚了。但是,为了查找给定ID的用户列表中的用户,创建一个虚拟用户对象是否合适? - udeleng
3
不,创建虚拟用户以在列表中查找“User”不是正确的方法。 要么使用“迭代器”搜索列表并检查每个元素上的字段,要么使用按id索引的“Map”存储用户(如下面的示例所示)。 “user.equals(Object)”的适当实现需要在搜索列表中寻找之前已经拥有寻找的用户。 - Edwin Buck

6

不要为那些数学上不相等的事物重写equals方法。

你可能认为这样做是一个好主意

User bob = new User("Bob");
if (bob.equals("Bob")) {
  ...
}

但这种情况很少见。你想让所有的等于代码混淆,当 Strings 等于 Users 时?

如果你需要一个查找方法,请自己编写它。

class User {

    private String id;

    public boolean equals(Object obj) {
        if (obj instanceof User) {
            User other = (User)obj;
            if (id.equals(other.id)) {
              return true;
            }
        }
        return false;
    }

    public String getId() {
        return id;
    }

}

然后在其他地方编写维护“快速查找”表的代码。

Map<String, User> idTable = new HashMap<String, User>();
User bob = new User("Bob");
idTable.put(bob.getId(), bob);

public User findUser(String id) {
  return idTable.get(id);
}

请注意,这不会影响equals实现,因此您现在可以安全地拥有用户集合用户列表等,而无需担心字符串会导致错误。现在,如果您找不到一个好的地方来维护按其id索引的用户 Map,则始终可以使用较慢的 Iterator 解决方案。
List<User> users = new List<User>();
users.add(new User("Bob"));
users.add(new User("Steve"));
users.ass(new User("Ann"));

public User findUser(String id) {
  Iterator<User> index = users.iterator();
  while (index.hasNext()) {
    User user = index.next();
    if (id.equals(user.getId())) {
      return user;
    }
  }
  return null;
}

我基本上同意你的答案,但是OP是在覆盖(overriding)而不是重载(overloading)。 - Kevin Welker

2

首先,你的第二个实现和第一个实现在功能上是等效的,因为String的实例不是User的实例,在检查String之前,第一个return语句将使其提前退出。

我的意思是,

public boolean equals(Object obj) {
    if (!(obj instanceof User)) { // This will execute if obj is a String
        return false;
    }
    if (obj instanceof String) {
        // Never executes, because if obj is a String, we already
        // returned false above
        return ((String)obj).equals(this.id);
    }
    return ((User)obj).id.equals(this.id);
}

因此,在接下来的回答中,我将假设所指的是...
public boolean equals(Object obj) {
    if ( obj == null ) return false; // Add a null check for good measure.
    if (!(obj instanceof User)) {
        if (obj instanceof String) {
            // Now we're checking for a String only if it isn't a User.
            return ((String)obj).equals(this.id);
        }
        return false;
    }
    return ((User)obj).id.equals(this.id);
}

现在我们来到了实际的问题。
实现一个返回 true 的 equals 方法用于 User 到 String 的比较是一种不好的做法,因为 equals 应该是对称的(即 a.equals(b) 当且仅当 b.equals(a))。由于一个字符串永远不可能等于一个用户,因此一个用户不应该等于一个字符串。

谢谢,我看到了我的短路错误。在发布时,我只是在这里输入了代码。我现在明白对称原理了。请看看我回应@dasblinkenlight的问题。 - udeleng
@trutheality,老实说,我完全没有理解你的观点。我很抱歉,我的假设是他的第二个等号实现方式半正确的,而不是一个概念性错误上面的一个漏洞竟然能够拯救一天。我想我最好放慢阅读速度,或者喝杯咖啡。 - Edwin Buck

1

如果你重写了Object.equals()方法,不要忘记重写Object.hashCode()方法。

相等的对象必须具有相等的哈希码,如果不遵守这个规则,可能会在使用集合时遇到问题。


0

第二个选择非常糟糕。这意味着您允许用户将除User之外的其他内容传递到User.equals()中(即,当您不是真正尝试将String与User进行比较时,实际上是Object.equals())。因此,您本质上违反了equals应该执行的契约。

此外,您的两个答案都没有处理空值检查。


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