Cadena vs StringBuilder vs StringBuffer en Java

Una de las clases más utilizadas en Java es la clase String. Representa una cadena (matriz) de caracteres y, por lo tanto, contiene datos textuales como "Hola...

Introducción

Una de las clases más utilizadas en Java es la clase String. Representa una cadena (matriz) de caracteres y, por lo tanto, contiene datos textuales como "¡Hola mundo!". Además de la clase String, hay otras dos clases que se usan para fines similares, aunque no con tanta frecuencia: StringBuilder y StringBuffer.

Cada uno existe por su propia razón y, sin darse cuenta de los beneficios de las otras clases, muchos programadores novatos solo usan cadenas, lo que reduce el rendimiento y la escalabilidad.

Cuerda

Inicializar una cadena es tan fácil como:

1
String string = "Hello World!";

Es atípico, como en todos los demás casos, crearíamos una instancia de un objeto usando la palabra clave nuevo, mientras que aquí tenemos una versión "acceso directo".

Hay varias formas de instanciar cadenas:

1
2
3
4
5
6
7
8
9
// Most common, short way
String str1 = "Hello World";

// Using the `new` keyword and passing text to the constructor
String str2 = new String("Hello World");

// Initializing an array of characters and assigning them to a String
char[] charArray = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
String str3 = new String(charArray);

Echemos un vistazo al código fuente de la clase y hagamos algunas observaciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /**
     * Initializes a newly created {@code String} object so that it represents
     * an empty character sequence.  Note that use of this constructor is
     * unnecessary since Strings are immutable.
     */
    public String() {
        this.value = new char[0];
    }

    /**
     * Allocates a new {@code String} so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     * @param  value
     *         The initial value of the string
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    
    ...
}

Primero podemos observar cómo se guarda el texto en sí mismo, en una matriz char. Dicho esto, es lógico que podamos formar una cadena a partir de una matriz de caracteres.

Una cosa realmente importante a tener en cuenta aquí es el hecho de que String se define como final. Esto significa que String es inmutable.

¿Qué significa esto?

1
2
3
String str1 = "Hello World!";
str1.substring(1,4).concat("abc").toLowerCase().trim().replace('a', 'b');
System.out.println(str1);

Producción:

1
Hello World!

Dado que String es definitivo, ninguno de estos métodos realmente lo cambió. Simplemente devolvieron el estado modificado que no usamos ni asignamos en ninguna parte. Cada vez que se llama a un método en una cadena, se crea una nueva cadena, se cambia el estado y se devuelve.

De nuevo, echando un vistazo al código fuente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

El str original nunca se cambia. Se copia su valor y se le añade el texto que concatenamos, tras lo cual se devuelve un nuevo String.

Si hiciéramos algo como esto:

1
2
3
String str1 = "Hello World!";
String str2 = str1.substring(1,4).concat("abc").toLowerCase().trim().replace('a', 'b');
System.out.println(str2);

Entonces seríamos recibidos con la salida:

1
ellbbc

Ahora, echemos un vistazo a estas dos cadenas:

1
2
String str1 = "qwerty";
String str2 = "qwerty";

Cuando instanciamos una String como esta, el valor, en este caso qwerty, se guarda en Java Heap Memory, que se utiliza para la asignación de memoria dinámica para todos los objetos de Java.

Si bien tenemos dos variables de referencia diferentes en este ejemplo, ambas se refieren a una sola ubicación de memoria en la memoria del montón de Java. Si bien puede parecer que hay dos objetos String diferentes, en realidad solo hay uno: str2 nunca se instancia como un objeto, sino que se le asigna el objeto en la memoria que corresponde a str1.

Esto sucede debido a la forma en que se optimizó Java para cadenas. Cada vez que desea crear una instancia de un objeto String como este, el valor que desea agregar a la memoria del montón se compara con los valores agregados anteriormente. Si ya existe un valor igual, el objeto no se inicializa y el valor se asigna a la variable de referencia.

Estos valores se guardan en el denominado String Pool, que contiene todos los valores de String literales. Sin embargo, hay una manera de evitar esto: usando la palabra clave nuevo.

Veamos otro ejemplo:

1
2
3
4
5
6
7
8
String str1 = "qwerty";
String str2 = "qwerty";
String str3 = new String("qwerty");

System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str1.equals(str2));
System.out.println(str1.equals(str3));

Producción:

1
2
3
4
true
false
true
true

Esto es lógico, ya que str1 y str2 apuntan al mismo objeto en la memoria. str3 se instancia explícitamente como nuevo, por lo que se crea un nuevo objeto para él, aunque el literal de cadena ya existe en el grupo. El método equals() compara sus valores, no los objetos a los que apuntan, razón por la cual devuelve true para todas estas cadenas.

Es importante tener en cuenta que los métodos substring() y concat() devuelven un nuevo objeto String y lo guardan en el grupo de cadenas.

Este es un fragmento de código muy pequeño, pero si consideramos algunos proyectos grandes que usan cientos de variables String y miles de operaciones como substring() o concat(), puede causar graves pérdidas de memoria y retrasos de tiempo. Es exactamente por eso que queremos usar StringBuffer o StringBuilder.

StringBuffer y StringBuilder

Mutabilidad

Los objetos StringBuffer y StringBuilder básicamente tienen el mismo valor que un objeto String: una secuencia de caracteres. Tanto StringBuffer como StringBuilder también son mutables, lo que significa que una vez que les asignamos un valor, ese valor se procesa como un atributo de un objeto StringBuffer o StringBuilder.

No importa cuántas veces modifiquemos su valor, como resultado, no se creará un nuevo objeto String, StringBuffer o StringBuilder. Este enfoque es mucho más eficiente en tiempo y consume menos recursos.

StringBuilder frente a StringBuffer

Estas dos clases son casi idénticas entre sí: utilizan métodos con los mismos nombres que devuelven los mismos resultados. Aunque hay dos grandes diferencias entre ellos:

  • Seguridad de subprocesos: Los métodos de StringBuffer están sincronizados, lo que significa que solo un subproceso puede llamar a los métodos de una instancia de StringBuffer a la vez. Por otro lado, los métodos StringBuilder no están sincronizados, por lo tanto, varios subprocesos pueden llamar a los métodos en la clase StringBuilder sin ser bloqueados.

    So we have come to a conclusion that StringBuffer is a thread-safe class while StringBuffer isn't.

    Is that something you should worry about? Maybe. If you are working on application which uses multiple threads it can be potentially dangerous to work with StringBuilder.

  • Velocidad: StringBuffer es en realidad dos o tres veces más lento que StringBuilder. La razón detrás de esto es la sincronización StringBuffer: solo permitir que 1 subproceso se ejecute en un objeto a la vez da como resultado una ejecución de código mucho más lenta.

Métodos

Tanto StringBuffer como StringBuilder tienen los mismos métodos (además de la declaración del método sincronizado en la clase StringBuilder). Repasemos algunos de los más comunes:

  • añadir()
  • insertar()
  • reemplazar()
  • eliminar()
  • reversa()

Como puede ver, el nombre de cada método describe bastante bien lo que hace. Aquí hay una demostración simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
StringBuffer sb1 = new StringBuffer("Buffer no 1");
System.out.println(sb1);
        
sb1.append(" - and this is appended!");
System.out.println(sb1);
sb1.insert(11, ", this is inserted"); 
System.out.println(sb1);
sb1.replace(7, 9, "Number"); 
System.out.println(sb1);
sb1.delete(7, 14);
System.out.println(sb1);
sb1.reverse();
System.out.println(sb1);

Producción:

1
2
3
4
5
6
Buffer no 1
Buffer no 1 - and this is appended!
Buffer no 1, this is inserted - and this is appended!
Buffer Number 1, this is inserted - and this is appended!
Buffer 1, this is inserted - and this is appended!
!dedneppa si siht dna - detresni si siht ,1 reffuB

Cadena frente a StringBuilder frente a StringBuffer


                    String   StringBuffer   StringBuilder

Mutable No Sí Sí Seguro para subprocesos Sí Sí No Tiempo eficiente No No Sí Memoria eficiente No Sí Sí


Nota: Como podemos ver en la tabla anterior, String es menos eficiente tanto en tiempo como en memoria, pero eso no significa que nunca debamos volver a usarlo.

De hecho, String puede ser muy útil de usar porque se puede escribir rápidamente y si alguna vez desarrolla una aplicación que almacena cadenas que no se van a manipular/cambiar más tarde, está absolutamente bien usar String .

Ejemplo de código

Para mostrar qué tan eficientes son String, StringBuffer y StringBuilder vamos a realizar una prueba comparativa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
String concatString = "concatString";
StringBuffer appendBuffer = new StringBuffer("appendBuffer");
StringBuilder appendBuilder = new StringBuilder("appendBuilder");
long timerStarted;

timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
    concatString += " another string";
}
System.out.println("Time needed for 50000 String concatenations: " + (System.currentTimeMillis() - timerStarted) + "ms");

timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
    appendBuffer.append(" another string");
}
System.out.println("Time needed for 50000 StringBuffer appends: " + (System.currentTimeMillis() - timerStarted) + "ms");
        
timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
    appendBuilder.append(" another string");
}
System.out.println("Time needed for 50000 StringBuilder appends: " + (System.currentTimeMillis() - timerStarted) + "ms");

Producción:

1
2
3
Time needed for 50000 String concatenations: 18108ms
Time needed for 50000 StringBuffer appends: 7ms
Time needed for 50000 StringBuilder appends: 3ms

Esta salida puede variar según su máquina virtual Java. Entonces, a partir de esta prueba comparativa, podemos ver que StringBuilder es el más rápido en la manipulación de cadenas. El siguiente es StringBuffer, que es entre dos y tres veces más lento que StringBuilder. Y finalmente tenemos String que es, con mucho, el más lento en la manipulación de cadenas.

El uso de StringBuilder resultó en un tiempo ~6000 veces más rápido que el de String's normal. Lo que le tomaría a StringBuilder concatenar en 1 segundo le tomaría a String 1.6 horas (si pudiéramos concatenar tanto).

Conclusión

Hemos visto el rendimiento de Strings, StringBuffers y StringBuilders, así como sus ventajas y desventajas. Ahora, surge la pregunta final:

¿Cuál es el ganador?

Bueno, la respuesta perfecta a esta pregunta es "Depende". Sabemos que las Strings son fáciles de escribir, fáciles de usar y son seguras para subprocesos. Por otro lado, son inmutables (lo que significa un mayor consumo de memoria) y muy lentos cuando se manipulan cadenas.

StringBuffers son mutables, eficientes en memoria y seguros para subprocesos. Su desventaja es la velocidad en comparación con StringBuilders mucho más rápidos.

En cuanto a StringBuilders, también son mutables y eficientes en memoria, son los más rápidos en la manipulación de cadenas, pero desafortunadamente no son seguros para subprocesos.

Si tiene en cuenta estos hechos, ¡siempre tomará la decisión correcta!

Licensed under CC BY-NC-SA 4.0