Каждый Java-разработчик сталкивался с необходимостью отладки кода или логирования состояния объектов. И здесь на помощь приходит метод toString(), один из базовых методов класса Object. Но писать его вручную для каждого класса это занятие утомительное и чреватое ошибками. Именно для решения этой проблемы и существует ToStringBuilder: он собирает строковое представление объекта за вас, и в простейшем случае весь метод умещается в одну строку return ToStringBuilder.reflectionToString(this);.

Кратко

  • ToStringBuilder из Apache Commons Lang3 собирает строковое представление объекта, чтобы не писать toString() руками для каждого класса.
  • Самый короткий вариант это ToStringBuilder.reflectionToString(this): через рефлексию он выведет все поля, включая приватные.
  • Поля с модификаторами static и transient в вывод не попадают, а все остальные попадают, поэтому пароли и токены легко утекают в логи.
  • Формат задается вторым аргументом: есть семь готовых стилей ToStringStyle, можно написать и свой.
  • appendSuper(super.toString()) добавляет данные родителя, а четвертый параметр reflectionToString() ограничивает подъем по иерархии наследования.
  • Рефлексия хороша для прототипа, а в рабочем коде поля чаще перечисляют явно через append(): это быстрее и безопаснее.
ToStringBuilder это вспомогательный класс, предоставляемый библиотекой Apache Commons Lang. Он обеспечивает полный и лучший контроль над тем, какие данные объекта и в каком количестве должны выводиться методом toString(), а также в каком формате.

Подключение библиотеки

Maven:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.20.0</version>
</dependency>

Gradle:

implementation 'org.apache.commons:commons-lang3:3.20.0'
Важно: Используйте commons-lang3 (пакет org.apache.commons.lang3), а не устаревший commons-lang версии 2.x. Старая версия больше не поддерживается.

Модели для примеров

Чтобы продемонстрировать различные возможности использования ToStringBuilder для построения метода toString() в различных сценариях, я создам три модели: AbstractUser.java, WebUser.java и GuestUser.java.

AbstractUser.java

package com.howtodoinjava.model;

import java.io.Serializable;
import org.apache.commons.lang3.builder.ToStringBuilder;

public abstract class AbstractUser implements Serializable {
    private static final long serialVersionUID = 1L;

    private int id;
    private String firstName;
    private String lastName;
    private String age;

    // Setters и getters
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }

    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }

    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }

    public String getAge() { return age; }
    public void setAge(String age) { this.age = age; }
}

WebUser.java

package com.howtodoinjava.model;

import java.time.LocalDateTime;

public class WebUser extends AbstractUser {
    private static final long serialVersionUID = 1L;
    private LocalDateTime lastLoggedIn;

    public LocalDateTime getLastLoggedIn() {
        return lastLoggedIn;
    }

    public void setLastLoggedIn(LocalDateTime lastLoggedIn) {
        this.lastLoggedIn = lastLoggedIn;
    }
}

GuestUser.java

package com.howtodoinjava.model;

public class GuestUser extends WebUser {
    private static final long serialVersionUID = 1L;
    private String location;

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }
}

Различные примеры использования

1. toString() в суперклассе используют все дочерние классы

Вы можете переопределить метод toString() в классе объекта самого верхнего уровня, т.е. в нашем случае AbstractUser.java. Этот метод используют все дочерние классы, если они не содержат собственную версию метода toString().
@Override
public String toString() {
    return ToStringBuilder.reflectionToString(this);
}
Приведенный выше метод toString() способен дать всю имеющуюся информацию о текущем классе, он также будет работать и в подклассах, если только подкласс не переопределит метод toString(). Пример использования:
package com.howtodoinjava;

import java.time.LocalDateTime;
import com.howtodoinjava.model.GuestUser;

public class ToStringDemoUsage {
    public static void main(String[] args) {
        GuestUser guest = getGuestUser();
        System.out.println(guest);
    }

    public static GuestUser getGuestUser() {
        GuestUser user = new GuestUser();
        user.setId(100);
        user.setFirstName("Lokesh");
        user.setLastName("Gupta");
        user.setAge("30");
        user.setLastLoggedIn(LocalDateTime.now());
        user.setLocation("New Delhi");
        return user;
    }
}
Вывод:
com.howtodoinjava.model.GuestUser@d1f24bb[location=New Delhi,lastLoggedIn=2025-11-28T13:31:05.482,id=100,firstName=Lokesh,lastName=Gupta,age=30]
Важные замечания:
  • Метод reflectionToString() использует рефлексию, что может снижать производительность при частом вызове
  • Рефлексия игнорирует поля, помеченные как transient или static
  • Будьте осторожны с конфиденциальными данными: они будут выведены в строку

2. Пользовательское форматирование любого типа, например даты

Можно применить пользовательский формат для любого поля в toString() методе. Пример пользовательского форматирования показан ниже:
package com.howtodoinjava.style;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.apache.commons.lang3.builder.ToStringStyle;

public class CustomToStringStyle extends ToStringStyle {
    private static final long serialVersionUID = 1L;

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    @Override
    protected void appendDetail(StringBuffer buffer, String fieldName, Object value) {
        if (value instanceof LocalDateTime) {
            value = FORMATTER.format((LocalDateTime) value);
        }
        buffer.append(value);
    }
}
Заодно из примера ушел SimpleDateFormat, который нельзя переиспользовать из нескольких потоков. DateTimeFormatter неизменяем и потокобезопасен, поэтому один экземпляр спокойно живет в статическом поле. Для использования форматирования поместите его в методе так:
@Override
public String toString() {
    return ToStringBuilder.reflectionToString(this, new CustomToStringStyle());
}
Вывод:
com.howtodoinjava.model.GuestUser@7910769b[location=New Delhi,lastLoggedIn=2025-11-28,id=100,firstName=Lokesh,lastName=Gupta,age=30]
Встроенные стили: Apache Commons Lang3 предоставляет семь готовых стилей форматирования. Вот как выглядит вывод каждого из них на примере из документации (объект Person с полями name, age и smoker):
Стиль Как выглядит вывод Комментарий
ToStringStyle.DEFAULT_STYLE Person@182f0db[name=John Doe,age=33,smoker=false] стандартный стиль, работает без настройки
ToStringStyle.SHORT_PREFIX_STYLE Person[name=John Doe,age=33,smoker=false] имя класса без пакета и хеш-кода
ToStringStyle.NO_FIELD_NAMES_STYLE Person@182f0db[John Doe,33,false] только значения, без имен полей
ToStringStyle.MULTI_LINE_STYLE Person@182f0db[ и дальше каждое поле на своей строке ] удобно читать в логах
ToStringStyle.NO_CLASS_NAME_STYLE [name=John Doe,age=33,smoker=false] без имени класса, доступен с версии 3.4
ToStringStyle.SIMPLE_STYLE John Doe,33,false ни класса, ни имен полей, только значения
ToStringStyle.JSON_STYLE {"name": "John Doe", "age": 33, "smoker": true} JSON-подобный вывод; без имен полей бросает исключение

3. Используйте информацию из суперкласса в подклассе

Если вы хотите переопределить toString() в дочернем классе, для добавления чего-либо к информации, полученной от суперкласса, сделайте это так:
public class WebUser extends AbstractUser {
    // Other code

    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .appendSuper(super.toString())
                .append("lastLoggedIn", lastLoggedIn)
                .toString();
    }
}
Вывод:
com.howtodoinjava.model.WebUser@22aed3a5[id=100,firstName=Lokesh,lastName=Gupta,age=30,lastLoggedIn=2025-11-28T13:31:05.482]
Метод appendSuper() позволяет включить строковое представление родительского класса, а затем добавить свои специфичные поля. Это особенно полезно в глубоких иерархиях наследования.

4. Использование информации только до определенного уровня иерархии наследования

Предположим, в каком-то дочернем классе вам необходимо раскрыть поля всех суперклассов. Вы можете включить информацию до определенного уровня иерархии наследования следующим путем:
public class GuestUser extends WebUser {
    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, new CustomToStringStyle(), true, WebUser.class);
    }
}
Вывод:
com.howtodoinjava.model.GuestUser@18dd7404[location=New Delhi,lastLoggedIn=2025-11-28]
Четвертый параметр (WebUser.class) указывает, до какого класса в иерархии нужно подниматься. В данном случае будут выведены поля только из GuestUser и WebUser, но не из AbstractUser.

5. Выводите только ту информацию, которую хотите

Иногда вы не хотите включить все поля класса в метод toString(). Тогда вы можете сделать это так:
public abstract class AbstractUser implements Serializable {
    // Other code

    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .append("firstName", firstName)
                .append("lastName", lastName)
                .append("age", age)
                .toString();
    }
}

public class GuestUser extends WebUser {
    // Other code

    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .appendSuper(super.toString())
                .append("location", location)
                .toString();
    }
}
Вывод:
com.howtodoinjava.model.GuestUser@6483dae1[firstName=Lokesh,lastName=Gupta,age=30,location=New Delhi]
Этот подход дает максимальный контроль над выводом и позволяет исключить конфиденциальные данные (например, пароли или токены доступа) из строкового представления объекта.

Когда ToStringBuilder не нужен

Библиотека решает конкретную задачу, но подключать ее стоит не всегда:
  • Класс описан как record. Начиная с Java 16 компилятор сам генерирует toString() со всеми компонентами записи, и отдельный билдер там лишний.
  • В проекте уже есть Lombok. Аннотация @ToString делает то же самое на этапе компиляции, без рефлексии во время работы, а ненужные поля убираются через @ToString.Exclude.
  • Метод вызывается в горячем коде. reflectionToString() каждый раз обходит поля рефлексией. Если toString() дергается в цикле или на каждый запрос, дешевле перечислить поля вручную или сгенерировать метод средствами IDE.

Заключение

ToStringBuilder от Apache Commons Lang3 это мощный инструмент для создания информативных строковых представлений объектов. Он позволяет:
  • Быстро создавать toString() через рефлексию
  • Гибко настраивать формат вывода
  • Контролировать иерархию наследования
  • Выбирать конкретные поля для отображения
Выбирайте подход в зависимости от ваших потребностей: рефлексия для прототипирования, явное перечисление для production-кода, и всегда помните о безопасности данных. Правильно реализованный toString() значительно упрощает отладку и делает ваши логи более информативными, экономя время на поиск проблем в работающих приложениях.

Вопросы и ответы

Что делает ToStringBuilder?

Он собирает строковое представление объекта вместо вас. В самом простом виде это одна строка: return ToStringBuilder.reflectionToString(this);, и метод toString() готов для любого класса. Если нужен контроль, поля перечисляют вручную через цепочку append(), а формат задают стилем.

Чем reflectionToString() отличается от ручного append()?

Объемом работы и ценой. Рефлексия сама находит все поля, включая приватные, и вывод не придется править, когда в классе появится новое поле. Но она медленнее обычного обращения к полям и выводит вообще все подряд, включая то, чего в логах быть не должно. Цепочка append() требует ручной поддержки, зато работает быстро и показывает ровно то, что вы выбрали.

Какие поля попадают в вывод reflectionToString()?

Все поля объекта, в том числе приватные, кроме помеченных как static и transient: статические принадлежат классу, а транзиентные обычно вычисляемые. Поля подкласса идут первыми, затем поля родителей. Если подниматься по иерархии нужно не до самого верха, четвертым параметром метода передают класс, на котором остановиться.

Как поменять формат вывода?

Вторым аргументом передают объект ToStringStyle. Есть семь готовых стилей, от SHORT_PREFIX_STYLE до JSON_STYLE, их вывод показан в таблице выше. Если ни один не подходит, наследуются от ToStringStyle и переопределяют нужные методы, например appendDetail(), чтобы по-своему печатать даты.

Нужен ли ToStringBuilder, если IDE умеет генерировать toString()?

Это разные компромиссы. IDE вставляет обычный код: он быстрый, но при добавлении поля его надо перегенерировать, и про это забывают. ToStringBuilder через рефлексию не требует правок вообще, но платит за это скоростью. А если класс описан как record, метод toString() язык сгенерирует сам, и билдер там не нужен.

Есть ли в Commons Lang похожие билдеры для equals и hashCode?

Да, в том же пакете лежат EqualsBuilder и HashCodeBuilder с такими же рефлексивными методами reflectionEquals() и reflectionHashCode(). Логика у них та же, но требования к этим методам строже, чем к toString(): у equals и hashCode есть контракт, который нельзя нарушать. Читайте также:

Если ты только начинаешь разбираться с методом toString() и хочешь понять фундаментальные правила, начни с базового гайда:

10 подсказок по переопределению метода toString()