Clase String de Java
Un análisis profundo de la clase String de Java: su diseño, estructura interna y métodos principales.
String es el tipo de referencia más utilizado en Java con diferencia. Lo conociste desde el primer día — String name = "Ada"; — y se coló en tu código sin demasiada ceremonia. Esta parte del libro profundiza bajo esa superficie. La clase tiene más profundidad de lo que aparenta: un diseño interno fijo, una sintaxis especial a nivel de lenguaje que otros tipos no tienen, un pool de memoria que afecta la identidad, y una decisión de diseño deliberada (la inmutabilidad) que repercute en la concurrencia, el hashing y la seguridad.
Este capítulo es el mapa. El resto de la Parte 9 rellena cada región.
¿Qué tipo de cosa es un String?
Un String es un objeto Java normal — java.lang.String, en el mismo paquete que Object e Integer. Es final, por lo que no puedes crear subclases, y cada método que "modifica" un string en realidad devuelve uno nuevo. El original nunca se toca.
String greeting = "hello";
greeting.toUpperCase(); // returns "HELLO" — discarded
System.out.println(greeting); // still prints "hello"
greeting = greeting.toUpperCase();
System.out.println(greeting); // now prints "HELLO"Ese hábito de devolver-uno-nuevo es el hecho más importante sobre la clase. Se trata en profundidad en inmutabilidad de String en Java.
Tratamiento a nivel de lenguaje
Hay dos elementos de sintaxis reservados para String que no son extensibles a otros tipos:
- Literales de string —
"hello"produce un objetoStringdirectamente, sinnew. El compilador también deduplica los literales idénticos en el string pool. - El operador
+— sobrecargado para strings:"a" + "b"es"ab". Incluso expresiones mixtas como"score: " + 42funcionan, porque Java convierte el lado derecho en unString.
Entre bastidores, los compiladores modernos de Java traducen las cadenas de + usando StringBuilder o la StringConcatFactory basada en invokedynamic, por lo que raramente necesitas escribir la concatenación manualmente. El compilador sabe qué hacer.
Estructura interna
Antes de Java 9, cada String contenía un char[] — dos bytes por carácter independientemente del contenido. Java 9 introdujo los compact strings: el array de respaldo es ahora un byte[], más un campo coder de un byte que indica si los bytes son Latin-1 (un byte por carácter) o UTF-16 (dos bytes). Para texto que cabe en Latin-1 — la mayoría del código, configuración, identificadores, inglés llano — esto aproximadamente reduce a la mitad el uso de memoria sin cambiar la API.
No puedes ver el campo, no puedes cambiarlo, no necesitas pensar en él. Pero es por eso que los programas con muchos strings en JDK 9+ usan notablemente menos heap que en JDK 8.
Las familias de métodos principales
La API de String es amplia pero se organiza en un puñado de grupos reconocibles:
Inspección. length(), isEmpty(), isBlank(), charAt(i), codePointAt(i), hashCode().
Búsqueda. indexOf, lastIndexOf, contains, startsWith, endsWith, matches.
Extracción. substring(start), substring(start, end), chars(), codePoints(), toCharArray().
Transformación. toUpperCase(), toLowerCase(), trim(), strip(), replace, replaceAll, replaceFirst, concat.
División y unión. split, String.join — tratado en split() y join().
Formateo. String.format, el método de instancia formatted, y la salida estilo printf — tratado en formateo de String.
Comparación. equals, equalsIgnoreCase, compareTo, compareToIgnoreCase, contentEquals — tratado en comparación de String.
Conversión. valueOf (estático), toString (de instancia), métodos de análisis en Integer, Double, etc. — tratado en conversiones de String.
Los listados completos de la API están en el Javadoc del JDK. La habilidad está en reconocer a qué familia acudir, no en memorizar cada sobrecarga.
Los strings son secuencias de unidades de código UTF-16
charAt(i) y length() cuentan unidades de código UTF-16, no caracteres Unicode. Para texto dentro del Plano Multilingüe Básico (la mayor parte de los scripts comunes), un char = un carácter y la distinción nunca importa. Para caracteres suplementarios — la mayoría de los emojis, algunas extensiones CJK, scripts antiguos — un carácter visible para el usuario ocupa dos chars, un par sustituto.
String emoji = "🙂";
System.out.println(emoji.length()); // 2 — two code units
System.out.println(emoji.codePointCount(0, emoji.length())); // 1 — one code pointSi necesitas iterar por código point Unicode, usa codePoints() o codePointAt. Para la mayoría de los casos de uso con texto ASCII — dividir CSVs, formatear líneas de log, comparar identificadores — length() y charAt son exactamente lo que necesitas.
Primos mutables: StringBuilder y StringBuffer
Cuando necesitas construir un string pieza a pieza, el += repetido asigna un nuevo String en cada paso. La biblioteca estándar incluye dos compañeros mutables para ese caso:
StringBuilder— rápido, de un solo hilo.StringBuffer— la misma API, métodos sincronizados, útil solo cuando más de un hilo escribe en el mismo buffer.
Tienen APIs paralelas y capítulos paralelos en esta parte del libro.
Un ejemplo práctico
Un pequeño ejercicio que toca las familias más comunes — inspección, búsqueda, extracción, transformación y conversión — sobre la misma entrada. Lee la salida línea por línea; cada llamada ilustra una herramienta de la lista anterior.
La última línea es la conclusión. Después de todas las transformaciones que usamos, line en sí misma es idéntica byte a byte al literal con el que empezamos — prueba de que el modelo de devolver-uno-nuevo es real, no solo una nota de documentación.
Qué sigue
Los strings tienen un modelo de memoria único en la biblioteca estándar: los literales idénticos comparten almacenamiento, y puedes incorporar strings arbitrarios a ese pool compartido con una sola llamada a un método. Continúa en Java String pool.