String, StringBuilder y StringBuffer en Java

Buenos días, a continuación vamos a ver en que consisten las clases String, StringBuffer y StringBuilder. Sun añadió la clase StringBuilder a la API de Java, para proveer una función mas rápida que el StringBuffer.


  • La Clase String

    En esta sección cubriremos la clase String, y el concepto clave para entender que una vez que un objeto String es creado, no puede ser cambiado, es decir, lo que está pasando cuando parece que un objeto String está cambiando.

    • Los objetos String son Inmutables

      Vamos a empezar con un poco mas de información sobre los strings. Puede que no lo necesitemos, pero un poco de contexto nos ayudará. El manejo de los caracteres de los “strings” es un aspecto fundamental en la mayoría de los lenguajes de programación. En Java, cada caracter en un string es un caracter de 16-bit Unicode. Ya que los caracteres Unicode son de 16 bits, se nos presenta un rico set de caracteres internacionales que son fácilmente representados en Unicode.

      En Java, los strings son objetos. Al igual que otros objetos, podemos crear una instancia de un String con la palabra clave new, como en el ejemplo siguiente.:

      				String s = new String();
      				

      Esta línea de código crea un nuevo objeto de la clase String, y le asigna la variable de referencia s. A la vista, el objeto String parece igual que cualquier otro objeto. Ahora, vamos a darle al String un valor:

      				s = "abcdef";
      				

      Como podemos esperar, la clase String tiene muchísimos constructores, por lo que podemos usar un atajo mas eficiente:

      				String s = new String("abcdef");
      				

      Y ya que vamos a usar strings todo el tiempo, podemos incluso decir esto:

      				String s = "abcdef";
      				

      Hay algunas diferencias entre estas opciones que veremos despues, pero lo que tienen en común es que todos ellos crean un nuevo objeto String, con el valor “abcdef”, y lo asigna a la variable de referencia s. Ahora vamos a crear una nueva referencia a un objeto String que se refiere a s:

      				String s2 = s;
      				

      Viendo estos ejemplos, los objetos String parecen tener un mismo comportamiento al igual que cualquier otro objeto, por lo que…¿Qué pasa con la inmutabilidad?¿Que es la INmutabilidad?. Una vez que hemos asignado a un String un valor, ese valor no puede ser cambiado nunca, es inmutable. La sbuenas noticias es que mientras que el objeto String es inmutable, su variable de referencia no lo es, por lo que vamos a continuar con el siguiente ejemplo:

      				s = s.concat(" more stuff"); // Este método concatena un literal al final
      				

      Ahora vamos a esperar un minutno, ¿No habíamos dicho que los Strings eran inmutables?¿Qué pasa cuando estamos añadiendo un literal al final del string? Son unas excelentes cuestiones, vamos a ver lo que en realidad está pasando.

      La VM coge el valor de la String s (la cual es “abcdef”), y añadiendo ” more stuff” al final, dandonos el valor “abcdef more stuff”. Ya que los Strings son inmutables, la VM no podría guardar este nuevo valor en la antigua referencia a la String en s, por lo que crea un nuevo objeto String, dandole el valor “abcdef more stuff”, y hace que s se refiera a el. En este punto de nuestro ejemplo, tenemos 2 objetos String: el primero que fue creado, con el valor “abcdef”, y otro segundo con el valor “abcdef more stuff”. Técnicamente ahora hay 2 objetos String, porque el argumento literal a concatenar, ” more stuff”, es por sí mismo otro objeto String. Pero solo tenemos referencias a “abcdef” (referenciado por s2) y “abcdef more stuff” (referenciado por s).

      ¿Qué pasaría si tuvieramos la suerte de crear una segunda variable d ereferencia para la String “abcdef” después de haber llamado a s = s.concat(” more stuff”);? En este caso, el original y sin cambiar String que contiene “abcdef” seguiría residiendo en memoría, pero se consideraría “perdido”. Ningún código en nuestro programa tiene ninguna manera de referenciarlo, estaría perdido para nosotros. Sin embargo, el String original “abcdef” no cambiaría (no puede, recordemos que son inmutables); solo la variable de referencia s fué cambiada, por lo que se referiría a un String diferente.

      Vamos a ver un repaso a nuestro anterior ejemplo:

      				String s = "abcdef"; 	// crea un nuevo objeto String, con
      										// el valor "abcdef", refiere s a el
      				String s2 = s;			// crea una segunda variable de referencia
      										// se refiere al mismo String
      				// Ahora creamos un nuevo objeto String, con el valor "abcdef more stuff"
      				// y s se refiere a el.(Cambia la referencia a s de la antigua String)
      				// (Recordemos que s2 se sigue refiriendo al String original "abcdef")
      				s = s.concat(" more stuff");
      				

      Vamos a ver otro ejemplo:

      				String x = "Java";
      				x.concat(" Rules!");
      				System.out.println("x = " + x); // La salida es "x = Java"
      				

      La primera linea es facil: crea un nuevo objeto String, le asigna el valor “jaa”, y refiere x a el. A continuación, la VM crea un segundo objeto Strin con el valor “Java Rules!” pero no lo refiere a el. El segundo objeto String se pierde inmediatamente; no podemos acceder a el. La variable de referencia x sigue refiriendose al String original con el valor “Java”.

      Vamos a expandir este actual ejemplo. Hemos empezado con:

      				String x = "Java";
      				x.concat(" Rules!");
      				System.out.println("x = " + x); // La salida es "x = Java"
      				

      Ahora añadimos:

      				x.toUpperCase();
      				System.out.println("x = " + x); // La salida sigue siendo x = Java
      				

      Actualmente creamos un nuevo objeto String con el valor “JAVA”, pero se pierde, y x sigue refiriendose al original, “Java”. Que pasa si añadimos:

      				x.replace('a', 'X');
      				System.out.println("x = " + x); // La salida sigue siendo x = Java
      				

      ¿Podemos determinar qué ha pasado? La VM ha creado de nuevo un nuevo objeto String, con el valor “JXvX, (reemplazando las a con X), pero de nuevo esta String se ha perdido, dejando que x se refiera aún a la String sin cambiar sigue sin poder cambiarse, con el valor “Java”. En todos estos casos hemos llamado a varios métodos de String para crear un nuevo String alterando el String existente, pero nunca hemos asignado la nueva String creada a una variable de referencia.

      Vamos a poner algo más a nuestro anterior ejemplo:

      				String x = "Java";
      				x = x.concat(" Rules!");			// Ahora estamos asignando
      													// una nueva String a x
      				System.out.println("x = " + x);		// La salida será: x = Java Rules!
      				

      Esta vez, cuando la VM ejecuta la segunda línea, un nuevo objeto String es creado con el valor “Java Rules!”, y x es una referencia a el. Pero esperemos, hay más, ahora el objeto original String, “Java”, se ha perdido, y nada está refiriendose a el. Por lo que en ambos ejemplos hemos creado 2 objetos String y solo una variable de referencia, por lo que solo uno de los dos objetos String se ha quedado fuera.

      				String x = "Java";
      				x = x.concat(" Rules!");
      				System.out.println("x = " + x);		// La salida es x = Java Rules!
      				
      				x.toLowerCase();					// No hay asignación, crea un nuevo String
      													// pero que ha sido abandonado
      				
      				System.out.println("x = " + x);		// No hay asignación, la salida sigue siendo
      													// x = Java Rules!
      				
      				x = x.toLowerCase();				// Crea un nuevo String, y le asigna x
      				
      				System.out.println("x = " + x);		// La asignación causa la salida x = java rules!				
      				

      En el ejemplo anterior contiene las claves para entender la inmutabilidad de los String en Java. Si captamos bien estos ejemplos, tendremos bien sabido al menos el 80% del comportamiento de los String en Java.

      Vamos a terminar esta sección presentando un ejemplo de un tipo de pregunta sobre los String con bastante truco:

      				String s1 = "spring ";
      				String s2 = s1 + "summer ";
      				s1.concat("fall ");
      				s2.concat(s1);
      				s1 += "winter ";
      				System.out.println(s1 + " " + s2);
      				

      ¿Cuál será la salida?¿Cuántos objetos String y cuatas variables de referencia han sido creadas antes de llegar a la sentencia println?

      La respuesta es la siguiente:

      El resultado de este fragmento de código es “spring winter spring summer”. Hay 2 variables de referencia, s1 y s2. Había un total de 8 objetos String creados: “spring”, “summer” (perdido), “spring summer”, “fall” (perdido), “spring fall” (perdido), “spring summer spring” (perdido), “winter” (lost), “spring winter” (en este punto “spring” se ha perdido). Solo 2 de los 8 objetos String no se han perdido en todo el proceso.

    • Factores Importantes sobre Strings y Memoria

      En esta sección discutiremos como Java maneja los objetos Strings en memoria, y algunas razones sobre sus comportamientos.

      Uno de los éxitos clave de cualquier lenguaje de programación es hacer un uso eficiente de la memoria. Cuando un aplicación crece, es muy común que losliterales String ocupen un gran montón de memoria del programa, y a veces hay mucha redundancia dentro del universo de los literales String en un programa. Para hacer que Java tenga un uso eficiente de la memoria, la JVM establece una area especial de memoria llamada “String constant pool”. Cuando el compilador encuentra un literal String, se comprueba si en el pool hay algún String idéntico que ya exista. Si se encuentra algun String igual, entonces el nuevo literal es redirigido al String existente, y no se crea ningún literal String mas. Ahora podemos empezar a ver porque hacer que los objetos String sean inmutables es una buena idea. Si una cantidad severa de variables de referencia se refieren al mismo String, sería una mala idea que cualquiera de ellos pudiera cambiar el valor del String.

      Podríamos decir “Bien esto todo está muy bien y bueno, pero ¿Qué pasa si alguien sustituye la funcionalidad de la clase String?” Esta es una de las razones principales por las que una clase String está marcada como final. Nadie puede sustituir los comportamientos de cualquiera de los métodos String, por lo que tenemosasegurado de que los objetos String en los que estamos contando son inmutables.

      • Creando Nuevos Strings

        Antes prometimos hablar sobre las diferencias entre los diferentes métodos de crear un String. Vamos a er un par de ejemplos en como los Strings pueden ser reados, y vamos a asumit que no existen otros objetos String en el pool:

        						String s = "abc"; // Crea un objeto String y una variable de referencia
        						

        En este caso simple, “abc” irá al pool y s se referirá a el:

        						String s = new String("abc");	// Crea 2 objetos, y una variable de referencia
        						

        En este segundo caso, como hemos usado la palabra clave new, Java creará un nuevo objeto String en memoria (no en el pool), y s se referirá ahora a el. En adición, el literal “abc” será puesto en el pool.

    • Métodos Importantes de la Clase String

      Los siguientes métodos son los mas comunes y los mas usados de la clase String:

      • charAt() – Retorna el caracter localizado en el índice especificado.
      • concat() – Concatena un String al final de otra (“+” tambien funciona).
      • equalsIgnoreCase() – Determina la igualdad de 2 Strings, ignorando el caso.
      • length() – Retorna el número de caracteres en un String.
      • replace() – Reemlaca las ocurrencias de un caracter por otro caracter.
      • substring() – Retorna una parte del String.
      • toLowerCase() – Retorna un String con las caracteres en mayúscula convertidos.
      • toString() – Retorna el valor de un String.
      • toUpperCase() – Retorna un String con los caracteres en minuscula convertidos.
      • trim() – Remueve los espacios en blanco al comienzo o final de un String.

      Vamos a ver un poco mas de estos métodos en detalle:

      public char charAt(int index)

      Este método retorna el caracter localizado en el index especificado en el String. Recordemos, los índices en los Strings comienzan por 0:

      				String x = "airplane";
      				System.out.println(x.charAt(2));	// La salida es 'r'
      				

      public String concat(String s)

      Este método retorna un String con el valor del String pasado en el método concatenado al final el String usado para invocar el método:

      				String x = "taxi";
      				System.out.println(x.concat(" cab"));	// La salida es "taxi cab"
      				

      Los operadores + y += sobrecargados realizan funciones similares el método concat():

      				String x = "library";
      				System.out.println(x + " card"); // La salida es "taxi cab"
      				
      				String x = "Atlantic";
      				x += " ocean";
      				System.out.println(x);	// La salida es "Atlantic ocean"
      				

      En el ejemplo anterior de “Atlantic ocean”, vemos que el valor de x realmente ha cambiado. Recordemos que el operador += es un operador de asignación, por lo que la linea 2 está realmente creando un nuevo String, “Atlantic ocean”, y asignandolo a la variable x. Antes de que la línea 2 se ejecute, el String original al que x se refería, “Atlantic”, es abandonado.

      public boolean equalsIgnoreCase(String s)

      Este método retorna un valor boolean (true o false) dependiendo en si el valor del String en el argumento es el mismo que el valor del String usado para invocar el método. Este método retorna true incluso cuando los caracteres en el objeto String que están siendo comparados tienen diferentes caracteres ya sea mayuscula o minuscula:

      				String x = "Exit";
      				System.out.println(x.equalsIgnoreCase("EXIT")); 	// Es true
      				System.out.println(x.equalsIgnoreCase("tixe"));		// Es false
      				

      public int length()

      Este método retorna la longitud de un String usado para invocar el método:

      				String x = "01234567";
      				System.out.println(x.length());	// Retorna 8
      				

      public String replace(char old, char new)

      Este método retorna un String cuyo valor es el String usado para invocar método, actualizado con cualquier ocurrencia de un caracter que se ha pasado como primer argumento reemplazado por el caracter que se ha pasado como segundo argumento:

      				String x = "oxoxoxox";
      				System.out.println(x.replace('x', 'X')); 	// La salida será "oXoXoXoX"
      				

      public String substring (int begin)

      public String substring (int begin, int end)

      EL método substring() es usado para retornar una parte de un String que se ha usado para invocar al método. El primer argumento representa lpor donde empieza. SI la llamada tiene solo un argumento, el substring que se retorna incluirá todos los caracteres hasta el final del String original. SI la llamada tiene 2 argumentos, el substring retornado terminará con el caracter que se encuentre en la posición pasada como segundo argumento. Desafortunadamente, el argumento de finalización no está basado en 0, por lo que si el segundo argumento es 7, el último caracter que se retornará del String será el que se encuentra en la posición 7 del String, el cual tiene como índice 6. Vamos a ver algún ejemplo:

      				String x = "0123456789";				// El valor de cada caracter
      														// es el mismo que el índice
      				System.out.println(x.substring(5));		// La salida es "56789"
      				System.out.println(x.substring(5, 8));	// La salida es "567"
      				

      El primer ejemplo debería ser facil: empieza en el índice 5 y retorna el resto del String. El segundo ejemplo se debería leer de la siguiente manera: empieza en el índice 5 y retorna el caracter incluyendo la 8ª posición (índice 7).

      public String toLowerCase()

      Este método retorna un String cuyo valor es el String usado para invocar el método, pero con cualquier caracter en mayúscula es convertido a minúscula:

      				String x = "A New Moon";
      				System.out.println(x.toLowerCase()); // La salida es "a new moon"
      				

      public String toString()

      Este método retorna como valor el String usado para invocar al método. ¿Qué?¿Por qué necesitaríamos un método en teoría no hace nada?. Todos los objetos en Java deben tener el método toString(), el cual retorna típicamente un String que de alguna manera significativamente describe el objeto en cuestión. En el caso de un objeto String:

      				String x = "big surprise";
      				System.out.println(x.toString());
      				

      public String toUpperCase()

      Este método retorna un String cuyo vlor es el String usado para invocar el método, pero con cualquier caracter en minúscula convertido a mayúscula:

      				String x = "A New Moon";
      				System.out.println(x.toUpperCase());	// La salida será "A NEW MOON"
      				

      public String trim()

      Este método retorna un String cuyo valor es un String usado para invocar al método, pero con cualquier espacio blanco al comienzo o al final del String eliminado:

      				String x = "     hi     ";
      				System.out.println(x + "x");		//El resultado es "     hi     x"
      				System.out.println(x.trim() + x);	// El resultado es "hix"
      				
    • Las clases StringBuffer y StingBuilder

      Las clases java.lang.StringBuffer y java.lang.StringBUilder deberían ser usados para hacer modificaciones de cadenas de caracteres. Como hemos discutido en la sección anterior, los objetos String son inmutables, por lo que si elegimos hacer manipulaciones con objetos String, terminaremos con un gran montón de objetos String en el pool. (Incluso aunque tengamos gigabytes de RAM, no es una buena idea malgastar memoria con objetos Strings en el pool)). De otra mano, los objetos del tipo StringBuffer y StringBuilder pueden ser modificados una y otra vez sin dejar atrás una gran cantidad de objetos String.

      • StringBuffer vs StringBuilder

        La clase StringBuilder fue añadida en Java 5. Tiene exactamente la misma API que la clase StringBuffer, excepto que StringBUilder no tiene métodos marcados como synchronized. Sun recomienda que usemos StringBuilder en vez de StringBuffer cuando sea posible porque StringBuilder se ejecutará mas rápido. Aparte de esto, cualquier cosa que digamos sobre los métodos de StringBuilder que sea verdad, tambien lo será para los métodos StringBuffer, y viceversa.

      • Usando StringBuilder y StringBuffer

        En la sección anterior, hemos visto como entender la inmutabilidad de los String con fragmentos como este:

        						String x = "abc";
        						x.concat("def");
        						System.out.println("x = " + x);	// La salida es "x = abc"
        						

        Ya que no se ha realizado ninguna asignación, el nuevo objeto String creado con el método concat() es abandonado instantaneamente. Tambien hemos visto ejemplos como el siguiente:

        						String x = "abc";
        						x = x.concat("def");
        						System.out.println("x = " + x);	// La salida es "x = abcdef"
        						

        Tenemos un nuevo String, pero sin embargo el String antiguo “abc” se ha perdido en el String pool, por lo tanto está malgastando memoria. Si estuvieramos usando StringBuffer en vez de String, el código sería como el siguiente:

        						StringBuffer sb = new StringBuffer("abc");
        						sb.append("def");
        						System.out.println("sb = " + sb);	// La salida será "sb = abcdef"
        						

        Todos los métodos de StringBuffer que discutiremos a continuación operan con el valor del objeto StringBuffer que ha invocado el método. Por lo que una llamada a sb.append(“def”); está actualmente concatenando “ef” así mismo (StringBuffer sb). Por tanto, estas llamadas a métodos pueden encadenarse unos a otros:

        						StringBuilder sb = new StringBuilder("abc");
        						sb.append("def").reverse().insert(3, "---");
        						System.out.println(sb);		// La salida es "fed---cba"
        						

        Vemos como en cada uno de los anteriores ejemplos, hay una única llamada a new, y en cada ejemplo no estamos creando ningún objeto extra. En cada ejemplo solo se significa un objeto StringXxx para ejecutarse.

    • Métodos Importantes de las Clases StringBuffer y StringBuilder

      Los siguientes métodos retornan un objeto StringXxx con el valor del argumento añadido al valor del objeto que ha invocado al método.

      public synchronized StringBuffer append(String s)

      Como hemos visto antes, este método actualizará el valor del objeto que ha invocado al método. Este método uede tener múltiples argumentos diferentes, incluyendo boolean, char, double, float, int, long, y otros:

      				StringBuffer sb = new StringBuffer("set ");
      				sb.append("point");
      				System.out.println(sb);		// La salida es "set point"
      				StringBuffer sb2 = new StringBuffer("pi = ");
      				sb2.append(3.14159ff);
      				System.out.println(sb2);	// La salida es "pi = 3.14159"
      				

      public StringBuilder delete(int start, int end)

      Este método retorna un objeto StringBuilder actualizado con el valor del objeto StringBuilder que ha invocado la llamada al método. En ambos casos, se elimina un substring del objeto original. El índice de comienzo de la cadena a ser eliminada es definido como primer argumento, y el índice final de la cadena a ser eliminada es definido por el segundo argumento. Estudiemos este ejemplo con cuidado:

      				StringBuilder sb = new StringBuilder("0123456789");
      				System.out.println(sb.delete(4, 6));	// La salida será "01236789"
      				

      public StringBuilder insert (int offset, String s)

      Este método retorna un objeto StringBuilder y que es actualizado por el objeto StringBuilder que ha invocado la llamada al método. En ambos casos, el String pasado en el segundo argumento es insertado en el StringBuilder original empezando en la localización representada por el primer argumento. De nuevo, otros tipos de datos pueden ser pasados como segundo argumento (boolean, char, double, float, int, long, etc), pero el segundo argumento es el que mas querremos estudiar:

      				StringBuilder sb = new StringBuilder("01234567");
      				sb.insert(4, "---");
      				System.out.println(sb);		// La salida es "0123---4567"
      				

      public synchronized StringBuffer reverse()

      Este método retorna un objeto StringBuffer que es actualizado con el valor del objeto StringBuffer que ha invocado la llamada al método. En ambos casos, los caracteres en el StringBuffer están al reverso, el primer caracter empieza al final, y el segundo a continuación:

      				StringBuffer s = new StringBuffer("A man a plan a canal Panama");
      				sb.reverse();
      				System.out.println(sb); 	// La salida es: "amanaP lanac a nalp a nam A"
      				

      public String toString()

      Este método retorna el valor del objeto StringBuffer que ha invocado la llamada al método como un String:

      				StringBuffer sb = new StringBuffer("test string");
      				System.out.println(sb.toString());		// La salida es "test string"
      				


Con esta entrada hemos podido ver como funciona la clase String en Java, sin duda una clase muy usada y que vamos a ver millones de veces, por lo que tenemos que entender como funciona internamente y que herramientas son las mas usadas y las mas comunes para trabajar con ella, y así poder dominar cualquier trabajo que tenga que ver con esta clase.

Sin mas, cualquier aporte o corrección es bienvenido.

Saludos!!!