Что писать в методе toString() ?

Написано 3 Июнь, 2011 в категории Java,Разработка ПО

Когда пишешь и используешь класс 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 по форматированию и автоматизации вывода содержимого полей объекта. Думаю возможностей применения этого класса еще вагон и маленькая тележка. Дерзайте! :)