Metoda toString()

Wprowadzenie

W Javie każda klasa dziedziczy po klasie Object, w której znajduje się kilka publicznych metod, które również są automatycznie dziedziczone. Obok metody equals() i hashCode() do takich metod należy także metoda toString(), która zwraca opis obiektu w postaci Stringa.

Metoda toString() z klasy Object

Sygnatura metody toString() z klasy Object to:

public String toString()

Wywołanie tej metody na dowolnym obiekcie spowoduje więc zwrócenie jej opisu w postaci Stringa złożonego z pełnej kwalifikowanej nazwy klasy (z uwzględnieniem pakietu) i wyniku metody hashCode() w postaci ósemkowej. Kod metody toString() w klasie Object wygląda dokładnie w taki sposób:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

Dla przykładowej klasy:

class Course {
    private String name;
    private String description;
    private double price;

    public Course(String name, String description, double price) {
        this.name = name;
        this.description = description;
        this.price = price;
    }

    //gettery settery
}

Wynik metody toString() wywołanej na dowolnym obiekcie:

Course course = new Course("Kurs Java", "Poznaj jeden z najbardziej popularnych języków programowania", 123);
System.out.println(course.toString());

Będzie wyglądał np. w ten sposób:

pl.javastart.course.Course@6b884d57

Przy założeniu, że w klasie nie została nadpisana metoda hashCode(), wywołanie metody toString() na kilku różnych obiektach niemal ze 100% pewnością zwróci różny wynik. Przykładowo poniższy kod:

Course course1 = new Course("Kurs Java", "Poznaj jeden z najbardziej popularnych języków programowania", 123);
Course course2 = new Course("Kurs HTML", "Naucz się tworzyć strony internetowe", 99);
System.out.println(course1.toString());
System.out.println(course2.toString());

Wydrukuje w konsoli np.:

pl.javastart.course.Course@6b884d57
pl.javastart.course.Course@38af3868

Nadpisywanie metody toString()

Metodę toString() odziedziczoną z klasy Object warto nadpisać własną implementacją, aby wydruk był bardziej przyjazny i użyteczny. Wynik metody toString() może, ale nie musi uwzględniać wszystkie pola, które znajdują się w klasie. Wynik powinniśmy dostosować w taki sposób, aby ułatwiało to np. poszukiwanie błędów w kodzie.

class Course {
    private String name;
    private String description;
    private double price;

    public Course(String name, String description, double price) {
        this.name = name;
        this.description = description;
        this.price = price;
    }

    //gettery, settery

    @Override
    public String toString() {
        return "Nazwa: %s, opis: %s, cena: %.2fzł".formatted(name, description, price);
    }
}

Uruchamiając teraz kod:

Course course1 = new Course("Kurs Java", "Poznaj jeden z najbardziej popularnych języków programowania", 123);
Course course2 = new Course("Kurs HTML", "Naucz się tworzyć strony internetowe", 99);
System.out.println(course1.toString());
System.out.println(course2.toString());

W wyniku otrzymamy:

Nazwa: Kurs Java, opis: Poznaj jeden z najbardziej popularnych języków programowania, cena: 123.00zł
Nazwa: Kurs HTML, opis: Naucz się tworzyć strony internetowe, cena: 99.00zł

Automatyczne wywoływanie metody toString()

Metoda toString() jest wywoływana automatycznie, jeżeli spróbujemy np. wykorzystać konkatenację na obiekcie, albo przekażemy obiekt jako argument metody System.out.print(). Oznacza to, że poniższe zapisy są sobie równoważne:

Course course = new Course("Kurs Java", "Poznaj jeden z najbardziej popularnych języków programowania", 123);
#1
System.out.println(course.toString());
#2
System.out.println(course);

W przypadku pierwszej wersji, środowisko programistyczne prawdopodobnie podpowie Ci, że zapis da się uprościć. W przypadku IntelliJ IDEA wygląda to tak:

Debugowanie

Nadpisywanie metody toString() odgrywa szczególnie istotną rolę w sytuacji, gdy korzystamy z debuggera.

Załóżmy, że klasa wygląda w taki sposób:

class Course {
    private String name;
    private String description;
    private double price;

    public Course(String name, String description, double price) {
        name = name;
        description = description;
        price = price;
    }

    //gettery i settery
}

W konstruktorze zapomnieliśmy o użyciu słowa this do wskazania pól klasy i nie nadpisaliśmy metody toString(). Jeżeli w naszym programie będziemy odwoływali się do pól z nazwą, czy opisem kursu, to otrzymamy w wyniku wartości null. Np.:

Course course = new Course("Kurs Java", "Poznaj jeden z najbardziej popularnych języków programowania", 123);
System.out.println("Nazwa kursu " + course.getName());

Wynik:

Nazwa kursu null

Jeżeli skorzystamy z debuggera to w konsoli nie zobaczymy na piewrszy rzut oka przydatnych informacji. Te są ukryte i musimy rozwinąć obiekt, aby się do nich dostać:

Po nadpisaniu metody toString() na pierwszy rzut oka otrzymujemy więcej informacji, które mogą ułatwić znalezienie problemu:

Na co uważać

Istnieje jedna sytuacja, w której błędna implementacja metody toString() może doprowadzić nawet do zatrzymania się aplikacji i przepełnienia stosu.

Jeżeli między dwoma klasami zachodzi agregacja, tzn. np. do kursu przypisana jest kategoria, a w kategorii będą przechowywane wszystkie przypisane do niej kursy, to np. w sytuacji, gdy automatycznie wygenerujemy metody toString() w obu klasach przy użyciu IDE, to wywołanie metody toString() na obiekcie kursu, lub kategorii doprowadzi do nieskończonego, wzajemnego wywoływania metod toString().

Course.java

package pl.javastart.stackoverflow;

class Course {
    private String name;
    private String description;
    private double price;
    private Category category;

    public Course(String name, String description, double price) {
        this.name = name;
        this.description = description;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public Category getCategory() {
        return category;
    }

    public void setCategory(Category category) {
        this.category = category;
    }

    @Override
    public String toString() {
        return "Course{" +
                "name='" + name + '\'' +
                ", description='" + description + '\'' +
                ", price=" + price +
                ", category=" + category + //wywołanie metody toString() na obiekcie Category
                '}';
    }
}

Category.java

package pl.javastart.stackoverflow;

import java.util.ArrayList;
import java.util.List;

class Category {
    private String name;
    private List<Course> courses = new ArrayList<>();

    Category(String name) {
        this.name = name;
    }

    void addCourse(Course course) {
        courses.add(course);
        course.setCategory(this);
    }

    @Override
    public String toString() {
        return "Category{" +
                "name='" + name + '\'' +
                ", courses=" + courses + //na obiektach Course w kolekcji będzie wywoływana metoda toString()
                '}';
    }
}

Uruchomienie kodu:

Course course = new Course("Kurs Java", "Poznaj jeden z najbardziej popularnych języków programowania", 123);
Category category = new Category("Programowanie");
category.addCourse(course);
System.out.println(course);

Spowoduje przepełnienie stosu:

Exception in thread "main" java.lang.StackOverflowError

Rozwiązaniem jest to, żeby np. w klasie Category nie pobierać pełnych informacji o wszystkich przypisanych kursach, a jedynie pobranie rozmiaru kolekcji (liczby przypisanych kursów):

package pl.javastart.stackoverflow;

import java.util.ArrayList;
import java.util.List;

class Category {
    private String name;
    private List<Course> courses = new ArrayList<>();

    Category(String name) {
        this.name = name;
    }

    void addCourse(Course course) {
        courses.add(course);
        course.setCategory(this);
    }

    @Override
    public String toString() {
        return "Category{" +
                "name='" + name + '\'' +
                ", courses=" + courses.size() + //pobieramy tylko rozmiar
                '}';
    }
}

Uruchamiając ten sam kod co wcześniej, teraz wszystko będzie ok, ponieważ w klasie Category nie są już wywoływane metody toString() na obiektach Course.

Dodatkowe źródła

Kod z tej lekcji: https://github.com/javastartpl/examples/tree/master/tostring

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.