Java OCAJP7: Tutto su String, StringBuilder, uguaglianza fra stringhe e Java String Pool

In Java le stringhe possono essere rappresentate tramite oggetti della classe String oppure oggetti delle classi StringBuilder e StringBuffer. La differenza tra queste ultime due risiede nel fatto che StringBuilder non è thread-safe mentre StringBuffer lo è. StringBuilder offre quindi performance migliori in ambienti single-threaded perchè non comporta l’overhead introdotto dalle operazioni di sincronizzazione ed, in generale, nella maggioranza dei casi è da preferire a StringBuffer. Per tale motivo, e per il fatto che è la sola tra le due che compare nell’esame OCAJP, nel seguito dell’articolo prenderemo in considerazione solo la classe StringBuilder ed analizzeremo le sue differenze con la classe String in termini di funzionamento e di comportamento rispetto ai test di uguaglianza su oggetti della classe.
La differenza principale tra String e StringBuilder è che gli oggetti della classe String sono immutabili mentre quelli della classe StringBuilder no. Il fatto che gli oggetti della classe String siano immutabili significa che una volta creati il loro valore rimane invariato ed ogni volta che vengono effettuate delle operazioni su di essi, volte a modificarne il valore (replace, concat, ecc..), quello che in realtà avviene è che viene creato un altro oggetto contenente il nuovo valore che viene referenziato dalla precedente reference. Un’operazione di modifica del valore di uno StringBuilder viene invece effettuata realmente sullo stesso oggetto senza che ne vengano creati e referenziati altri.

Ma andiamo con ordine.

Gli oggetti String possono essere creati in due modi:
– Tramite l’operatore new: String s1 = new String(“aaa”);
– Con la sintassi abbreviata di inizializzazione tramite il simbolo ‘=‘ ed il valore racchiuso tra ‘‘: String s2 = “bbb”;

Queste due modalità, sebbene possano sembrare simili, nascondono in realtà una grossa differenza: utilizzando l’operatore new viene creato ogni volta un nuovo oggetto, mentre quando le stringhe vengono dichiarate con la sintassi abbreviata, esse vengono inserite nel cosiddetto string pool ed ogni volta che viene utilizzata una stringa con lo stesso valore viene prima controllato se tale stringa è già presente nel pool e, in tal caso, viene referenziata senza creare un nuovo oggetto. Con questo meccanismo la JVM implementa il cosiddetto string interning e permette di risparmiare memoria a runtime evitando di creare più oggetti diversi che contengono uno stesso valore, mantenendone invece uno solo.

Da questo ne deriva che due stringhe con lo stesso valore, create utilizzando la sintassi abbreviata e quindi sfruttando lo string pool, rappresentano in realtà la stessa stringa e quindi il loro confronto con l’operatore ‘==’ restituisce true, mentre due stringhe con uguale valore ma create con l’operatore new rappresentano due oggetti diversi ed otterremo quindi ovviamente false se le confrontiamo con l’operatore ‘==’.
Vediamo nel dettaglio.

Confronto tra stringhe dello string pool:

public class JavaCertification {
    public static void main(String[] args) {
        String s1 = "aaa";
        String s2 = "aaa";
        System.out.println(s1 == s2);
    }
}

Il test con l’operatore ‘==’ in questo caso, come detto, ha esito positivo:
OUTPUT:

true

Lo scenario appena rappresentato può essere illustrato con la figura seguente, nella quale si nota che entrambe le stringhe referenziano lo stesso oggetto e che tale oggetto è allocato nella parte di heap riservata allo string pool (da Java7 lo string pool è stato spostato dall’area PermGen all’Heap, ma questo esula dalla discussione, per cui non approfondiremo la cosa). Quando viene richiesta la creazione di s2 con la sintassi abbreviata, la JVM controlla nello string pool se tale stringa è già presente. In questo caso lo è per cui s2 viene fatta riferire allo stesso oggetto referenziato da s1, senza creare un nuovo oggetto con lo stesso valore.

String Interning in String Pool

Adesso vediamo invece il caso in cui due stringhe con lo stesso valore vengano create utilizzando l’operatore new:

public class JavaCertification {
    public static void main(String[] args) {
        String s1 = new String("aaa");
        String s2 = new String("aaa");
        System.out.println(s1 == s2);
    }
}

In questo caso, ad ogni invocazione di new, viene creato un nuovo oggetto, per cui il confronto con ‘==’ tra le due stringhe restituisce ovviamente false.
OUTPUT:

false

Le due stringhe inoltre vengono allocate nell’heap, esternamente all’area riservata allo string pool. L’immagine seguente, ancora una volta, illustra graficamente quanto accade in memoria:

New String Objects in Heap memory

L’uguaglianza su oggetti della classe String creati con l’operatore new deve essere testata tramite il metodo equals() che è appunto ridefinito su tale classe in modo da considerare uguali due istanze che hanno lo stesso valore della stringa stessa.

public class JavaCertification {
    public static void main(String[] args) {
        String s1 = new String("aaa");
        String s2 = new String("aaa");
        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
    }
}

In questo caso le stringhe s1 ed s2 costituiscono due oggetti diversi ma con lo stesso valore, per cui il confronto con l’operatore ‘==’ restituisce false mentre il confronto tramite il metodo equals() restituisce true.

L’ultimo caso simile che ci resta da analizzare è quello in cui creiamo una stringa con l’operatore new ed un’altra, con lo stesso valore, con la sintassi abbreviata che la inserisce nello string pool.

			
public class JavaCertification {
    public static void main(String[] args) {
        String s1 = "aaa";
        String s2 = new String("aaa");
        System.out.println(s1 == s2);
    }
}

OUTPUT:

false

Il risultato, anche in questo caso, è piuttosto prevedibile. Ogni invocazione dell’operatore new crea un nuovo oggetto diverso, per cui le due reference si riferiranno inevitabilmente a due oggetti diversi e l’operatore ‘==’ restituirà inevitabilmente false.
L’immagine seguente descrive nuovamente la situazione in memoria.

String Pool vs. new String object

La classe String mette a disposizione anche un metodo intern() che, quando invocato su un oggetto stringa, controlla nel pool di stringhe se una stringa con lo stesso valore (determinata tramite un confronto con il metodo equals) è già presente nel pool. In questo caso la stringa contenuta nel pool viene restituita. Nel caso in cui invece una stringa con lo stesso valore di quella su cui viene invocato il metodo intern() non sia già presente nel pool, allora essa viene aggiunta e viene restituito il riferimento ad essa.

public class JavaCertification {
    public static void main(String[] args) {
        String s1 = new String("aaa");
        s1 = s1.intern();
    	String s2 = "aaa";
        System.out.println(s1 == s2);
    }
}

OUTPUT:

true

String object interning

Dopo aver visto le modalità di creazione di oggetti di tipo String ed il funzionamento dello string pool, riprendiamo anche il discorso sulla classe StringBuilder. Come abbiamo anticipato prima, la differenza fondamentale tra String e StringBuilder è che gli oggetti della prima classe sono oggetti immutabili, mentre quelli della seconda no.

Vediamo con un semplice esempio questa differenza:

public class JavaCertification {
    public static void main(String[] args) {
        String s1 = new String("aaa");
        s1.concat("-bbb");
        
        StringBuilder sb1 = new StringBuilder("ccc");
        sb1.append("-ddd");
        
        System.out.println(s1);
        System.out.println(sb1);
    }
}

Ecco l’output prodotto dal programma:

OUTPUT:

aaa
ccc-ddd

Come si può vedere da questo risultato, eseguendo un’operazione di modifica su un oggetto String si perde tale modifica se non si assegna il risultato restituito dall’invocazione del metodo ad una reference (la stessa od una nuova). Questo perchè, quello che succede in realtà è che viene creato in memoria un nuovo oggetto String, contenente il valore “aaa-bbb” che però non viene assegnato a nessuna variabile, per cui si perde ogni riferimento ad esso. Tale oggetto diventa quindi immediatamente candidato per la Garbage Collection. La stringa originale s1, resta immutata, e mantiene il suo valore “aaa” che viene infatti stampato in output.
Nel caso dell’oggetto di tipo StringBuilder invece, le modifiche vengono effettuate proprio sull’oggetto originale che cambia quindi il suo valore senza che vengano creati nuovi oggetti. Essendo la modifica effettuata sull’oggetto stesso, non è necessario riassegnare l’oggetto modificato ad un nuova reference per mantenere il nuovo valore. Come vediamo dal risultato precedente, la stampa del valore dello StringBuilder ci fornisce il valore modificato con la seconda parte aggiunta tramite il metodo append.
Su questo aspetto occorre prestare molta attenzione durante l’esame OCAJP7 ed identificare correttamente i valori stampati in output dagli esempi di codice in cui vengono modificati i valori di oggetti String e StringBuilder e, per gli oggetti String, prestare attenzione a quando il nuovo oggetto creato dall’operazione di modifica viene riassegnato o meno.

Continuando a parlare della classe StringBuilder ci sono altre cose importanti da sottolineare e ricordare per l’esame OCAJP7. La prima è che gli oggetti StringBuilder possono essere creati solo tramite l’operatore new. Non si possono creare tramite la sintassi abbreviata per cui, un assegnamento come il seguente deve far subito pensare ad un errore di compilazione:

public class JavaCertification {
    public static void main(String[] args) {
        StringBuilder sb1 = "aaaa";
        System.out.println(sb1);
    }
}

Provando a compilare il codice qui sopra si ottiene infatti il seguente risultato:

JavaCertification.java:3: error: incompatible types
        StringBuilder sb1 = "aaaa";
                            ^
  required: StringBuilder
  found:    String

Il fatto che oggetti StringBuilder siano sempre creati tramite operatore new significa inoltre che il confronto tramite l’operatore ‘==’ restituirà sempre “false”, essendo inevitabilmente sempre oggetti diversi.

L’altra cosa fondamentale da ricordare è che, a differenza della classe String, sulla classe StringBuilder il metodo equals() non è ridefinito. Questo significa che il confronto tramite il metodo equals() su due oggetti StringBuilder diversi che contengono lo stesso valore restituisce false.

public class JavaCertification {
    public static void main(String[] args) {
        StringBuilder sb1 = new StringBuilder("aaa");
        StringBuilder sb2 = new StringBuilder("aaa");
        System.out.println(sb1.equals(sb2));
    }
}

OUTPUT:

false

Un’ulteriore casistica che vale la pena considerare, in modo da evitare ogni possibile confusione, è quella del confronto tramite il metodo equals tra oggetti della classe String e oggetti della classe StringBuilder. Il metodo equals, nella sua valutazione, controlla per prima cosa che gli oggetti che sta confrontando siano della stessa classe e poi passa ad analizzare il valore degli oggetti stessi. Da questo ne deriva il fatto che il confronto tra una String ed uno StringBuilder con lo stesso valore restituirà sempre e comunque false.

public class JavaCertification {
        public static void main(String[] args) {
        	String s1 = new String("aaa");
        	StringBuilder sb1 = new StringBuilder("aaa");
        	System.out.println(s1.equals(sb1));
        	System.out.println(sb1.equals(s1));
      }

}

OUTPUT:

false
false

Per poter effettuare un confronto tra il valore di una String e quello di uno StringBuilder, occorre trasformare il valore di quest’ultimo a sua volta in una String tramite il metodo toString().

public class JavaCertification {
        public static void main(String[] args) {
        	String s1 = new String("aaa");
        	StringBuilder sb1 = new StringBuilder("aaa");
        	System.out.println(s1.equals(sb1.toString()));
      }
}

OUTPUT:

true

Il programma seguente offre un riepilogo di quanto discusso in modo dettagliato all’interno dell’articolo e delle varie casistiche di verifica dell’uguaglianza tra oggetti di tipo String e StringBuilder

public class JavaCertification {
        public static void main(String[] args) {

                String s1 = new String("aaa");
                String s2 = new String("aaa");

                String s3 = "bbb";
                String s4 = "bbb";

                StringBuilder sb1 = new StringBuilder("zzz");
                StringBuilder sb2 = new StringBuilder("zzz");

                System.out.println("1) String == String -> " + (s1 == s2));
                System.out.println("2) String == String [from string pool]-> " + (s3 == s4));

                System.out.println("3) StringBuilder == StringBuilder -> " + (sb1 == sb2));

                // equals sulle stringhe è ridefinito
                System.out.println("4) String.equals(String) -> " + s1.equals(s2));

                // equals per la classe StringBuilder NON è ridefinito
                System.out.println("5) StringBuilder.equals(StringBuilder) -> " + sb1.equals(sb2));

                // equals non vale per String e StringBuilder con lo stesso valore
                StringBuilder sb3 = new StringBuilder("aaa");
                System.out.println("6) String.equals(StringBuilder) -> " + s1.equals(sb3));
                System.out.println("7) StringBuilder.equals(String) -> " + sb3.equals(s1));

                // equals e' vero  per String e StringBuilder invocando toString su quest'ultimo
                System.out.println("8) String.equals(StringBuilder.toString()) -> " + s1.equals(sb3.toString()));
        }

}

OUTPUT:

1) String == String -> false
2) String == String [from string pool]-> true
3) StringBuilder == StringBuilder -> false
4) String.equals(String) -> true
5) StringBuilder.equals(StringBuilder) -> false
6) String.equals(StringBuilder) -> false
7) StringBuilder.equals(String) -> false
8) String.equals(StringBuilder.toString()) -> true

Leave a Reply

Your email address will not be published. Required fields are marked *