Метод toString() в Java
Переопределяйте toString() в классах Java для удобного строкового представления объектов при логировании и отладке.
toString — это метод, к которому обращаются System.out.println, конкатенация строк и ваш отладчик, когда им нужно представить объект в виде текста. По умолчанию — унаследованный от Object — он возвращает имя класса плюс нечитаемый шестнадцатеричный хэш. Переопределение toString — одно из самых дешёвых и высокоэффективных действий, которые вы можете сделать для тех, кто будет читать ваши логи и трассировки стека (нередко — будущей версии вас самих).
Где вызывается метод
Вы почти никогда не вызываете toString напрямую. Он вызывается неявно всякий раз, когда объект преобразуется в текст:
Point p = new Point(3, 4);
System.out.println(p); // calls p.toString()
String msg = "got " + p; // calls p.toString()
log.info("point: {}", p); // calls p.toString()Везде, где Object появляется в контексте, требующем String, запускается toString. Именно поэтому поведение по умолчанию так разочаровывает — оно встречается повсюду и не даёт никакой полезной информации.
Поведение по умолчанию
Object.toString() возвращает getClass().getName() + "@" + Integer.toHexString(hashCode()). Таким образом, обычный класс выводится примерно так: com.example.Point@1540e19d. Имя класса ещё куда ни шло, а вот шестнадцатеричное число — это идентификационный хэш-код, бесполезный, когда вы пытаетесь понять, что содержат поля объекта.
Хорошее переопределение
Хорошее toString короткое, однозначное и содержит данные, которые читатель хочет видеть:
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}Несколько полезных соглашений:
- Включайте имя класса. Когда в строке лога смешиваются объекты, важно знать, что это за тип.
- Используйте стабильный, разбираемый формат.
Class[field=value, field=value]— это то, что используют записи (records) и большинство современных библиотек Java. Такой формат читаем и удобен для поиска с помощью grep. - Показывайте важные поля — как правило, те, что участвуют в
equals. Пропускайте огромные поля (блоб размером 10 МБ,Connection). - Не включайте секреты. Пароли, токены, персональные данные — опускайте их или маскируйте.
toStringпопадает в лог-файлы.
С String.format или текстовыми блоками
Для более чем трёх-четырёх полей String.format (или текстовый блок) читается лучше, чем конкатенация:
@Override
public String toString() {
return String.format("User[id=%d, name=%s, role=%s]", id, name, role);
}Не делайте метод дорогостоящим
toString вызывается чаще, чем вы думаете — фреймворки логирования вычисляют аргументы лениво, но достаточно охотно, так что toString с O(n²) сложностью на списке из 100 тысяч элементов точно замедлит ваш сервис. Держите вывод ограниченным по объёму. Метод toString коллекции следует ограничивать разумным числом первых элементов, добавляя ... (N more) для остальных.
Не выбрасывайте исключения из toString
Выброс исключения в toString превращает рядовую строку лога в неожиданный NullPointerException — а исходная проблема, которую вы пытались залогировать, теряется. Защищайтесь от null:
@Override
public String toString() {
return "Order[id=" + id + ", customer=" + (customer == null ? "<none>" : customer.name()) + "]";
}Records получают toString бесплатно
Если ваш класс является простым контейнером данных, records уже генерируют хорошее toString:
record Point(int x, int y) {}
System.out.println(new Point(3, 4)); // Point[x=3, y=4]Поэтому переопределение toString в основном нужно для классов, не являющихся records.
Рабочий пример
Что дальше
toString создаёт описание объекта. Следующая глава посвящена созданию копии объекта — clone, Cloneable и непростым архитектурным решениям, связанным с поверхностным и глубоким копированием. Продолжите с клонирования объектов в Java.