Действительно ли стоит реализовывать toString () для классов сущностей?

Постоянно рекомендуется переопределить (реализовать) метод toString() класса.

  • документация по Java API сам говорит: «Рекомендуется, чтобы все подклассы переопределяли этот метод.».
  • Блох в Эффективной Java имеет пункт «Всегда заменять toString». А Блоху противоречит только дурак, не так ли?

Однако я сомневаюсь в этом совете: действительно ли стоит реализовывать toString() для классов сущностей?


Постараюсь изложить свои рассуждения.

  1. Объект entity имеет уникальный идентификатор; он никогда не совпадает с другим объектом, даже если два объекта имеют эквивалентные значения атрибутов. То есть (для ненулевого x) следующий инвариант применяется для класса сущности (по определению):

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

  2. Метод toString() возвращает строку, которая «текстуально представляет» свой объект (говоря словами Java API).

  3. хорошее представление захватывает сущность объекта, поэтому, если два представления различны, они являются представлениями разных (неэквивалентных) объектов, и наоборот, если два представления эквивалентны, они являются представлениями эквивалентных объектов. Это предполагает следующий инвариант для хорошего представления (для ненулевых значений x, y):

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

  4. Таким образом, для сущностей мы ожидаем x.toString().equals(y.toString()) == (x == y), то есть каждый объект-сущность должен иметь уникальное текстовое представление, которое возвращает toString(). Некоторые классы сущностей будут иметь уникальное имя или числовое поле идентификатора, поэтому их метод toString() может возвращать представление, которое включает это имя или числовой идентификатор. Но вообще метод toString() не имеет доступа к такому полю.

  5. Без уникального поля для объекта лучшее, что может сделать toString(), - это включить поле, которое вряд ли будет одинаковым для разных объектов. Но это в точности требование для _ 11_, что и предоставляет Object.toString().

  6. Итак, Object.toString() подходит для объекта сущности, не имеющего элементов данных, но для большинства классов вы хотели бы включить их в текстовое представление, верно? Фактически, вы хотите включить все из них: если тип имеет (не нулевой) член данных x, вы захотите включить x.toString() в представление.

  7. Но это создает проблему для элементов данных, содержащих ссылки на другие сущности, то есть на ассоциации. Если объект Person имеет член данных Person father, наивная реализация создаст фрагмент генеалогического древа этого человека, а не самого Person. Если есть двусторонние связи, наивная реализация будет рекурсивной, пока вы не получите переполнение стека Так что, возможно, пропустите элементы данных, которые содержат ассоциации?

  8. Но как насчет типа значения Marriage, имеющего элементы данных Person husband и Person wife? Об этих ассоциациях следует сообщить Marriage.toString(). Самый простой способ заставить все toString() методы работать - это Person.toString() сообщать только поля идентификации (Person.name или System.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() при отладке IlegalArgumentException, выданного Person.marry()?


person Raedwald    schedule 03.02.2011    source источник
comment
Я не согласен с вашим пунктом №3. Вы не всегда хотите, чтобы все свойства отображались в строковом представлении.   -  person finnw    schedule 03.02.2011
comment
Формальное определение, которое вы используете для слова entity, будет полезно для понимания вашего аргумента :)   -  person Affe    schedule 03.02.2011
comment
Очень понравилось ваше заявление о Блохе и дураках :) Не спорю, что эти ребята умные и т. Д., Но просто вспомните EntityBeans в EJB2.x. Это была катастрофа :) В общем, я бы избегал использования хороших практик, если для этого нет веской причины.   -  person Stas    schedule 03.02.2011
comment
›› Bloch, в Effective Java есть пункт Всегда переопределять toString. А Блоху противоречит только дурак, не так ли?   -  person bestsss    schedule 03.02.2011
comment
Как вы пришли к № 3? Я быстро проверил соответствующий раздел в Эффективной Java, но я не смог найти это утверждение - и был бы удивлен, если бы у меня был ...   -  person Puce    schedule 03.02.2011
comment
@Raedwald: Ваш пример сообщения об ошибке не имеет для меня смысла. Более полезным может быть "Illegal request for " + this + " to marry " + spouse + ": A Person cannot marry itself". Но в любом случае это не лучший пример того, когда toString() был бы полезен, поскольку marry() имеет полный доступ к внутренним компонентам Person для передачи более осознанного сообщения.   -  person Mark Peters    schedule 03.02.2011
comment
Мне сложно понять вашу логику. Тот факт, что вам не нужно предоставлять исключительно полезную функцию в своем классе, не является причиной избегать этого.   -  person Highland Mark    schedule 04.02.2011
comment
@Highland MarK: Просто потому, что вам не нужно предоставлять исключительно полезную функцию: но я бы задал вопрос, было ли переопределение исключительно полезным для классов сущностей.   -  person Raedwald    schedule 04.02.2011
comment
Это может быть / исключительно / полезно, но с использованием IDEA (и, возможно, других IDE) в целом полезный toString () можно тривиально добавить за меньшее время, которое потребовалось для добавления этого комментария, не говоря уже об исходной публикации ...   -  person Gwyn Evans    schedule 04.02.2011
comment
У вашего «Человека» может быть имя, более читабельным будет фраза «Пол не может жениться на себе» вместо «@ 472357, возможно, не жениться на себе». Вы сами решаете, что это может быть в виде строки и что может быть наиболее пояснительным.   -  person Tokazio    schedule 31.03.2016
comment
@Tokazio Однако, поскольку Person имена не уникальны, они бесполезны в качестве идентификаторов. Вы бы не использовали имя человека в качестве первичного ключа в таблице базы данных.   -  person Raedwald    schedule 31.03.2016
comment
@Raedwald: конечно, это пример того, что имя или описание более объяснимы, чем адрес памяти;)   -  person Tokazio    schedule 31.03.2016


Ответы (5)


Пункт №3 является слабым звеном в этом аргументе, и я фактически категорически не согласен с ним. Ваш инвариант (переупорядочен)

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

Я бы сказал, скорее:

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

То есть логическое следствие. Если x и y равны, их toString () должны быть равны, но равенство toString () не обязательно означает, что объекты равны (подумайте об отношении _3 _: _ 4_; равные объекты должны имеют один и тот же хэш-код, но один и тот же хеш-код не может не означать, что объекты равны).

По сути, 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-объектов не дают вам никакой информации вообще < / em>.

person Cowan    schedule 03.02.2011
comment
+1 Другие говорили о том, что №3 ошибочен, но ваш ответ - самое ясное указание на проблему. - person Raedwald; 04.02.2011
comment
Однако обратите внимание на мои №4 и №5: когда есть поля типа ID, мне кажется очевидным, что метод toString() должен отображать их. Ваш пример семейства Смитов демонстрирует это с помощью Person.name. Но что делать, если таких полей нет? - person Raedwald; 04.02.2011
comment
Я думаю, мне нужно, чтобы вы пояснили, что вы имеете в виду под объектом «сущность». Если вы говорите о EJB или JPA Entity, у них всегда есть какое-то поле ID, потому что по определению JPA Entity имеет первичный ключ, который можно использовать для его уникальной идентификации. И это первичный ключ, который, вероятно, НАИБОЛЕЕ полезно иметь в журналах ... - person Cowan; 04.02.2011
comment
x.equals(y) == (x == y) - это определение сущности здесь. - person Raedwald; 04.02.2011
comment
@Raedwald: Я бы сказал, что вы применяете термин «сущность» к двум различным основным типам вещей, одну из которых я бы назвал изменяемым держателем данных. Отдельные экземпляры изменяемого типа держателя данных обычно не эквивалентны, даже если они содержат идентичные данные, но два объекта, каждый из которых содержит ссылки такого типа, могут быть эквивалентными, если целевые объекты этих ссылок содержат идентичные данные и никогда не будет отображаться ко всему, что может их изменить. Напротив, объекты, содержащие ссылки на отдельные экземпляры сущностей, никогда не эквивалентны. - person supercat; 21.11.2012

Таким образом, кажется, что предоставленная реализация toString () на самом деле не так уж плоха для классов сущностей. В таком случае, зачем его отменять?

Что заставляет вас думать, что цель toString() - просто иметь уникальную строку? Это не его цель. Его цель - дать вам контекст об экземпляре, а просто имя класса и хэш-код не дают вам контекста.

Редактировать

Просто хочу сказать, что я ни в коем случае не думаю, что вам нужно переопределять toString() для каждого объекта. Бесполезные объекты (например, конкретная реализация слушателя или стратегии) ​​не должны переопределять toString(), поскольку каждый отдельный экземпляр неотличим от любого другого, что означает, что достаточно имени класса.

person Mark Peters    schedule 03.02.2011
comment
дать вам контекст: но мне кажется, что информация о сущностях не так полезна. Сравните с проверкой предусловия в методе, выбрасывая подходящий IlegalArgumentException. Я предполагаю, что предварительное условие будет касаться объектов значений и идентичности или иного значения сущностей, и в этом случае использование предоставленного toString() в сообщении об исключении не так уж плохо для сущностей. - person Raedwald; 03.02.2011
comment
@Raedwald: может тебе стоит привести практический пример. Такие термины, как класс сущности, имеют тенденцию быть разбросанными. - person Mark Peters; 03.02.2011

Наличие метода toString() в классах сущностей может быть чрезвычайно полезным для целей отладки. С практической точки зрения использование шаблонов IDE или чего-то вроде аннотации Project Lombok @ToString значительно упрощает и позволяет легко и быстро внедрить.

person Kai Sternad    schedule 03.02.2011
comment
может быть чрезвычайно полезным для целей отладки. Какие аспекты toString() метода для класса сущности, по вашему мнению, обычно полезны для отладки? - person Raedwald; 03.02.2011
comment
Например. при модульном тестировании, равен ли граф объекта, полученный JPA, ожидаемому графу объекта - person Puce; 03.02.2011
comment
равно ожидаемому означает, что единственной релевантной взаимосвязью является x.equals(y), которая для сущностей равна x==y. Итак, все, что вам нужно, это toString() представление, которое показывает, хорошо ли это x==y: Object.toString(). - person Raedwald; 03.02.2011

Я всегда использую toString () для своих целей, а не из-за каких-то технических требований. Когда у меня есть класс Person, метод toString возвращает имя человека. Ни больше ни меньше. Это не уникально, но для целей отладки достаточно увидеть, о чем идет речь. Это очень удобно, особенно в веб-разработке, когда мне просто нужно написать имя объекта в JSP, чтобы получить имя человека, чтобы я знал, что у меня есть правильный объект.

Если объект имеет некоторые уникальные данные (например, идентификатор базы данных), то это идеальный кандидат для toString (), поэтому он может вернуть #294: John Doe. Но уникальность не является обязательным требованием.

На самом деле ... Даже если господин Блох так говорит ... Я не думаю, что имеет смысл иметь какие-либо правила для реализации toString (). Это имеет смысл для hashCode () и equals (), но не для toString ().

person kayahr    schedule 03.02.2011

Да, оно того стоит. ToString помогает визуально отображать состояние объекта. ИМО, это особенно важно для объекта, потому что 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!

person hisdrewness    schedule 03.02.2011
comment
Причина для включения значений, составляющих первичный ключ объекта, но не любых других. Я уже упоминал представление, которое включает это имя или числовой идентификатор. - person Raedwald; 03.02.2011