真的值得为实体类实现 toString() 吗?

2022-09-03 18:12:41

始终建议重写(实现)类的方法。toString()

  • Java API文档本身说“建议所有子类都覆盖此方法。
  • Bloch,在 Effective Java 中有一个项目“Always override toString”。只有傻瓜才会与布洛赫相矛盾,对吧?

然而,我开始怀疑这个建议:它真的值得为实体类实现吗toString()


我会试着列出我的推理。

  1. 实体对象具有唯一的标识;它永远不会与另一个对象相同,即使这两个实体具有等效的属性值。也就是说,(对于非空 x),以下不变量适用于实体类(根据定义):

    x.equals(y) == (x == y)

  2. 该方法返回一个字符串,该字符串“以文本方式表示”其对象(用Java API的单词表示)。toString()

  3. 个好的表示捕获对象的本质,所以如果两个表示不同,它们是不同(非等效)对象的表示,相反,如果两个表示是等价的,它们是等效对象的表示。这为一个好的表示(对于非空 xy)提出了以下不变性:

    x.toString().equals(y.toString()) == x.equals(y)

  4. 因此,对于实体,我们期望的是,每个实体对象都应该有一个唯一的文本表示,该表示形式返回。某些实体类将具有唯一的名称或数字 ID 字段,因此它们的方法可以返回包含该名称或数字 ID 的表示形式。但通常,该方法无法访问此类字段。x.toString().equals(y.toString()) == (x == y)toString()toString()toString()

  5. 如果没有实体的唯一字段,最好的办法是包含一个对于不同对象不太可能相同的字段。但这正是System.identityHashCode()的要求,这就是所提供的。toString()Object.toString()

  6. 因此,对于没有数据成员的实体对象来说,这是可以的,但是对于大多数类,您可能希望将它们包含在文本表示形式中,对吗?实际上,您希望包含所有这些成员:如果类型具有(非空)数据成员 x,则希望包含在表示中。Object.toString()x.toString()

  7. 但是,这给持有对其他实体的引用的数据成员带来了问题:即,它们是关联。如果一个对象有一个数据成员,那么幼稚的实现将产生该人的家谱的片段,而不是其本身的片段。如果存在双向关联,则幼稚的实现将递归,直到您获得堆栈溢出,因此可以跳过持有关联的数据成员?PersonPerson fatherPerson

  8. 但是,具有和数据成员的值类型呢?这些关联应由 报告。使所有方法都起作用的最简单方法是仅报告 的标识字段(或)。MarriagePerson husbandPerson wifeMarriage.toString()toString()Person.toString()Person.nameSystem.identityhashCode(this)Person

  9. 因此,似乎提供的 实现对于实体类来说实际上并不太糟糕。在这种情况下,为什么要覆盖它?toString()


若要使其具体化,请考虑以下代码:

public final class Person {

   public void marry(Person spouse)
   {
      if (spouse == this) {
         throw new IlegalArgumentException(this + " may not marry self");
      }
      // more...
   }

   // more...
}

在调试抛出的时,覆盖 的有用程度如何?toString()IlegalArgumentExceptionPerson.marry()


答案 1

因此,似乎提供的 toString() 实现对于实体类来说实际上并不太糟糕。在这种情况下,为什么要覆盖它?

是什么让你认为目标只是拥有一个独特的字符串?这不是它的目的。它的目的是为您提供有关实例的上下文,而仅通过类名和哈希码不会为您提供上下文。toString()

编辑

只是想说,我绝不认为你需要覆盖每个对象。无价值对象(如侦听器或策略的具体实现)不需要重写,因为每个实例都无法与其他任何实例区分开来,这意味着类名就足够了。toString()toString()


答案 2

第3点是这个论点的薄弱环节,事实上我强烈反对它。您的不变量是(重新排序)

x.equals(y) == x.toString().equals(y.toString()); 

我会说,更确切地说:

x.equals(y) → x.toString().equals(y.toString()); 

也就是说,逻辑含义。如果 x 和 y 相等,则它们的 toString()应该相等,但等于 toString() 并不一定意味着对象相等(想想 : 关系;相等的对象必须具有相同的哈希代码,但相同的哈希代码不能被理解为对象相等)。equals()hashCode()

从根本上说,在程序化意义上没有任何“意义”,我认为你正试图用一个“意义”来灌输它。 作为日志记录等工具最有用;你问被覆盖的有用性有多大:toString()toString()toString()

throw new IlegalArgumentException(this + " may not marry self");

我会说它非常有用。假设您注意到日志中有很多错误,并查看:

IllegalArgumentException: com.foo.Person@1234ABCD cannot marry self
IllegalArgumentException: com.foo.Person@2345BCDE cannot marry self
IllegalArgumentException: com.foo.Person@3456CDEF cannot marry self
IllegalArgumentException: com.foo.Person@4567DEFA cannot marry self

你是做什么工作的?你完全不知道发生了什么。如果您看到:

IllegalArgumentException: Person["Fred Smith", id=678] cannot marry self
IllegalArgumentException: Person["Mary Smith", id=679] cannot marry self
IllegalArgumentException: Person["Mustafa Smith", id=680] cannot marry self
IllegalArgumentException: Person["Emily-Anne Smith", id=681] cannot marry self

然后你实际上有机会弄清楚发生了什么('嘿,有人试图让史密斯家族自己结婚'),这实际上可能有助于调试等。Java 对象 ID 根本没有为您提供任何信息