Что писать в методе toString() ?
Когда пишешь и используешь класс bean (pojo), бывает нужно вывести информацию о его содержимом в лог. Для этого предусмотрен метод toString(). Допустим нужно вывести содержимое всех полей. Приходится вручную добавлять поля и их форматирование, например так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public String toString() { return "LogInfo{" + ", center='" + getCenter() + '\'' + ", name='" + getName() + '\'' + ", priority=" + getPriority() + ", direction=" + getDirection() + ", protocolId='" + getProtocolId() + '\'' + ", rawDataSize=" + getRawDataSize() + ", modificateDate=" + modificateDate + ", uid='" + getUid() + '\'' + ", status=" + status + ", tryCount=" + tryСount + '}'; } |
Возникает ряд неудобств. Когда добавляются новые поля необходимо делать правку в двух местах, беда в том, что вам никто об этом не подскажет и узнаете вы об этом когда в логе не увидите новых полей. В случае если изменяются имена существующих полей (рефакторинг) или поля удаляются, о необходимости внести изменения в toString() вам напомнит компилятор или среда разработки (поклон Intellij Idea), и даже в этом случае необходимость вносить изменение в нескольких местах отнимает время и силы.
Есть еще одно обстоятельство. Когда система разрабатывается довольно долго, начинаешь забывать, как ты форматировал в других bean'ах или появляется новый «любимый» стиль форматирования, но настоящая проблема начинается, если разработку ведут несколько человек. Вероятность, того что стиль возвращаемой методом toString() строки будет у каждого свой практически стопроцентная. Это приводит к тому что в логах появляются записи, которые очень непривычно(трудно) читать, даже если люди не впадают в крайности (а крайности бывают: чрезмерное использование бантиков и рюшечек или наоборот компактно-аскетичный стиль), необходимо время что-бы привыкнуть к чужому стилю.
Решение этих проблем было найдено в виде класса ReflectionToStringBuilder. Этот класс используя reflection просматривает поля объекта и вызывает у них метод toString(). Располагается он в бибилиотеке commons-lang (я использую версию 2.4), почитать доки и скачать библиотеку можно по ссылке http://commons.apache.org/lang/.
Рассмотрим примеры (из листингов были удалены импорты (import's) и методы get/set для экономии места):
Листинг 1. Простой вывод.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /** * Date: 18.10.2009 15:46:50 * Тестовый класс, точка входа в программу, во всех * наших примерах он меняться не будет * (вывод в данном примере производится в консоль, что * на самом деле не принципиально, * с таким же успехом мы можем выводить лог в файл * используя, например, log4j) */ public class Test { public static void main(String[] args) { A test = new A("Привет", 23, new B("Всем", "Кто не спит")); System.out.println(test.toString()); } } |
Класс А.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class A { private String fielda; // поле-объект стандартного типа private int fieldb; // поле примитивного типа private B fieldc; // поле-объект пользовательского класса (то есть класс мой) public A() { } public A(String fielda, int fieldb, B fieldc) { this.fielda = fielda; this.fieldb = fieldb; this.fieldc = fieldc; } @Override public String toString() { return ReflectionToStringBuilder.toString(this); } } |
Класс B.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class B { private String fieldd; private String fielda; public B() { } public B(String fieldd, String fielda) { this.fieldd = fieldd; this.fielda = fielda; } @Override public String toString() { return ReflectionToStringBuilder.toString(this); } } |
На выходе:
reflection.A@1546e25[fielda=Привет,fieldb=23,fieldc=reflection.B@b1c5fa[fieldd=Всем,fielda=Кто не спит]]
Строка не очень красивая, но единый формат мы уже имеем. При любом добавлении, удалении или изменении полей - toString() меняться не будет! Формат вывода полей такой fieldName=value. В принципе, если поправить формат, то получиться коротко и довольно красиво. На наше счастье, формат вывода можно менять, в первом примере использовался DEFAULT_STYLE.
Листинг 2. Мне больше нравится стиль по-короче.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class A { private String fielda; // поле-объект стандартного типа private int fieldb; // поле примитивного типа private B fieldc; // поле-объект пользовательского класса (то есть класс мой) public A() { } public A(String fielda, int fieldb, B fieldc) { this.fielda = fielda; this.fieldb = fieldb; this.fieldc = fieldc; } @Override public String toString() { return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class B { private String fieldd; private String fielda; public B() { } public B(String fieldd, String fielda) { this.fieldd = fieldd; this.fielda = fielda; } @Override public String toString() { return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE); } } |
На выходе: A[fielda=Привет,fieldb=23,fieldc=B[fieldd=Всем,fielda=Кто не спит]]
На мой взгляд стало намного симпатичнее + стандартизировано + один раз написал и забыл.
Есть еще ряд вариантов:
ToStringStyle.MULTI_LINE_STYLE
На выходе:
reflection.A@1546e25[
fielda=Привет
fieldb=23
fieldc=reflection.B@b1c5fa[
fieldd=Всем
fielda=Кто не спит
]
]
ToStringStyle.NO_FIELD_NAMES_STYLE
На выходе:
reflection.A@1546e25[Привет,23,reflection.B@b1c5fa[Всем,Кто не спит]]
ToStringStyle.SIMPLE_STYLE
На выходе:
Привет,23,Всем,Кто не спит
Вроде бы все проблемы, которые у меня возникли разрешились, остались тонкости. Например, вопрос, а что делать если нам не нужны все поля? Вопрос хороший, хоть и не частый (во всяком случае у меня), поэтому приведу решение:
Листинг 3. Пример удаления ненужных полей из вывода toString().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class A { private String fielda; // поле-объект стандартного типа private int fieldb; // поле примитивного типа private B fieldc; // поле-объект пользовательского класса (то есть класс мой) public A() { } public A(String fielda, int fieldb, B fieldc) { this.fielda = fielda; this.fieldb = fieldb; this.fieldc = fieldc; } @Override public String toString() { String[] excludeFieldNames = {"fielda"}; return (new ReflectionToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) .setExcludeFieldNames(excludeFieldNames)) .toString(); } } |
На выходе: A[fieldb=23,fieldc=B[fieldd=Всем,fielda=Кто не спит]]
Тут решение подлиннее, но оно на мой взгляд требуется, только в случае если полей чересчур много, или выводится много лишнего (может отвлекать или трудно найти нужную инфу). Хочу обратить внимание, что поле fielda класса B не отфильтровалось, а вот из класса А исчезло!
Еще немного вкусностей. Мы можем форматировать (и ваще делать что угодно) со значениями полей. Например, если у нас есть поле типа Date, можно задать формат его вывода, что и будет сейчас продемонстрировано.
Листинг 4. Форматируем дату в соответствие с нашим форматом времени.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public class A { private static final String DATE_FORMAT = "dd.MM.yyyy HH:mm:ss"; private String fielda; // поле-объект стандартного типа private int fieldb; // поле примитивного типа private B fieldc; // поле-объект пользовательского класса (то есть класс мой) private Date date; // выведем дату в нужном нам формате public A() { } public A(String fielda, int fieldb, B fieldc) { this.fielda = fielda; this.fieldb = fieldb; this.fieldc = fieldc; date = new Date(System.currentTimeMillis()); } @Override public String toString() { return (new ReflectionToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) { protected Object getValue(Field field) throws java.lang.IllegalArgumentException, java.lang.IllegalAccessException { return (field.getName().equals("date")) ? DateFormatUtils.format((Date) super.getValue(field), DATE_FORMAT) : super.getValue(field) ; } }).toString(); } } |
На выходе: A[fielda=Привет,fieldb=23,fieldc=B[fieldd=Всем,fielda=Кто не спит],date=19.10.2009 18:56:37]
Все просто — мы создаем анонимный класс на основе ReflectionToStringBuilder, а говоря проще заменяем метод getValue() своей реализацией (спасибо разработчикам, что оставили такую возможность). А в своей реализации ставим фильтр на поле date и выдаем значение в уже отформатированном виде. Кстати, по умолчанию дата выводится вот в таком виде: Mon Oct 19 19:05:04 MSD 2009. Опять же стоит оговориться, что хоть величина кода выросла, но плюсы остались. Минус пожалуй только в том, что если мы захотим эти поля удалить или изменить — прийдется вносить правки в toString(), но это ситуация достаточно редкая, а в случае удаления поля, код в toString() вообще не будет влиять на компиляцию (просто в методе появится чуток мусора). Кстати сказать, класс DateFormatUtils тоже находится в commons-lang.
Листинг 5. Теперь можно немного расслабиться — посмотрим как производится вывод Map'ов и List'ов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public class A { private static final String DATE_FORMAT = "dd.MM.yyyy HH:mm:ss"; private String fielda; // поле-объект стандартного типа private int fieldb; // поле примитивного типа private B fieldc; // поле-объект пользовательского класса (то есть класс мой) private Map<String, Object> context = new HashMap<String, Object>(); // посмотрим как выводится Map private ArrayList list = new ArrayList(){}; // посмотрим как выводится List private Date date; // выведем дату в нужном нам формате public A() { } public A(String fielda, int fieldb, B fieldc) { this.fielda = fielda; this.fieldb = fieldb; this.fieldc = fieldc; context.put("Name", "Stas"); context.put("Family", "Yakovlev"); list.add("first"); list.add("second"); list.add("third, four"); date = new Date(System.currentTimeMillis()); } @Override public String toString() { return (new ReflectionToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) { protected Object getValue(Field field) throws java.lang.IllegalArgumentException, java.lang.IllegalAccessException { return (field.getName().equals("date")) ? DateFormatUtils.format((Date) super.getValue(field), DATE_FORMAT) : super.getValue(field); } }).toString(); } } |
На выходе: A[fielda=Привет,fieldb=23,fieldc=B[fieldd=Всем,fielda=Кто не спит],context={Family=Yakovlev, Name=Stas},list=[first, second, third, four],date=19.10.2009 19:18:56]
Вот так, через запятую, выводятся значения в листе. Причем в выводе мы видим 4 элемента, а на самом деле их всего лишь три, т.е. запятая никак не экранируется. Приведенный в листинге 5 случай, скорее исключение из правил (я специально подобрал такую строку), но лучше об этой особенности знать, чем не знать. Если в объекте записаны не стандартные объекты и не примитивные типы, то элементы будут экранироваться скобками. Примерно такая же ситуация для Map'ов, как видно ключ и значение разделяются знаком равно, а сами пары разделяются запятой. Наверное, сложно подобрать такой разделитель, который бы не мог бы случайно встретиться в значении поля объекта, поэтому квадратная скобка вполне подходит.
В своем рассказе я обошёл еще один вопрос, который меня затронул — как будет производиться вывод значений, если мы имеем дело с наследованием. Давайте посмотрим пример.
Листинг 6. Вывод полей родительского класса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class A extends B { private String fielda; // поле-объект стандартного типа private int fieldb; // поле примитивного типа private B fieldc; // поле-объект пользовательского класса (то есть класс мой) public A() { } public A(String fielda, int fieldb, B fieldc) { super(fieldc.getFieldd(), fieldc.getFielda()); this.fielda = fielda; this.fieldb = fieldb; this.fieldc = fieldc; } @Override public String toString() { return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE); } } |
На выходе: A[fielda=Привет,fieldb=23,fieldc=B[fieldd=Всем,fielda=Кто не спит],fieldd=Всем,fielda=Кто не спит]
Как видите поля родительского класса выводятся после полей потомка, аналогично если бы у класса В был бы родитель, то в конце вывода мы бы увидили его поля. К слову сказать, можно указать ReflectionToStringBuilder уровень наследования на котором необходимо прекратить вывод родительских полей. У меня в этом необходимости не было, поэтому отсылаю вас к javadoc ReflectionToStringBuilder.html#setUpToClass .
Есть еще одна, достаточно неожиданная, особенность связанная с наследованием.
Листинг 7. Фильтр полей при наследовании.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class A extends B { private String fielda; // поле-объект стандартного типа private int fieldb; // поле примитивного типа private B fieldc; // поле-объект пользовательского класса (то есть класс мой) public A() { } public A(String fielda, int fieldb, B fieldc) { super(fieldc.getFieldd(), fieldc.getFielda()); this.fielda = fielda; this.fieldb = fieldb; this.fieldc = fieldc; } @Override public String toString() { String[] excludeFieldNames = {"fielda"}; return (new ReflectionToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) .setExcludeFieldNames(excludeFieldNames)) .toString(); } } |
На выходе: A[fieldb=23,fieldc=B[fieldd=Всем,fielda=Кто не спит],fieldd=Всем]
Думаю вы заметили, что было отфильтровано не только поле класса потомка (класс А), но и класса родителя (класс В), хотя на самом деле это разные (независимые) поля. Честно говоря, я обнаружил это случайно и решением этой задачки не занимался. Буду признателен, если кто-то разберется.
Были продемонстрированы некоторые (не побоюсь этого слова, основные) возможности ReflectionToStringBuilder по форматированию и автоматизации вывода содержимого полей объекта. Думаю возможностей применения этого класса еще вагон и маленькая тележка. Дерзайте!