Статья

Тонкости метода toString() в Java

Каждый класс в Java является дочерним прямо или косвенно по отношению к классу Object. И поскольку класс Object содержит метод toString(), мы можем вызвать toString() в любом экземпляре и получить его строковое представление.
В этой статье мы рассмотрим поведение toString() по умолчанию и узнаем, как его изменить.

Поведение по умолчанию

Всякий раз, когда мы печатаем ссылку на объект, она вызывает внутренний метод toString(). Итак, если мы не определяем метод toString() в нашем классе, то вызывается Object.toString().
Метод toString() в Object довольно общий:
public String toString() {
    return getClass().getName()+"@"+Integer.toHexString(hashCode());
}
Чтобы увидеть, как это работает, давайте создадим объект Car, который мы будем использовать на протяжении всей нашей статьи:
public class Car {
    private String model;
    private String color;

    // здесь стандартные геттеры и сеттеры. Нет реализации метода toString()

}
Теперь, если мы попытаемся напечатать наш объект Car, будет вызван Object.toString(), и результат будет похож на что-то подобное:
com.test.tostring.Car@6d06d54c

Переопределение поведения по умолчанию

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

Примитивные типы и строки

Наш объект Car имеет как строковые, так и примитивные атрибуты. Нам нужно переопределить метод toString() для достижения более значимого результата:
public class CarPrimitiveToString extends Car {
    private long weight;

    @Override
    public String toString() {
        return "Car [weight=" + weight + ", getModel()=" + getModel()
          + ", getColor()=" + getColor() + "]";
    }
}
Давайте посмотрим, что мы получим, когда вызовем toString():
@Test
public void givenPrimitive_whenToString_thenCarDetails() {
    CarPrimitiveToString car = new CarPrimitiveToString();
    car.setModel("Ford");
    car.setColor("White");
    car.setWeight(1300);
    assertEquals("Car [weight=1300, getModel()=Ford, getColor()=White]", 
      car.toString());
}

Сложные объекты Java

Давайте теперь рассмотрим сценарий, в котором наш объект Car также содержит атрибут order, имеющий тип Order. Наш класс Order содержит поля как строкового, так и примитивного типа данных.
Итак, давайте снова переопределим toString():
public class CarComplexObjectToString extends Car {
    private Order order;
    //здесь стандартные геттеры и сеттеры
    
    @Override
    public String toString() {
        return "Car [order=" + order + ", getModel()=" + getModel()
          + ", getColor()=" + getColor() + "]";
    }      
}
Order – это сложный объект, поэтому если мы просто напечатаем экземпляр Car, не переопределяя метод toString() в нашем классе Order, он будет печатать как Order@<hashcode>.

Чтобы исправить это, давайте также переопределим toString() по порядку:
public class Order {
    
    private String orderId;
    private String desc;
 
    @Override
    public String toString() {
        return "Order [orderId=" + orderId + ", desc=" + desc + "]";
    }
}
Теперь давайте посмотрим, что произойдет, когда мы вызовем метод toString() для нашего объекта Car, который содержит атрибут order:
@Test
public void givenComplex_whenToString_thenCarDetails() {
    CarComplexObjectToString car = new CarComplexObjectToString();    
    // .. здесь заполняем car

    Order order = new Order();
    order.setOrderId("4512");
    order.setDesc("car order");
    car.setOrders(order);
        
    assertEquals("Car [order=Order [orderId=4512, desc=car order], " +
      "getModel()=Ford, getColor()=White]", car.toString());
}

Массив объектов

Далее давайте изменим класс Car, чтобы у него был массив Order. Если мы просто напечатаем наш объект Car, без специальной обработки для нашего объекта orders, он будет печатать Order;@<хэш-код>.
Чтобы исправить это, давайте воспользуемся Arrays.toString() для поля orders:
public class CarArrayToString extends Car {
    private Order[] orders;

    @Override
    public String toString() {
        return "Car [orders=" + Arrays.toString(orders) 
          + ", getModel()=" + getModel()
          + ", getWhite()=" + getWhite() + "]";
    }    
}
Давайте посмотрим на результат вызова вышеупомянутого метода toString():
@Test
public void givenArray_whenToString_thenCarDetails() {
    CarArrayToString car = new CarArrayToString();
    // .. здесь заполняем car и order
 
    car.setOrders(new Order[] { order });         
    
    assertEquals("Car [orders=[Order [orderId=4512, desc=car order]], " +
      "getModel()=Ford, getColor()=White]", car.toString());
}

Wrappers, Collections и StringBuffers

Когда объект состоит из врапперов, коллекций или StringBuffer-ов, пользовательская реализация toString() не требуется, поскольку эти объекты уже переопределили метод toString() нужными представлениями:
public class CarWrapperCollectionToString extends Car {
    private Integer year; // Wrapper class

    private List<String> orders; // Collection

    private StringBuffer pilot; // StringBuffer
  
    @Override
    public String toString() {
        return "Car [year=" + year + ", orders=" + orders + ", pilot=" + pilot
          + ", getModel()=" + getModel() + ", getColor()=" + getColor() + "]";
    }
}
java