Java – Il metodo toString degli oggetti

Questo articolo descrive il concetto del metodo toString degli oggetti in Java. È un argomento estremamente basilare nella programmazione Java ma è anche molto importante da conoscere bene, specialmente per chi sta iniziando da poco tempo a sviluppare in Java.

Concetto

Partiamo quindi dal concetto in generale: toString è un metodo definito originariamente nella classe java.lang.Object (la classe “padre” di tutti gli oggetti in Java) con lo scopo principale di fornire una “rappresentazione testuale” di un oggetto.

In Object il metodo toString ha la seguente forma:

public String toString()

Il metodo è pubblico, è senza parametri e restituisce semplicemente una stringa. All’interno di una qualunque altra classe più specifica (derivante direttamente o indirettamente da Object) è lecito ridefinire (principio di overriding) il metodo toString al fine di fornire una rappresentazione testuale più accurata ed appropriata per tutti gli oggetti di quella classe.

Prima di vedere del codice più concreto è bene valutare dove può essere usato il toString. Può ovviamente essere invocato esplicitamente (come per qualunque altro metodo), ad esempio:

String descrizione = unOggetto.toString();

Ma ci sono anche contesti in cui viene invocato implicitamente/internamente, ad esempio quando si usa la concatenazione delle stringhe oppure quando si passa un oggetto ad un metodo che riceve un generico Object e deve rappresentarlo testualmente da qualche parte.

1) String descrizione = "L'oggetto è: " + unOggetto;
2) System.out.println(unOggetto);

In 1) viene usato l’operatore + per cui se almeno uno dei due operandi è un String, avviene la cosiddetta “concatenazione delle stringhe” (convertendo eventualmente l’altro operando in String, se necessario). Il compilatore tratta la concatenazione delle stringhe facendo generare del codice che sfrutta la ben nota classe StringBuilder (da Java 5 in poi, in precedenza usava StringBuffer) ed ad un certo punto viene invocato il metodo append(Object) passando la variabile unOggetto.
In 2) viene invocato il metodo println(Object) della classe PrintStream passando la variabile unOggetto.

In entrambi i casi viene invocato un metodo che riceve un oggetto visto genericamente come Object e per ottenerne la rappresentazione testuale invoca toString su quell’oggetto ricevuto. Come si può osservare quindi, c’è effettivamente una invocazione di toString che però non è esplicita nel nostro codice ma è invece interna al framework.

Ridefinizione del toString

L’aspetto più importante del toString non è tanto la invocazione (che è basilare, come si è visto) ma la ridefinizione in una classe specifica.

Partiamo da un esempio semplice, una classe Punto2D dove non c’è esplicitamente la definizione di un toString.

public class Punto2D {
    private double x;
    private double y;

    public Punto2D(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }
}

Se proviamo ad usare questa classe con un semplice main di prova così:

public class ProvaStampaPunto2D {
    public static void main(String[] args) {
        Punto2D p1 = new Punto2D(15, 22);
        System.out.println("p1 vale: " + p1);
    }
}

L’output che si ottiene è qualcosa del tipo (nota: sicuramente varia nella parte finale dopo “@”):

p1 vale: Punto2D@35db9742

Perché viene mostrata questa “strana” forma? Il motivo è semplice: la classe Punto2D non ha ridefinito il toString e quindi nell’oggetto Punto2D l’unico metodo toString esistente è quello “ereditato” dalla classe java.lang.Object.

Il toString di Object è implementato per fornire soltanto una stringa che ha la seguente forma generalizzata:

nome.qualificato.Classe@xxxxxxxx

Dove quel xxxxxxxx è l’esadecimale del valore restituito dal metodo hashCode che, se non ridefinito, è anch’esso quello di Object che si basa in qualche modo sull’indirizzo dell’oggetto.

La classe Object purtroppo non può fare altro, non “sa” nulla del concetto e del contenuto di una sottoclasse specifica e quindi non ha alcun appiglio per fornire una rappresentazione testuale più significativa degli oggetti. I progettisti di Java hanno scelto questa forma predefinita per il toString di Object, che però poche volte è utile (anzi, quasi mai, salvo casi davvero particolari).

Con l’oggetto Punto2D costruito nell’esempio, sarebbe molto più utile e pratico poter ottenere una rappresentazione in stringa del tipo ad esempio: Punto2D(x=15.0; y=22.0)

Per ottenere questo è sufficiente ridefinire correttamente il metodo toString nella nostra classe Punto2D. La ridefinizione di un metodo (“di istanza”, cioè non static) in una classe è il principio di overriding dei metodi in Java e ha delle regole ben precise.
Senza entrare nei dettagli del overriding (non è l’argomento principale di questo articolo), per il toString la forma deve restare quella riportata all’inizio, ovvero:

public String toString() { .....codice..... }

In sostanza:

  • deve chiamarsi esattamente toString (non può essere ToString, tostring o altro)
  • deve restare public (non può avere un altro livello di accesso es. private, ecc...)
  • deve restituire String (non può essere altro)
  • deve essere senza parametri (se inserite un toString con dei parametri state facendo un overloading invece di un overriding)
  • non deve dichiarare eccezioni checked (perché il toString di Object non ne dichiara)

Nota: si possono eventualmente aggiungere delle annotation, il modificatore final (se si vuole impedire la ridefinizione di toString in una ulteriore sottoclasse) e il modificatore synchronized (per la sincronizzazione tra thread). Ma per il resto la forma base deve restare quella indicata sopra.

Mettiamo quindi un toString appropriato nella classe Punto2D:

public class Punto2D {

    // ... il resto esattamente come prima ...

    @Override
    public String toString() {
        return "Punto2D(x=" + getX() + "; y=" + getY() + ")";
    }
}

A livello basilare è sufficiente restituire una stringa composta sfruttando la concatenazione delle stringhe. Ma si possono usare anche altre tecniche più o meno sofisticate (a seconda delle necessità e del numero di attributi da elencare).

Parlando in generale, non esiste una forma “standard” per la rappresentazione di un oggetto. Dipende molto dal significato della classe, dal suo contenuto e soprattutto da dove si intende usare principalmente la rappresentazione testuale (es. in un componente grafico, in un file di log, per debugging, ecc...)

Ci sono comunque delle forme abbastanza comuni e ricorrenti. Il framework standard di Java adotta sovente una forma come quella mostrata qui di seguito per un oggetto Rectangle, ovvero ad esempio:

java.awt.Rectangle[x=10,y=20,width=80,height=50]

Ricompilando la classe Punto2D dopo aver aggiunto il toString e riprovando quindi la classe ProvaStampaPunto2D il risultato è la seguente stampa:

p1 vale: Punto2D(x=15.0; y=22.0)

Che è appunto quanto era stato proposto prima.

Conclusioni

In questo articolo ho descritto principalmente il concetto e la ridefinizione del metodo toString. Si potrebbero ancora dire diverse altre cose, ad esempio come implementare un “buon” toString oppure come sfruttare librerie esterne che offrono delle utilità specifiche per il toString. Questo potrà sicuramente essere lo spunto per ulteriori articoli.