hashCode()应该返回一个对象的唯一ID吗?

3
在我的 Kotlin/Java 项目中,我编写了一些继承自抽象类 BaseItem 的模型类:
/**
 * All data model classes should implement this
 */
abstract class BaseItem {

    /**
     * A unique integer identifier used to represent the data item in the database.
     */
    abstract val id: Int

    override fun equals(other: Any?) = (other is BaseItem) && id == other.id

}

这些模型类将用于表示来自数据库的数据。 在数据库中,有一个包含唯一整数标识符的ID列。
因此,当我使用这些模型类时,可以保证每个对象的“id”属性是唯一的。
阅读Java规范中关于“hashCode()”的内容后:
无论何时在应用程序的执行过程中多次调用同一对象的“hashCode”方法,只要在equals比较上使用的信息未修改,该方法必须始终返回相同的整数。 这个整数不需要在同一应用程序的另一个执行中保持一致。
如果两个对象根据equals(Object)方法相等,则在每个对象上调用hashCode方法必须产生相同的整数结果。
不要求如果两个对象根据equals(Object)方法不相等,则在每个对象上调用hashCode方法必须产生不同的整数结果。 然而,程序员应该知道为不相等的对象产生不同的整数结果可能会提高哈希表的性能。
我的问题是:
hashCode()中返回这个唯一标识符是一个好的实践吗?
注意:我知道在Kotlin中,我们可以使用data classes,以便编译器自动派生预定义成员,如equals()hashCode()toString()等,但abstract类不能成为data类。(然而,我可以将BaseItem的子类设为data类 - 我不确定这是否是这种情况下更好的选择)。

1
鉴于 equals 的实现,你几乎别无选择。 - Oliver Charlesworth
5
我更关心你的equals()方法:当比较ID为1的用户和ID为1的订单时,它不应该返回true。订单和用户永远不应该相等:尽管它们扩展了BaseItem,但它们根本不是同一件事。 - JB Nizet
@JBNizet 这是一个非常有价值的观点 - 我没有想到。是否适合拥有一个抽象的 getType() 函数,根据 BaseItem 的种类返回一个整数?或者您认为使用 Kotlin 的 数据类 提供的标准实现的 equals()hashCode() 更好呢? - Farbod Salamat-Zadeh
不,我会让子类根据需要实现equals(和hashCode)。是否使用ID取决于上下文,持久性框架的约束(如果有),等等,这是一个好主意还是不好的主意。例如,ID为空并且仅在由数据库自动生成后填充时很常见。在这种情况下,您无法真正使用ID进行equals()和hashCode()。有时,没有equals()和hashCode()(即使用java.lang.Object中的内容)是正确的做法。 - JB Nizet
在对象模型中携带数据库代理整数键是一个不好的想法。 - Lew Bloch
1个回答

2

由于您的抽象 BaseClass 用于数据类 (a.k.a. 值类),因此应将 equalshashCode 定义为 abstract 并强制实现具体类。例如:

abstract class BaseItem {
    abstract val id: Int
    abstract override fun equals(other: Any?): Boolean
    abstract override fun hashCode(): Int
}

data class Person(override val id: Int, val name: String) : BaseItem()

data class Product(
        override val id: Int,
        val name: String,
        val cost: BigDecimal
) : BaseItem()

在基类中实现这些函数并且不在具体的子类中重写它们可能会导致违反 equalshashCode 协定的情况。
以下是一个对称性违规的例子,如果你不强制子类实现 equals/hashCode:
abstract class BaseItem {
    abstract val id: Int
    override fun equals(other: Any?) = (other is BaseItem) && id == other.id
    override fun hashCode() = id
}

class Person(override val id: Int, val name: String) : BaseItem() {
    override fun equals(other: Any?): Boolean {
        return (other is Person) && id == other.id && name == other.name
    }

    override fun hashCode() = 31 * (31 + id.hashCode()) + name.hashCode()
}

class Product(
        override val id: Int,
        val name: String,
        val cost: BigDecimal
) : BaseItem()

fun main(args: Array<String>) {
    val baseItem1: BaseItem = Person(1, "Oliver")
    val baseItem2: BaseItem = Product(1, "grease", BigDecimal.TEN)
    println(baseItem1 == baseItem2) // false
    println(baseItem2 == baseItem1) // true
}

如果 equals/hashCode 根据它们的契约实现,那么两个相等性检查将始终返回相同的结果(在这种情况下应该是 false,因为 Product 还应该重写这些函数并检查 other 是否也是一个 Product 并检查每个相关属性等)。请参阅 Joshua Bloch 的《Effective Java, Second Edition》中的“条款 8:重写 equals 时遵守通用契约”和“条款 9:重写 equals 时总是重写 hashCode”以获取有关这些契约及不同层次值类的不同方法所带来的问题的更多详细信息。

为什么会导致违规? - Oliver Charlesworth
@OliverCharlesworth 我已更新我的答案,包括一个违规示例。 - mfulton26
@OliverCharlesworth 好观点。这仅适用于数据/值类。我已更新我的答案以澄清这一点。《Effective Java》还指出,从抽象类继承equals / hashCode 有一些有效的使用情况,例如AbstractListAbstractSet等。 - mfulton26
@mfulton26 谢谢您,这是一份详细的答案!我对 hashCode()equals() 有了更深入的了解,这些例子也很有帮助。我之前确实读过《Effective Java》,但可能忘记了很多内容,所以我可能会再读一遍。 ;) - Farbod Salamat-Zadeh
如果您在抽象类中正确实现了 hashCodeequals,就不会出现这样的对称性违规。例如,如果您在 Kotlin 中有一个 getClass() 方法(就像 Java 中存在的那样),您可以在 BaseItemequals 方法中检查其他对象是否与此实例属于相同的类,然后再检查 ID。没有任何违规。 - fps
显示剩余2条评论

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