Strings en Java

Introducción

Cada programador en su camino al conocimiento Java sigue un camino distinto. Pero no importa qué camino hayas seguido: Si decís ser un programador sabés algo de “strings” (o cadenas, bah...). Y si decís que además sabés Java sabés algo de como es un String en Java. Los strings están diseminados por todos lados, y es exactamente por eso que a veces es difícil ver sus características distintivas y cómo son realmente distintos en los distintos lenguajes.

Considero a la información de este artículo como de nivel básico. Sin embargo me he encontrado con más de un programador “senior” que no sabe más de una de las cosas que voy a decir en este artículo. Así que creo que no está de más leerlo aunque parezca que no se lo necesita.

Caracteres en Java

En algunos lenguajes se confunde caracter con byte. Pero un byte es solamente la manera en la que en algunos lenguajes se implementa la noción de caracter. ¿Qué es un caracter entonces si no es un byte? Un caracter es una letra determinada. No es un número, no es un código, es una letra. 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, un código, a cada una. La cantidad de letras enumeradas por Unicode es enorme, y entonces no alcanza un byte para implementar la noción “caracter” en Java. Para hacerlo se usan dos bytes, lo que le da a Java la posibilidad de representar fácilmente 65.536 caracteres (aunque en verdad Unicode completo serían 4 bytes).

El tipo usado en Java para representar letras es char. Es equivalente a un tipo número sin signo de dos bytes, que puede almacenar un número entre 0 y 65.535. Una serie de caracteres entonces es una serie de estos números. Pero no se puede escapar de los bytes: La memoria de una computadora se suele direccionar en bytes, los protocolos de red llevan bytes de una PC a otra, los archivos son secuencias de bytes. Así que cada información en una computadors uno debe saber que va a necesitar expresarla en bytes.

En un byte el valor máximo almacenable es 255. No hay una única manera en computación de convertir una serie de números mayores (como lo son los códigos unicode de los caracteres), o sea que no entran en un byte, en una serie de bytes. Puede sonar loco, pero, por ejemplo, el número 1000, que en binario se representa como 1111101000, algunas computadoras lo guardarán con los bytes 00000011 11101000 y otras al revés: 11101000 00000011 (a la primer manera se la conoce como big endian, y a la segunda como little endian). O sino, si de toda esa serie de números se sabe que muy pocos serán mayores a 128, podría elegirse una codificación que use un solo byte por cada caracter con valor menor a 128 y una serie de largo variable de bytes mayores a 128 si el caracter es mayor a 128.

¿Los perdí? Retomemos, para convertir una serie de caracteres (o un solo caracter es necesario “codificarlos” a bytes, de una manera que no está preestablecida, que no es obvia. A las distintas maneras en que se puede convertir un caracter en bytes (y viceversa) es las llama codificaciones, o “encodings”. Algunas codificaciones son UTF-8, UTF-16LE, ISO-8859-1 (esta última no permite codificar caracteres cuyo número en Unicode sea mayor a 255, igual alcanza para todos los usados en el castellano). En Java se los conoce también como charsets.

En la práctica todo esto significa que en el mundo cálido, acogedor y elegante de Java uno puede trabajar con caracteres Unicode (letras chinas, acentos, símbolos raros) sin problemas. Sin embargo, en las fronteras con el mundo exterior, cuando se lee un archivo de texto, cuando se accede a una conexión de red, será necesario decir con qué codificación debe convertirse, porque hacia y desde el mundo exterior nos comunicamos con bytes. De una conexión de red, a través de su InputSream, no leemos caracteres, leemos bytes. En un archivo no grabamos ni leemos caracteres, son solamente bytes.

Existen varias maneras en Java de ir de bytes a caracteres y viceversa. En el caso de flujos de entrada y salida, donde es necesario ir convirtiendo mientras se lee o se escribe, se utiliza InputStreamReader y OutputStreamWriter. Si no, se usa la clase CharsetEncoder.

Strings en Java

Un string es una secuencia finita de caracteres. En Java, es una secuencia de chars. La propiedad más distintiva e importante de los objetos String en java es que son inmutables. No existe ningún método en la clase String que modifique al objeto sobre el que se invoca. Todos los métodos de manipulación de strings devuelven uno nuevo. 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 el resto de los 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;
	}
}

¡Artículo en construcción! =) Continuará! Mientras... ¿no querés ver mi artículo sobre collections en Java? O si venís de C/C++ quizá te interese un repaso sobre las diferencias, explicando cosas de Java para gente de C++.

Vea mis otros artículos sobre programación en Java, por ejemplo uno sobre collections, u otro en el que trato de enumerar las cosas más importantes del mundo Java.


Me encantaría recibir cualquier tipo de comentarios sobre esta página. ¿Qué pongo? ¿Qué saco? Hecho por Nicolás Lichtmaier.

HTML 4.01 estricto válido

Creative Commons License
Licenciado bajo la licencia Creative Commons Attribution 3.0.