是否真的值得为实体类实现toString()
始终建议覆盖(实现)类的toString()
方法。
- Java API文档本身说:“建议所有子类都覆盖此方法。”
- 有效Java中的Bloch具有“始终覆盖toString”项。 只有傻瓜才与布洛赫相矛盾,对吧?
然而,我怀疑这个建议:是否真的值得为实体类实现toString()
?
我会尝试列出我的推理。
-
实体对象具有唯一标识; 即使两个entites具有相同的属性值,它也永远不会与另一个对象相同。 也就是说,(对于非null x ),以下不变量适用于实体类(根据定义):
x.equals(y) == (x == y)
-
toString()
方法返回一个“文本表示”其对象的字符串(用Java API的话说)。 -
良好的表示捕获对象的基本要素,因此如果两个表示不同,则表示不同(非等效)对象的表示,相反,如果两个表示是等价的,则它们是等效对象的表示。 这表明以下对于良好表示的不变性(对于非空x , y ):
x.toString().equals(y.toString()) == x.equals(y)
-
因此,对于实体,我们期望
x.toString().equals(y.toString()) == (x == y)
,即每个实体对象应该具有唯一的文本表示,toString()
返回。 某些实体类将具有唯一的名称或数字ID字段,因此它们的toString()
方法可以返回包含该名称或数字ID的表示。 但通常,toString()
方法无法访问此类字段。 -
如果没有实体的唯一字段,
toString()
可以做的最好的事情就是包含一个对于不同对象不太可能相同的字段。 但这正是System.identityHashCode()
的要求,这是Object.toString()
提供的。 -
因此对于没有数据成员的实体对象,
Object.toString()
是可以的,但对于大多数类,您希望将它们包含在文本表示中,对吧? 实际上,您希望包含所有这些:如果类型具有(非null)数据成员x ,您可能希望在表示中包含x.toString()
。 -
但这会对持有对其他实体的引用的数据成员产生问题:即哪些是关联 。 如果
Person
对象具有Person father
数据成员,则天真实现将生成该人的家谱的片段,而不是Person
本身。 如果存在双向关联,那么天真的实现将会递归,直到您获得堆栈溢出所以可能会跳过持有关联的数据成员? -
但是有关
Person husband
和Person wife
数据成员的价值型Marriage
怎么样? 这些关联应该由Marriage.toString()
报告。 使所有toString()
方法起作用的最简单方法是使Person.toString()
仅报告Person
的标识字段(Person.name
或System.identityhashCode(this)
)。 -
所以似乎提供的
toString()
实现对于实体类来说实际上并不算太糟糕。 在那种情况下,为什么要覆盖呢?
为了使其具体,请考虑以下代码:
public final class Person { public void marry(Person spouse) { if (spouse == this) { throw new IlegalArgumentException(this + " may not marry self"); } // more... } // more... }
调试Person.marry()
抛出的Person.marry()
时, toString()
的重写是多么有用?
第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 根本不会给你任何信息 。
所以似乎提供的toString()实现对于实体类来说实际上并不算太糟糕。 在那种情况下,为什么要覆盖呢?
是什么让你认为toString()
的目标只是拥有一个独特的字符串? 这不是它的目的。 它的目的是为您提供有关实例的上下文,并且只是类名和哈希码不会为您提供上下文。
编辑
只想说我绝不认为你需要在每个对象上覆盖toString()
。 无值对象(如侦听器或策略的具体实现)不需要覆盖toString()
因为每个实例都与其他实例无法区分,这意味着类名就足够了。
在实体类中使用toString()
方法对于调试目的非常有用。 从实用的角度来看,使用IDE模板或Project Lombok @ToString
注释之类的东西可以简化这一过程并使其易于快速实现。
我总是将toString()用于我自己的目的,而不是因为某些技术要求。 当我有一个Person类时,toString方法返回该人的名字。 不多也不少。 它不是唯一的,但出于调试目的,它足以看出人的意思。 特别是在Web开发中,当我只需要在JSP中编写对象名来获取人的名字时,这非常方便,所以我知道我有正确的对象。
如果对象有一些唯一的数据(比如数据库ID),那么这是toString()的完美候选者,所以它可以返回#294: John Doe
。 但唯一性不是必需的。
真的……即使布洛赫先生这么说……我认为有任何实施toString()的规则是没有意义的。 它对hashCode()和equals()有意义,但对toString()没有意义。
是的,这是值得的。 ToString有助于为对象的状态提供有形的视觉输出。 IMO,它在实体中尤其重要,因为ORM或其他第三方库经常将对象打印为其日志记录策略的一部分。
logger.debug("Entity: {}", entity);
显然会隐式调用toString()。
它帮助我一次又一次地在视觉上看到实体的状态,以确定它在事务性方面是暂时的还是持久的,只是一般的调试。
你愿意看到这个:
DEBUG | pattern: test.entity.MyEntity@12345f
或这个:
DEBUG | pattern: MyEntity [id = 1234.0, foo=bar, bar=baz]
简而言之,你不能覆盖toString的唯一原因是懒惰。 在最近的版本中,Eclipse甚至还有一个toString生成器!