String Java

Caracteres en Java
¿Qué es un caracter? Un caracter es una letra, un simbolo alfanumerico. Y el estándar internacional que define cuáles son las letras posibles en la computación se llama Unicode. Este estándar enumera todas las letras que se necesitan para todos los idiomas conocidos y le asigna un número, que representa un código, a cada una. La cantidad de letras Unicode es muy grande, y no alcanza un byte para implementar la idea de “caracter” en Java. Para hacer esto se usan dos bytes, lo que le da a Java la posibilidad de representar 65.536 caracteres.

El tipo nativo en Java es char, que puede almacenar un número entre 0 y 65.535. Una serie de caracteres entonces es una serie de estos números.
En un byte el valor máximo almacenable es 255.

Strings en Java
Un string en Java, es una secuencia de chars. Los objetos String en java son inmutables. Todos los métodos de manipulación de strings devuelven un nuevo objeto String. Por ejemplo str.trim() en otro lenguaje modificaría str, quitándole espacios por delante y por detrás. No es así en Java. Este código no hace lo que un programador naïve podría llegar a pensar:
 
void saludar(String str)
{
 str.trim();
 System.out.println("Hola, " + str + "!");
}

Lo que está pasando en este pedacito de codigo es que la línea del trim crea un nuevo String. Este nuevo string es desechado, ignorado, descartado, al no ser asignado a ninguna variable. Luego, se usa str, que es exactamente el mismo que fue pasado originalmente como parámetro (o sea que el trim termina no teniendo ningún efecto). El código correcto sería:
 
void saludar(String str)
{
 str = str.trim();
 System.out.println("Hola, " + str + "!");
}

¿Por qué a los diseñadores de Java se les ocurrió hacer algo tan raro? Es por eficiencia y seguridad. Supongamos que existe un método “change()” que sí modifica al objeto. Supongamos también que tenemos una clase nuestra, con un campo privado llamado “nombre”. No queremos permitir que nadie por fuera de la clase cambie el “nombre”. Por eso hacemos un método getNombre() que devuelve ese campo, y no hacemos ningún “setNombre()”. Ahora, si existe “change()”... ¿qué nos impide hacer o.getNombre().change("Jorge");? La única forma de evitar esto sería que getNombre() no devuelva el campo nombre, sino que construya un nuevo objeto, una copia, cada vez que se lo invoca. Eso sería más lento y ocuparía más memoria.
El diseño actual de los strings de Java permite que en un momento dado de la ejecución de una aplicación, un mismo objeto String pueda estar siendo referido desde cientos de clases que no tienen relación entre sí, y sin que esto represente una violación de “seguridad” para ninguna de ellas.
Todo esto no es una cuestión teórica, es necesario saberlo al programar en Java. Supongamos que tenemos todo el texto de una novela en una variable de tipo String. Cientos de miles de letras. Queremos añadirle “© 2008” al final:
 
novela = novela + "© 2008";

Ahora bien, el objeto novela original... es desechado! Su contenido es copiado a uno nuevo, que además tiene el texto añadido. Copiar cientos de miles de caracteres... no es algo muy eficiente. Bueno, si lo hacemos una sola vez puede que no nos demos cuenta... pero... si nos están envíando la novela letra por letra?
 
String novela = "";
 while(true)
 {
  char letra;
  letra = damePróximaLetraDeLaNovela();
  if( letra == null )
   break;
  novela = novela + letra; // auch!
 }

Imaginemos que este programa es una persona. Es un laborioso copiador de novelas. Muy laborioso. Cada vez que aparece una nueva letra que anotar... copia todo lo que había escrito hasta ese momento en otro lado, kuego añade la letra nueva y posteriormente quema todo su trabajo anterior. ¿Cuántas letras termina escribiendo? Para una novela de cuatro letras escribe primero una, luego dos, luego tres y por último escribe todas las cuatro letras. Para una novela de 100.000 letras escribe... 4.999.950.000 letras! (en general, para una novela de n letras escribe n⋅(n − 1) ⁄ 2 letras).

Una manera de solucionar esto sería ir armando la novela en un array común, un array de char. Claro que los arrays tienen un tamaño fijo, por lo que habría que ir copiando cada tanto el array cuando la capacidad anterior se ve alcanzada. Es decir, si primero estimamos que la novela tendrá 100 letras, al llenarse el array hay que crear uno nuevo de, por ejemplo 200 letras y copiar el contenido que ya tenía el viejo de 100.
Como se ve, es necesaria toda una lógica. Java tiene una clase que implementa internamente esta lógica. Una clase que encapsula, encierra, precisamente el manejo de un array que se va extendiendo, y en el que además podemos cambiar las letras del medio. Esta clase es StringBuilder, y debe considerársela como un laboratorio de Strings. Un StringBuilder es el útero en el que un String se puede ir gestando, creciendo, armándose. Al final, el método toString() nos da el String listo para consumo humano (o cibernético, bah).

El código que recibe la novela se vería entonces así:
 
StringBuilder sb = new StringBuilder();
 while(true)
 {
  char letra;
  letra = damePróximaLetraDeLaNovela();
  if( letra == null )
   break;
  sb.append(letra); // muy bien!!! =)
 }
 String novela = sb.toString();

Antes de que apareciera StringBuilder se usaba StringBuffer, la diferencia es que la segunda permite ser utilizada desde varios threads a la vez (cosa inútil que la hace más lenta).
Un detalle interesante extra: Hay algo más que Java puede hacer sólo porque los Strings son inmutables. Cuando obtenemos un “substring”, es decir, cuando extraemos una sección de un string, Java no copia las letras. El substring es un nuevo objeto sí, pero que apunta internamente al mismo array. Es decir que novela.substring(1000, 1010) da un String que tiene los caracteres del 1000 al 1009, pero para obtenerlo Java no copió las cientos de miles de letras! Esto no podría hacerse si el String original pudiera ser cambiado, ya que el cambio podría estar afectando a la sección.

Strings y otros objetos

Una de las características que define a un lenguaje de programación es como se relacionan los strings con el resto de los tipos. En algunos lenguajes hay una conversión automática, desde y hacia strings. En esos lenguajes un valor numérico se convierte siempre automáticamente en string cuando se necesita, y un string se hace número cuando se opera matemáticamente con él. En Java estas conversiones son muy restringidas.
Para los tipos fundamentales (int, float, char, boolean) Java genera atomáticamente una representación String. Es así como podemos escribir System.out.println("Tengo " + edad + " años.");. El camino inverso no se permite. Si se tiene una variable String edadStr, no se puede directamente compararla con 18 (un valor “entero”). Para convertir un string en número se usa la función Integer.parseInt(). Es lo mismo para cada uno de los tipos fundamentales (Float.parseFloat(), Boolean.parseBoolean(), etc).
Por otra parte, los objetos (es decir, todo lo que no es un tipo fundamental) proveen una manera estándar de “convertirse” a Strings. Muchas partes de Java que necesiten una versión String de un objeto invocarán al método toString(). Como este método está definido en Object, y todos los objetos extienden Object, este método está presente siempre. Muchas de las clases que vienen con Java ya implementan correctamente este método: La clase Date lo implementa devolviendo un string con la fecha, las clases que almacenan números (Integer, BigInteger, Float, etc.) devuelven el número convertido a string. La clase String también implementa el método toString()... y... obviamente... devuelve this =), es decir, se devuelve el mismo objeto sobre el que se invoca el método.

Si uno crea una nueva clase, muchísimas veces tiene sentido implementar correctamente un método toString. Por ejemplo, esta sería una implementación adecuada en una clase NúmeroComplejo:
 
class NúmeroComplejo
{
 double i, r;

 // más métodos, constructores, etc...
 
 public String toString()
 {
  return i + "i+" + r;
 }
}

No hay comentarios:

Publicar un comentario en la entrada