3. La clase String en profundidad
| Campo | Contenido |
|---|---|
| Unidad | UD3, Programación Orientada a Objetos con la API de Java |
| RA | RA2, Escribe y prueba programas sencillos, reconociendo y aplicando los fundamentos de la programación orientada a objetos |
| CE cubiertos | CE 2.2 (se han escrito programas simples), CE 2.4 (se han utilizado métodos y propiedades de los objetos) y CE 2.6 (se han utilizado parámetros en la llamada a métodos) de forma directa; refuerzo de CE 2.3, CE 2.5, CE 2.7 y CE 2.8 |
En esta sección aprenderás:
- Por qué
Stringes inmutable, qué consecuencias prácticas tiene esa decisión de diseño en cada línea que escribes y cómo sacarle partido en vez de sufrirla. - Qué es el pool de cadenas (string interning): la zona especial del montón donde viven los literales, y por qué esa es la razón profunda de la trampa
==frente a.equals()que arrastramos desde la UD1. - El repertorio de búsqueda (
contains,startsWith,endsWith,indexOf,lastIndexOf), transformación (toUpperCase,toLowerCase,trim,strip,replace,replaceAll), extracción (substring,charAt,split,chars), construcción (String.join,repeat) y comparación y comprobación (equals,equalsIgnoreCase,compareTo,isEmpty,isBlank). - A construir cadenas con plantillas:
String.format()yformatted(), con sus especificadores%s,%d,%fy%n.
Necesitas tener fresco (de unidades y secciones anteriores):
- De la UD1:
Stringcomo tipo de referencia; concatenación con+;String.format()yformatted()como primeras plantillas; la trampa==frente a.equals();String.valueOf(). - De la UD2: bucles
foryfor-each; las sentencias de salto;try-catch; los arrays (String[],.length) y el depurador con Evaluate Expression (Alt+F8). - De la sección 1: pila y montón, referencias, identidad frente a igualdad de contenido, y la promesa: «el pool de cadenas lo abrimos en la sección 3».
- De la sección 2: leer la Javadoc, la regla del receptor (instancia frente a estático), la sobrecarga y el circuito
Ctrl+Space→Ctrl+P→Ctrl+Q.
3.1 El protagonista escondido del curso
La sección anterior terminó con una promesa: íbamos a abrir de par en par la clase que más has usado en todo el curso. Ha llegado el momento. String es, con muchísima diferencia, la clase más empleada de todo el API de Java: no existe programa sin cadenas — nombres, mensajes, menús, ficheros, contraseñas, JSON — y cada literal que has escrito desde la UD1 era un objeto de esta clase viviendo tranquilamente en el montón. Si en la sección 2 aprendiste a orientarte en el almacén, hoy tocamos la estantería que más vas a visitar.
Y conviene empezar corrigiendo una impresión. Después de dos secciones de POO, cabe pensar que String es «lo fácil», lo que se ve el primer día y no guarda secretos. Falso. String guarda varios de los detalles más finos del lenguaje — la inmutabilidad, el pool de cadenas, el orden lexicográfico — y es también la clase con la que más errores cometen los programadores noveles: comparar con ==, olvidar que substring no incluye el final, pedirle %d a un double... Esta sección existe para que ninguno de esos errores te pille a ti.
Empecemos por releer lo conocido con las gafas de las dos secciones anteriores:
Todo lo de la sección 2 se cumple aquí a la letra. String vive en java.lang, así que jamás ha necesitado un import (y por eso ninguno de los programas de hoy llevará uno). Casi todo su repertorio son métodos de instancia: la respuesta depende del contenido de esa cadena concreta — frase.length() responde 19 porque esta cadena tiene diecinueve caracteres. Y también tiene su puñado de miembros estáticos — String.format, String.join, la familia valueOf — que pronto volverás a cruzarte. Si abres ahora su Javadoc y bajas al Method Summary, verás más de setenta métodos públicos contando las sobrecargas: hoy recorreremos la treintena que cubre el 95 % del trabajo real, y las herramientas de la sección 2 te permitirán descubrir el resto por tu cuenta cuando lo necesites.
Dos detalles de su declaración que puedes leer directamente en la página de la clase y que ya sabes interpretar. Primero: public final class String. Ese final significa que nadie puede heredar de ella — no habrá subclases de String, jamás — y es una pieza clave de su diseño, como verás en seguida. Segundo: sus índices van de 0 a length() - 1, exactamente como los arrays de la UD2. Acostúmbrate a pensarlo: una cadena es, en el fondo, una fila de caracteres numerada, y casi todos sus métodos hablan de posiciones de esa fila.
Para no perdernos entre treinta métodos, vamos a organizarlos por verbos, que es como tu cabeza los buscará cuando programes: buscar, transformar, extraer, construir, comparar y formatear. Este es el mapa del territorio que recorreremos:
flowchart TD
S["El repertorio de String\nque veras hoy"]
B["BUSCAR\ndonde esta algo\ncontains · startsWith · endsWith\nindexOf · lastIndexOf"]
T["TRANSFORMAR\ncrear una variante\ntoUpperCase · toLowerCase\nstrip · trim · replace · replaceAll"]
E["EXTRAER\ntrozos y caracteres\nsubstring · charAt · split · chars"]
C["CONSTRUIR\ncadenas nuevas\nString.join · repeat · +"]
P["PREGUNTAR\ncomparar y comprobar\nequals · equalsIgnoreCase\ncompareTo · isEmpty · isBlank"]
F["FORMATEAR\nplantillas con huecos\nString.format · formatted"]
S --> B
S --> T
S --> E
S --> C
S --> P
S --> F
style S fill:#2e7d32,color:#ffffff,stroke:#1b5e20
style B fill:#c8e6c9,color:#000000,stroke:#2e7d32
style T fill:#c8e6c9,color:#000000,stroke:#2e7d32
style E fill:#c8e6c9,color:#000000,stroke:#2e7d32
style C fill:#c8e6c9,color:#000000,stroke:#2e7d32
style P fill:#c8e6c9,color:#000000,stroke:#2e7d32
style F fill:#c8e6c9,color:#000000,stroke:#2e7d32
Un apodo que veras en las firmas: CharSequence
Cuando leas la ficha Javadoc de contains o de replace, el parámetro no aparece como String, sino como CharSequence. No te asustes: es un contrato que significa «soy una secuencia de caracteres», y String es su implementación estrella. Mientras llegamos a las interfaces en la UD7, quédate con la regla práctica: si una ficha pide CharSequence, puedes pasarle un String sin pensarlo, porque String es un CharSequence (herencia de tipos, la de la sección 1). El indexOf('a') que verás más abajo declara su parámetro como int: un char, por dentro, es un número de 16 bits, y encaja ahí sin ceremonia.
Pero antes de recorrer los verbos, hay un hecho de String que condiciona a todos los demás. Empecemos por él, porque es la propiedad más importante de toda la sección.
3.2 Inmutabilidad: cadenas de roca
Aquí viene la propiedad que lo cambia todo: un objeto String no puede cambiar de contenido después de haber sido creado. Nunca. No existe ningún método en toda la clase que modifique los caracteres del objeto que lo recibe. Cuando «cambias» una cadena — la pasas a mayúsculas, le quitas espacios, sustituyes letras — lo que ocurre en realidad es que se fabrica un objeto String nuevo con el resultado, y el original permanece exactamente igual que nació. A esa propiedad se la llama inmutabilidad, y String es el ejemplo canónico de todo el API.
La sección 1 te lo adelantó de pasada, cuando hablamos de encapsulación: String es «una caja con cerradura llevada al extremo — ni siquiera el propio objeto se deja modificar». Hoy lo convertimos en mecánica. Observa este experimento, que vale más que mil definiciones:
La primera llamada no hace nada visible: crea un String nuevo con el texto "PROGRAMAR", lo devuelve... y como nadie lo recoge, ese objeto recién nacido queda huérfano y el recolector de basura se lo llevará en su próxima ronda. La cadena original, "programar", ni se enteró. Solo en la segunda llamada, al reasignar el resultado con frase = frase.toUpperCase();, la referencia pasa a apuntar al objeto nuevo. La variable cambia de flecha; los objetos, jamás de contenido:
flowchart LR
subgraph monton["Montón de memoria"]
s1["objeto String 'programar'\n(nació con el literal)"]
s2["objeto String 'PROGRAMAR'\n(nace con la llamada)"]
end
ref1["referencia frase\nantes de reasignar"] --> s1
ref2["referencia frase\ntras frase = frase.toUpperCase()"] --> s2
s1 -- "toUpperCase() no modifica:\nfabrica una cadena nueva y la devuelve" --> s2
style s1 fill:#c8e6c9,color:#000000,stroke:#2e7d32
style s2 fill:#fff3e0,color:#000000,stroke:#e65100
style ref1 fill:#e3f2fd,stroke:#1565c0
style ref2 fill:#e3f2fd,stroke:#1565c0
De aquí sale la primera regla práctica de la sección, la que te ahorrará la mitad de los sustos con cadenas:
frase.trim()no limpiafrase: limpia una copia. Si quieres conservar el resultado, reasigna:frase = frase.trim();.
Es el patrón «empleado diligente» de la sección 1, llevado al extremo: los métodos de String ni siquiera pueden tocar la obra original; solo te entregan una obra nueva.
Por qué alguien diseñaría algo tan rígido
Que las cadenas sean de roca parece una molestia — ¿no sería cómodo poder cambiar un carácter a lo bestia, como en un array? — y sin embargo es una de las decisiones más acertadas del diseño de Java. Tres motivos que podrás apreciar ya mismo:
- Seguridad. Las cadenas viajan por donde más duele: contraseñas, rutas de ficheros, nombres de usuario, consultas. Imagina que validas una contraseña y, justo después de la comprobación, otro trozo del programa modifica ese mismo objeto y coloca dentro otra cosa. Con
String, ese ataque es físicamente imposible: la cadena que validaste es la misma que la que se usará después, palabra por palabra, hasta que muera. (Curiosidad de seguridad: por ese mismo motivo, para guardar contraseñas en memoria se prefierechar[], que sí se puede borrar;Stringno se puede borrar nunca. Lo verás en la práctica profesional, no en este curso.) - Compartir sin miedo. Como nadie puede modificarlas, dos partes del programa pueden compartir el mismo objeto sin riesgo de que una estropee lo de la otra. Esa es justo la puerta que abre el pool de cadenas de la subsección siguiente: prestar es seguro cuando nada se puede romper.
- Fiabilidad del cálculo. Un objeto inmutable puede calcular cosas de sí mismo una sola vez y confiar en que no caducan.
Stringguarda su longitud y su código hash precalculados desde que nace, porque sabe que nunca cambiarán. Cuando en la UD5 uses cadenas como claves de mapas, ese detalle se traduce en velocidad pura.
Y una razón transversal: el final de su declaración. Si alguien pudiera heredar de String y añadir métodos que sí modificaran, toda esta fortaleza se vendría abajo; por eso la clase está cerrada con llave. Inmutabilidad y final van de la mano.
Las dos consecuencias que te acompañarán todo el curso
Primera: el encadenado es gratis y natural. Ya lo viste en la sección 1 con frase.strip().toUpperCase(), y ahora sabes por qué funciona tan bien: cada eslabón recibe un objeto nuevo e intacto, le aplica su transformación y entrega otro objeto nuevo. Nadie pisa a nadie; el orden de la cadena es el único que manda.
Segunda: la concatenación en bucles es el talón de Aquiles, y te lo digo hoy para que lo reconozcas cuando lo veas. Este código, que parece inocente, fabrica una cadena nueva en cada vuelta:
No es un error: funciona y el resultado es correcto. Pero es caro: mil vueltas, mil cadenas intermedias creciendo de tamaño, mil cadenas viejas esperando al recolector. Para mil líneas pasa; para un millón, el programa se arrastra. La solución profesional tiene nombre — StringBuilder — y tiene ya su cita en el calendario: sección 7 de esta unidad, donde desmontaremos este bucle y lo reconstruiremos como es debido. Hoy basta con que sepas que el problema existe y de dónde viene: de la inmutabilidad, precisamente.
Pruébalo en IntelliJ: el laboratorio de Alt+F8
La ventana Evaluate Expression que conociste en la UD2 (con el depurador parado en un breakpoint, Alt+F8) es el banco de pruebas perfecto para los métodos de esta sección, sin escribir ni un programa: evalúa " java 25 ".strip() y mira el resultado; luego "hola".toUpperCase(); luego "hola".repeat(3). Cada expresión te devuelve su valor al instante, y puedes comparar lo que devuelve una llamada con el estado del objeto original en la ventana Variables: el original, intacto siempre. Cadena de roca, comprobado con tus propios ojos.
3.3 El pool de cadenas: la caja prometida se abre
En la UD1 te contamos la trampa de == frente a .equals() y te pedimos que confiaras. En la sección 1 la volviste a ver con el modelo de memoria delante y quedó explicada a medias: == compara flechas (identidad) y .equals() compara contenido. Pero quedó un cabo suelto, y la sección 1 lo dejó citado aquí con nombre y apellido: ¿por qué == con literales a veces funciona y da la falsa sensación de ser válido? Ha llegado el momento de abrir esa caja con calma, porque la respuesta tiene entidad propia.
La caja se llama pool de cadenas (string pool, y el mecanismo, string interning): una zona especial del montón donde la JVM guarda una sola copia de cada cadena literal que aparece en el programa. La regla es sencilla: cuando el compilador se encuentra un literal como "java", no fabrica un objeto nuevo cada vez — consulta el pool, y si esa cadena ya está ahí, reutiliza el mismo objeto. Los literales idénticos de tu programa acaban siendo, literalmente, el mismo objeto.
Compruébalo con el experimento completo, el que cierra el expediente abierto en la UD1:
Tres líneas de salida — true, false, true — y ahora cada una tiene su explicación completa. El diagrama de memoria lo cuenta mejor que yo:
flowchart LR
subgraph pila["Pila (stack)"]
va["a"]
vb["b"]
vc["c"]
end
subgraph monton["Montón (heap)"]
subgraph pool["Pool de cadenas (zona especial)"]
sp["'java' — UN solo objeto\ncompartido por los literales"]
end
sc["objeto String nuevo\n'java' — fuera del pool"]
end
va --> sp
vb --> sp
vc --> sc
va -. "a == b es true: misma flecha" .- vb
va -. "a == c es false: flechas a objetos distintos" .- sc
style sp fill:#c8e6c9,color:#000000,stroke:#2e7d32
style sc fill:#fff3e0,color:#000000,stroke:#e65100
style pila fill:#e3f2fd,stroke:#1565c0
Fíjate en lo que acaba de pasar con a == b: comparar contenido con == funcionó. Y ahí está el veneno exacto de esta trampa: no falla siempre — falla a veces, justo cuando algo como new String(...) o una concatenación en tiempo de ejecución mete un objeto nuevo por medio. Un error que aparece a veces es mucho más peligroso que uno que aparece siempre, porque se cuela en los tests y salta en producción. Por eso la regla de oro de la UD1 no se negociaba entonces y no se negocia ahora que ya entiendes su porqué:
Para comparar contenido,
.equals(). Siempre.==con cadenas solo responde a «¿son el mismo objeto?», una pregunta de identidad que casi nunca es la que quieres hacer.
¿Y para qué existe el pool, con el lío que trae? Por ahorro de memoria: los programas reales repiten cadenas hasta el hartazgo — etiquetas de interfaz, claves de configuración, nombres de campos, mensajes. Si cada aparición de "nombre" en un programa ocupara su propio objeto, el montón se llenaría de gemelos. El pool lo evita: una copia, muchas referencias. Y fíjate en la simetría del diseño: el pool solo es posible porque las cadenas son inmutables (compartir sería suicida si cualquiera pudiera modificar el objeto compartido). Inmutabilidad y pool son dos piezas del mismo mecanismo, no dos noticias separadas.
Una advertencia de contexto para cerrar: new String("java") es una aberración didáctica — solo se escribe para experimentos como el de arriba. Los constructores de String existen (la Javadoc los lista: a partir de un array de caracteres, y demás), pero en la práctica profesional las cadenas nacen de literales, de fábricas como String.valueOf o de métodos que las construyen. Si en una entrevista te preguntan cómo se crea una cadena, «con un literal» es la respuesta que espera oír.
intern(): pedir la copia del pool a propósito
La clase ofrece el método intern(), que devuelve la representación canónica de una cadena: la copia que vive en el pool (metiéndola antes, si no estaba). Con él puedes comprobar que c, el objeto rebelde de fuera del pool, sí tiene su gemelo dentro:
En código de este curso no lo usarás nunca — equals resuelve la comparación de contenido sin ceremonias —, pero conviene haberlo visto: es la llave oficial de la caja que acabamos de abrir.
El último pliegue: concatenar literales en tiempo de compilación
Hay un pliegue final del pool que encanta en exámenes y entrevistas, así que mejor lo ves aquí primero. El compilador es capaz de sumar literales cuando los conoce de antemano, y el resultado entra en el pool:
La diferencia entre las dos sumas es cuándo se hacen: la primera, con todos los datos sobre la mesa antes de ejecutar una sola línea; la segunda, con una variable cuyo valor se conoce en ejecución. Si este pliegue te despista, no te preocupes: no cambia la regla de oro ni una coma. Comparar contenido se hace con equals, y el pool deja de ser una trampa y pasa a ser una curiosidad que ya entiendes.
3.4 Búsqueda: encontrar cosas dentro del texto
Cerrado el expediente de la memoria, toca el repertorio. Empezamos por el verbo más pedido en cualquier programa: buscar. ¿Contiene esta cadena aquello? ¿Empieza o termina así? ¿En qué posición está? La clase String responde con dos familias: los que devuelven boolean (la pregunta es sí o no) y los que devuelven int (la pregunta es dónde).
| Método | Devuelve | Pregunta que responde |
|---|---|---|
contains(trozo) |
boolean |
¿Aparece trozo en algún sitio de la cadena? |
startsWith(prefijo) |
boolean |
¿Empieza la cadena por prefijo? |
endsWith(sufijo) |
boolean |
¿Termina la cadena por sufijo? |
indexOf(x) |
int |
¿En qué posición aparece x por primera vez? |
lastIndexOf(x) |
int |
¿En qué posición aparece x por última vez? |
Todas comparten dos reglas de casa que conviene fijar antes de usarlas. Primera: son sensibles a mayúsculas y minúsculas — "Java".contains("java") es false, sin apelación. Segunda: indexOf y lastIndexOf aceptan indistintamente un char o un String (sobrecarga, como las de IO en la sección 2: tú eliges la versión con los argumentos), y si lo buscado no aparece, devuelven -1, no cero ni una excepción. Ese -1 es una convención histórica de todo el API y la vas a manejar constantemente.
Vamos a verlas todas en acción sobre un material realista, un nombre de fichero:
Fíjate en el ejemplo del guion: indexOf y lastIndexOf son la pareja con la que se localiza algo que aparece repetido — la primera mención y la última —, y la distancia entre ambas te dice cosas del texto (aquí, que entre el guion de la posición 7 y el de la 13 hay un apellido de cinco letras). Ambos tienen versiones con posición de partida: indexOf(x, desde) empieza a buscar a partir de desde, y es el truco clásico para saltar la primera aparición:
El patrón del -1: buscar es comprobar antes de usar
La búsqueda con indexOf casi nunca es un fin: quieres la posición para hacer algo con ella — cortar la cadena, saltarte un prefijo. Y ahí vive el patrón más importante de esta subsección, el que escribirás mil veces en tu vida: comprobar el -1 antes de usar la posición. Si te lo saltas, la aritmética posterior produce resultados absurdos (recorta desde -1 y te carga el primer carácter), sin que nada avise:
Guárdate ese if (pos == -1) como un reflejo: búsqueda que no comprueba el -1 es búsqueda a medias. Y guarda también su complemento, el antídoto contra la sensibilidad a mayúsculas: como los métodos de transformación devuelven cadenas nuevas (3.2), se encadenan con naturalidad y la búsqueda insensible se escribe bajando todo a un común:
Ese idiom — normalizar y luego buscar — es exactamente el que usarás en la práctica guiada del final, y también el que hay detrás de equalsIgnoreCase, que llegará con su familia en la subsección de comparación.
3.5 Transformación: cadenas nuevas a partir de la vieja
Segundo verbo del mapa: transformar. Aquí viven los métodos que toman la cadena y devuelven una variante — en mayúsculas, sin espacios de los bordes, con trozos sustituidos. Lo primero, y ya sabes por qué: ninguno modifica el original (inmutabilidad, subsección 3.2), así que todos se usan con el patrón de reasignación o en cadena. Lo segundo: casi todos son de instancia — la transformación opera sobre esta cadena concreta —, con una excepción que ya conoces de la sección 2 y que reaparecerá aquí mismo.
Mayúsculas, minúsculas y limpieza de bordes
La pareja toUpperCase() / toLowerCase() no guarda secretos: devuelve la cadena con la caja cambiada, y los caracteres que no tienen caja (números, símbolos) pasan tal cual. Lo que sí merece atención es la limpieza de bordes, porque hay dos métodos para lo mismo y la elección cuenta:
trim() |
strip() |
|
|---|---|---|
| Desde | Java 1.0 (los orígenes) | Java 11 |
| Qué quita | Solo caracteres de control y el espacio «clásico» (códigos hasta U+0020) | Todo el espacio en blanco que Unicode considera tal |
| Con espacios de toda la vida | Funciona | Funciona |
| Con espacios «tipográficos» Unicode (p. ej. el espacio ideográfico japonés, o el em space de imprenta) | Los deja | Los quita |
| Veredicto del curso | Reconocerlo en código antiguo | La opción moderna por defecto |
La diferencia solo asoma con texto internacional — pero asoma: si limpias un texto japonés con trim(), sus márgenes quedan sucios. Compruébalo con un experimento de laboratorio usando el espacio ideográfico (escribimos su código con \u3000, que el compilador convierte en el carácter real):
strip() llegó en Java 11 en el mismo paquete de novedades que repeat() e isBlank() — verás ese año repetido en el Since de sus fichas Javadoc — y trae dos hermanos quirúrgicos: stripLeading() (limpia solo por la izquierda) y stripTrailing() (solo por la derecha), útiles cuando el espacio de un lado significa algo (sangrías, alineaciones). Para la limpieza normal de entradas de usuario, strip() solo.
Sustituir: replace y su primo con superpoderes
El repertorio de sustitución tiene tres métodos con nombres sospechosamente parecidos, y la distinción entre ellos es una de esas cosas que separan a quien leyó la ficha de quien copió de Stack Overflow:
La primera versión, replace('a', 'e'), es la que la sección 1 te adelantó de pasada; ahora ya la lees entera. La segunda, replace("programar", "cocinar"), cambia trozos completos. Ambas comparten dos reglas: sustituyen todas las apariciones (no solo la primera) y trabajan sobre texto literal, sin interpretar nada raro en lo que les pasas. replace es el destornillador de la sustitución: predecible y seguro.
El primo con superpoderes es replaceAll, y aquí va la advertencia más importante de la subsección:
replaceAll interpreta su primer argumento como una expresión regular — un mini-lenguaje de patrones de texto —, y por eso logra en una línea lo que con replace no se puede: «uno o más caracteres de espacio, sean los que sean» (\\s+) o «cualquier dígito» (\\d). El precio es doble. Primero, el mini-lenguaje tiene sintaxis propia: si le pasas replaceAll("a.c", "X") creyendo sustituir el texto literal a.c, te llevarás una sorpresa, porque el punto significa «cualquier carácter» y devaneará tu texto sin avisar. Segundo, esos dobles \\ del código no son un capricho: en Java, \\ dentro de un literal significa «un carácter barra invertida», y es la expresión regular quien recibe entonces \s y \d. Dos niveles de escape, dos motores distintos — el compilador primero, el motor de patrones después —; te lo encontrarás cada vez que unas cadenas con regex.
Las expresiones regulares tienen su propia unidad
replaceAll (y split, que verás enseguida) te acaban de enseñar lo mínimo para no llevarte un susto: saben regex, sustituir todas las coincidencias, y tres recetas (\\s+, \\d, el punto salvaje). El estudio completo del mini-lenguaje — Pattern, Matcher, validaciones de correos y teléfonos — está citado en la UD5, sección 10, cuando ya domines colecciones y streams. Hoy: usa replace para todo lo literal, y replaceAll solo con recetas probadas. Existe además replaceFirst(regex, cambio), que sustituye solo la primera coincidencia del patrón: misma advertencia, misma cita en UD5.
El resumen del verbo, en una tabla
| Quiero... | Método | Devuelve |
|---|---|---|
| Todo en mayúsculas / minúsculas | toUpperCase() / toLowerCase() |
cadena nueva |
| Quitar espacios de los bordes (moderno) | strip() (y stripLeading() / stripTrailing()) |
cadena nueva |
| Quitar espacios de los bordes (histórico) | trim() |
cadena nueva |
| Cambiar un carácter por otro (todas las veces) | replace('a', 'e') |
cadena nueva |
| Cambiar un trozo literal por otro | replace("viejo", "nuevo") |
cadena nueva |
| Cambiar según un patrón | replaceAll("\\d", "*") |
cadena nueva |
Una sola fila de esta tabla no devuelve «cadena nueva»... y es que ninguna lo hace: todas devuelven una cadena nueva. Si escribo esta columna es para que el patrón quede grabado a fuego: el verbo transformar nunca toca el original.
3.6 Extracción: substring, charAt, split y chars
Tercer verbo: extraer. Si buscar pregunta dónde está, extraer responde dame eso. Es el grupo con más trampas de índices de toda la sección, así que vamos con calma y con el modelo de la fila numerada bien presente: los caracteres de una cadena ocupan las posiciones 0 a length() - 1, exactamente como los elementos de un array.
substring: cortar por la línea de puntos
substring recorta un trozo de la cadena, y tiene dos versiones: desde una posición hasta el final, o entre dos posiciones. La segunda es la fuente inagotable de errores de examen, así que mirémosla despacio:
substring(1, 4) devuelve las posiciones 1, 2 y 3... y no la 4. Esa es la regla que hay que tatuar en el antebrazo: el índice inicial va incluido y el final, excluido — el intervalo se escribe en matemáticas [1, 4). Dibuja la fila numerada y lo verás para siempre:
flowchart LR
subgraph cadena["'programar' — 9 caracteres, posiciones 0 a 8"]
c0["p\n0"]
c1["r\n1"]
c2["o\n2"]
c3["g\n3"]
c4["r\n4"]
c5["a\n5"]
c6["m\n6"]
c7["a\n7"]
c8["r\n8"]
end
c0 --- c1 --- c2 --- c3 --- c4 --- c5 --- c6 --- c7 --- c8
style c1 fill:#2e7d32,color:#ffffff,stroke:#1b5e20
style c2 fill:#2e7d32,color:#ffffff,stroke:#1b5e20
style c3 fill:#2e7d32,color:#ffffff,stroke:#1b5e20
style c4 fill:#fff3e0,color:#000000,stroke:#e65100
Las casillas verdes son lo que devuelve substring(1, 4): desde la posición 1 (incluida) hasta la 4 (que queda fuera, en naranja, esperando su turno). La recompensa de entenderlo así es que las cuentas salen solas: la longitud del trozo es siempre final - inicial (aquí, 4 - 1 = 3 letras), y una consecuencia elegante: palabra.substring(0, palabra.length()) es la cadena entera. Si te pasas de rango — un final mayor que length(), o un inicial negativo — el programa lanza StringIndexOutOfBoundsException, prima hermana de la ArrayIndexOutOfBoundsException que ya domaste con los arrays de la UD2: mismos límites, mismo nombre de familia.
Con substring e indexOf juntos ya puedes escribir el clásico de los clásicos — cortar un correo en su nombre y su dominio, usando el patrón del -1 de la subsección anterior:
Fíjate en el corte + 1 del segundo recorte: la arroba está en corte, y el dominio empieza en la posición siguiente. Sumar y restar posiciones alrededor de un indexOf es el pan de cada día del trabajo con texto.
charAt: un carácter, por su número
charAt(i) devuelve el carácter que ocupa la posición i, y su tipo de retorno es char — el primitivo de la UD1 que guarda un solo carácter Unicode. Es el puente entre el mundo de las cadenas y el mundo de los números, porque un char por dentro es un número de 16 bits, y con él se puede hacer aritmética:
Ese 32 de la última línea es una cifra que te acompañará toda la vida programando: la distancia entre una letra minúscula y su mayúscula en Unicode. Y la causa profunda de que la búsqueda y la comparación de cadenas distingan caja: 'a' (97) y 'A' (65) son números distintos, y para un ordenador no hay manera «natural» de considerarlos iguales. Comparar caracteres con >= y <=, como se comparan números, es totalmente legal y lo usarás en la práctica guiada para cazar mayúsculas letra a letra.
split: una cadena, muchas piezas
split parte la cadena cada vez que aparece un separador y te entrega un array de String — material de la UD2 que ahora brilla con luz nueva. Recorrerlo con un for-each es el idiom estándar para procesar texto troceado:
Su separador es... una expresión regular, como el primer argumento de replaceAll (misma advertencia, misma cita en UD5): con separadores simples — una coma, un espacio, un punto y coma — no hay nada que temer; cuando necesites separar por cosas complicadas («uno o más espacios»), ya sabes qué mini-lenguaje te espera en la unidad 5. Y un detalle fino: split descarta las piezas vacías del final — "a,b,,".split(",") entrega dos piezas, no cuatro —, comportamiento que discutiremos cuando el procesamiento de texto sea serio.
chars: la cadena como chorro de números
Último método del grupo, y es un adelanto con etiqueta de origen: chars() devuelve la cadena como un IntStream, un chorro de los códigos numéricos de sus caracteres. Es tu primer contacto con la Streams API, el tema estrella de la UD5 — el mismo adelanto que te espera con Random.ints() en la sección 5. Hoy basta con dos operaciones de chorro que no necesitan teoría: contar y sumar:
La cuenta da 4 — una por carácter — y la suma da 420, que es lo que suman los códigos de la h, la o, la l y la a (104 + 111 + 108 + 97). No hay que memorizar nada: solo reconocer la forma cadena.chars().operación() y saber que, cuando llegue la UD5, esa flechita desbloqueará un arsenal entero de procesamiento. Hasta entonces, la puerta queda señalizada.
El pariente clásico: toCharArray()
Antes de que existiera chars(), el API ofrecía toCharArray(), que copia la cadena en un char[] de la UD2 y se recorre con un bucle clásico por índices. Hoy es menos elegante pero sigue siendo legal y útil cuando necesitas los caracteres como material manipulable (por ejemplo, para ordenarlos con la clase Arrays —te espera en la sección 9— o para desordenarlos en un juego de anagramas). Coexisten por la misma razón que trim y strip: el API nunca borra, solo añade.
3.7 Construcción: join, repeat y la vieja concatenación
Cuarto verbo: construir — fabricar cadenas nuevas a partir de piezas. La herramienta más vieja ya la dominas desde la UD1: el operador + de concatenación. Hoy le añadimos dos métodos con personalidad propia y una advertencia sobre el operador veterano.
String.join: el ensamblador estático
join recibe un separador y varias piezas, y devuelve la cadena que sale de ensamblarlas con ese separador por medio. La noticia interesante es su receptor, y aquí la sección 2 cobra su inversión: join es un método estático, así que se pide a la clase, no a una cadena:
Piénsalo con la regla mental de la sección 2 y verás que el diseño es impecable: ¿la respuesta de «ensaémbame estas piezas con guiones» dependería de una cadena concreta que recibiera el mensaje? No — depende solo de sus argumentos —, así que el miembro pertenece al colectivo: Clase.metodo(...). Es exactamente la familia de Integer.parseInt y Math.sqrt. (Y tiene dos versiones sobrecargadas en la Javadoc: una para piezas sueltas y otra para colecciones — esa segunda la apreciarás en la UD5.)
repeat: la cadena que se multiplica
repeat — que tú mismo descubriste en la misión 2 de la sección 2, así que esto es oficialmente un examen de memoria — repite la cadena que lo recibe las veces que le indiques. Es de instancia, obviamente: lo que se repite es esta cadena:
Tres detalles de ficha técnica: llegó en Java 11 (Since: 11, el mismo año que strip e isBlank); repetir cero veces devuelve la cadena vacía, perfectamente legal; y repetir un número negativo lanza IllegalArgumentException — una más para tu colección de excepciones de la UD2.
La advertencia sobre el veterano +
La concatenación con + es cómoda y la seguirás usando toda la vida, pero esconde una trampa de asociatividad que golpea justo cuando mezclas texto y números. Como + se evalúa de izquierda a derecha, todo lo que queda a la derecha del primer texto se convierte en texto y se concatena en lugar de sumarse:
La primera línea no es ningún error de sintaxis: compila, ejecuta y devuelve Total: 12 — que es exactamente la salida que un bug de estos produce en producción, silencioso y digno. El paréntesis de la segunda fuerza la suma antes de que entre en contacto con el texto. Grábatelo: cuando una expresión mezcla + numérico y + de cadenas, decide tú el orden con paréntesis, y no dejes que sea la asociatividad quien decida por ti.
La familia estatica que ya conoces: String.valueOf
Completa el verbo construir la familia valueOf, que conociste en la UD1 para convertir valores a texto: String.valueOf(42), String.valueOf(3.5), String.valueOf(true). Es una familia sobrecargada estática — una versión por tipo primitivo, más una para Object — así que la regla de la sección 2 la clasifica de un vistazo: la conversión no depende de ninguna cadena concreta, luego se pide a la clase. valueOf era la forma elegante de convertir antes de las plantillas; hoy la seguirás viendo en código ajeno y la tendrás a mano para casos puntuales.
3.8 Comprobación y comparación: preguntar a la cadena
Quinto verbo, y el más delicado de todos: preguntar. ¿Es esta cadena igual a aquella? ¿Va antes o después en el alfabeto? ¿Está vacía? Sobre estas preguntas se construyen las validaciones — de formularios, de menús, de entradas de usuario — que son el pan de cada día de cualquier aplicación.
equals y equalsIgnoreCase: igualdad de contenido
De equals ya sabes lo importante: compara contenido, carácter a carácter, y es el único modo legítimo de comparar cadenas (regla de oro blindada con la explicación del pool en la 3.3). Su hermano equalsIgnoreCase hace la misma pregunta ignorando la caja:
Esa última línea condensa tres herramientas de la sección en una sola: strip limpia los espacios accidentales del usuario, equalsIgnoreCase tolera la caja, y el conjunto convierte «sÍ », « si» y «SI» en un true cada uno. Normalizar y luego comparar, exactamente igual que normalizar y buscar en la subsección 3.4.
Una advertencia de null antes de seguir: equals se invoca sobre una referencia, y si esa referencia está en blanco, la llamada lanza NullPointerException — la mecánica exacta de la sección 1. IO.readln nunca devuelve null (devuelve la cadena vacía si no escribes nada), pero muchos métodos del API sí, y lo verás en la UD6. El patrón defensivo clásico lo resuelve con el cortocircuito de && que conoces de la UD1: comprobar null antes de tocar la cadena.
compareTo: el orden del diccionario
equals responde sí o no; compareTo responde algo más rico: cómo de ordenadas están las dos cadenas. Es el método que usa el orden de los diccionarios — el orden lexicográfico — y su contrato es de una elegancia que conviene leer despacio:
| Llamada | Devuelve | Significado |
|---|---|---|
"casa".compareTo("coche") |
un número negativo | casa va antes que coche |
"coche".compareTo("casa") |
un número positivo | coche va después |
"casa".compareTo("casa") |
0 |
son idénticas |
¿Por qué casa va antes? Se compara letra a letra desde la izquierda: la c inicial empata, y en la segunda letra a (código 97) es menor que o (código 111) — ahí se decide. La comparación es, en el fondo, numérica entre los códigos Unicode de cada posición, y eso tiene una consecuencia que debes conocer de primera mano: las mayúsculas van antes que las minúsculas, porque sus códigos son menores ('Z' es 90 y 'a' es 97):
Ni Zebra antes que apple ni Ada antes que ada es lo que un humano esperaría del alfabeto: es lo que espera un ordenador del código de caracteres. Cuando necesites el orden «de persona», la receta es comparar las versiones normalizadas: a.toLowerCase().compareTo(b.toLowerCase()).
¿Y para qué sirve un método así? Para ordenar. En la UD2, si ordenaste, ordenaste números a mano, con la burbuja; en la sección 9, la clase java.util.Arrays convertirá esa tarea en una sola llamada — y Arrays.sort sobre un array de cadenas usará exactamente compareTo para decidir quién va antes, sin que tú escribas comparación alguna:
Cuando en la UD5 ordenes colecciones completas, el motor será el mismo: el orden de las cadenas ya lo defines tú hoy.
isEmpty e isBlank: las tres formas de «no haber nada»
Última pareja del grupo, y la más nueva: isEmpty responde si la cadena tiene cero caracteres; isBlank (Java 11) responde si no tiene nada que no sean espacios — vacía o compuesta solo de espacios en blanco. La diferencia es pequeña y deliciosa:
Con las tres piezas juntas puedes por fin escribir el patrón completo de validación de una entrada, que distingue los tres estados de «no haber nada» y es el estándar que usarás en la práctica guiada:
El || cortocircuita: si la referencia es null, la segunda condición ni se evalúa y el NullPointerException nunca llega a nacer. Y isBlank atrapa de un golpe tanto la cadena vacía como la de puros espacios: es el detector de «el usuario le dio a Enter sin querer». Tabla final de los tres estados, para que el mapa quede completo:
| Estado | == null |
isEmpty() |
isBlank() |
Ejemplo |
|---|---|---|---|---|
| Sin objeto | true |
NPE si lo llamas | NPE si lo llamas | una referencia sin asignar |
| Cadena vacía | false |
true |
true |
"" |
| Cadena de espacios | false |
false |
true |
" " |
| Cadena con contenido | false |
false |
false |
"ada" |
3.9 Plantillas de texto: String.format y formatted
Llega el último verbo, y con él la promesa que la sección 2 dejó escrita al hilo de su «error 3»: el método format llegaría a fondo en la sección 3. Hoy cobramos esa deuda entera, porque las plantillas son la forma adulta de construir texto con mezcla de tipos — y porque ya usaste ambas versiones sin criterio en la UD1; ahora les pones fundamento.
El problema que resuelven lo conoces de memoria: componer una línea mezclando texto fijo y valores de tipos distintos con + es verboso y frágil — y con la trampa de asociatividad de la subsección 3.7 acechando. La solución es declarar una plantilla con huecos y rellenarla:
Las dos llamadas hacen exactamente lo mismo — misma plantilla, mismo motor por dentro — y la pareja es una clase magistral de la sección 2 en miniatura: format es estático (el receptor es la clase String, y ya sabes que escribirlo sobre una instancia es el «error cobarde» que IntelliJ subraya); formatted es de instancia (el receptor es la propia plantilla, que llega con Java 15, la misma versión de los text blocks de la UD1). La regla de estilo para elegir es sencilla: si la plantilla es un literal que tienes delante — el caso más común —, "...".formatted(...) se lee de maravilla; si la plantilla viene de una variable o la construyes a medias, String.format(...) es la forma natural.
Los especificadores: el idioma de los huecos
Los huecos se marcan con % seguido de una letra que anuncia qué tipo de valor espera el hueco. Este es el diccionario esencial:
| Especificador | Espera | Ejemplo de llamada | Resultado |
|---|---|---|---|
%s |
cualquier cosa: texto, números, objetos | "%s vale %s".formatted("x", 3.5) |
x vale 3.5 |
%d |
enteros (int, long...) |
"%d filas".formatted(42) |
42 filas |
%f |
reales (double, float) |
"%f".formatted(2.5) |
2,500000 |
%.2f |
real con dos decimales | "%.2f".formatted(2.5) |
2,50 |
%n |
salto de línea del sistema | "a%nb".formatted() |
a y b en dos líneas |
%% |
un % literal |
"El %d%%".formatted(50) |
El 50% |
Cuatro observaciones de quien ha visto muchos errores. Primera: %s es el comodín universal — convierte a texto lo que le eches —, pero %d y %f son exigentes: %d solo acepta enteros y %f solo reales. Segunda: %.2f es la forma estándar de pintar dinero y notas — recorta y redondea a los decimales pedidos —; sin precisión, %f pinta seis decimales por defecto. Tercera: %% es la única forma de escribir un % dentro de una plantilla, y su ausencia es un error classicísimo cuando la plantilla lleva porcentajes. Cuarta: fíjate en que los ejemplos de %f muestran coma decimal, no punto — sobre eso va justamente el aviso que sigue.
El locale: la coma y el punto de tu ordenador
Las plantillas usan la configuración regional (locale) de la máquina para decidir el separador decimal: en un sistema configurado en español, %.2f pinta 2,50 — con coma, como en la calle —; en uno inglés, 2.50. Es el mismo fenómeno del Scanner y su separador decimal que viste en la UD1, ahora por el lado de la salida. Para tus programas de consola de este curso, la coma es correcta y esperable. Si algún día necesitas forzar el punto (p. ej. para generar datos que leerá otra máquina), existe la versión con locale explícito, String.format(java.util.Locale.ROOT, "%.2f", 2.5): guárdala en la mochila por si acaso, pero no la uses salvo necesidad real.
El error estrella de las plantillas
La exigencia de %d y %f no es caprichosa, y el error que provoca es el más enseñado de todos: mezclar el especificador con el tipo equivocado no falla al compilar — la plantilla es una cadena más, y el compilador no la analiza — sino al ejecutarse, con una excepción de formato que llega sin aviso previo:
Que el error aparezca en ejecución y no en compilación es la lección de fondo: las plantillas se comprueban con pruebas, no depositando confianza en el compilador. Acostúmbrate a ejecutar cada plantilla nueva con datos reales antes de darla por buena.
Plantillas multilínea: text blocks + formatted
La pareja que abre la UD1 y esta sección se junta por fin: un text block como plantilla y formatted para rellenarla es el idiom moderno de Java para informes y fichas completas. Fíjate en dos detalles de conducción: se usa IO.print (no println) porque el propio bloque ya termina en salto de línea, y el %n interior genera los saltos del sistema:
Cuatro huecos, tres especificadores distintos, una expresión aritmética (precio * unidades) calculada antes de rellenar, y el texto alineado de serie. Esta es la apariencia que tendrá tu práctica guiada al terminar la sección.
Que significan los tres puntitos de la firma
Si miras la firma de formatted o format en la Javadoc, verás formatted(Object... args). Ese ... se llama varargs: significa «recibe un número variable de argumentos». Gracias a él puedes rellenar una plantilla con uno, tres o diez valores sin que exista una sobrecarga distinta por cada caso — no es sobrecarga de la sección 2, es otra mecánica —. Y como el hueco genérico es Object, cada valor primitivo que pasas viaja encajonado en su versión objeto por el autoboxing, cuyo capítulo completo vive en la sección 8. Dos detalles que ahora solo necesitas reconocer.
Extras de alineacion que veras en codigo ajeno
Entre el % y la letra pueden intercalarse números que controlan la anchura y la alineación: %10s reserva diez caracteres para el texto (alineado a la derecha), %-10s lo alinea a la izquierda, y %5d hace lo propio con enteros. Se usan para columnas alineadas en informes de consola — "%-10s %5d".formatted("Ada", 47) produce Ada 47 —. Hoy los reconoces; los practicarás cuando la sección de ejercicios pida una tabla bonita.
3.10 Práctica guiada: el protocolo de registro
Toca poner a trabajar toda la sección en un programa de verdad, y el escenario es de los más realistas que existen: el registro de un usuario en un sistema, con sus validaciones de texto. Construiremos el protocolo de registro del aula virtual del centro: un programa que pide nombre de usuario y contraseña, los valida con las reglas de la sección y termina emitiendo un informe con formato. El código base te lo doy entero; las mejoras las construirás tú, con la Javadoc y el autocompletado como brújula — igual que en la sección 2.
Escribe este programa en un fichero nuevo, Registro.java, y ejecútalo probando entradas correctas e incorrectas:
Pausa para leerlo con las gafas de la sección: cuatro reglas de validación, cada una ejerciendo un método distinto — length, contains, equals — sobre cadenas concretas, con la lógica booleana de la UD1 soldando todo. Nada nuevo que no hayas visto hoy; el programa base es deliberadamente sencillo para que las misiones tengan espacio para crecer. Ellas van ahora, en orden y sin saltarse la parte incómoda: intenta resolverlas antes de abrir la solución plegable.
Misión 1 — Los espacios traidores. Ejecuta el programa tecleando " ada " (con espacios delante y detrás) como usuario. La regla de longitud cuenta esos espacios como letras y puede validar un usuario que en realidad está sucio. Haz que el programa limpie ambas entradas antes de validar nada.
Solución a la misión 1
La herramienta es strip(), y el detalle fino es la reasignación: por la inmutabilidad (subsección 3.2), usuario.strip() no limpia a usuario — devuelve una cadena limpia nueva que hay que recoger:
Colócalas justo después de los readln y el resto del programa no cambia una coma: las cuatro reglas ahora evalúan las cadenas limpias. Es el patrón «limpiar la entrada en la frontera»: se hace una vez, al entrar, y todo lo demás trabaja con datos decentes. Con " ada " la regla 1 pasa a evaluar "ada" — y a fallar por longitud, que es exactamente lo que debe pasar.
Misión 2 — El detector de dominios. Añade al registro una tercera entrada: el correo. Valida que contiene una arroba y, si la contiene, muestra por separado el dominio (todo lo que va después de la arroba). Para "ada@ies.thiar.es", el programa debe mostrar ies.thiar.es.
Solución a la misión 2
Es el patrón estrella de la sección, indexOf + substring, con su comprobación del -1:
El + 1 es la aritmética de índices de la subsección 3.6: la arroba vive en arroba, y el dominio empieza justo después. Existe un camino alternativo con split — correo.split("@")[1] — pero es más frágil: si el correo tiene dos arrobas te da la pieza del medio, y si no tiene ninguna, revienta con ArrayIndexOutOfBoundsException (índice 1 en un array de una pieza). El patrón indexOf + -1 + substring controla todos los casos: es el que debe salirte de forma natural.
Misión 3 — La caza de la mayúscula. Añade una quinta regla: la contraseña debe contener al menos una letra mayúscula. Pista: te hará falta un bucle, charAt y la sorpresa numérica del char de la subsección 3.6.
Solución a la misión 3
Recorremos la cadena posición a posición y comparamos cada carácter con el rango de las mayúsculas — que son, recuerda, números consecutivos del 65 ('A') al 90 ('Z'):
Dos detalles de factura. El break del bloque if no es obligatorio — el bucle seguiría hasta el final con el mismo resultado — pero es cortesía profesional: no sigas buscando lo que ya has encontrado. Y el rango c >= 'A' && c <= 'Z' funciona porque los códigos de las mayúsculas son consecutivos en Unicode; la misma idea con c >= 'a' && c <= 'z' detecta minúsculas, y con los rangos del '0' al '9', dígitos — tres validaciones clásicas que ya sabes escribir sin regex (que llegan en la UD5 para esto mismo).
Misión 4 — El informe con formato. Sustituye los cuatro IO.println provisionales por un informe digno: un marco de signos =, el nombre del usuario, cuántas de las cinco reglas supera (recuéntalas en un int) y un porcentaje de fortaleza con un decimal. Para "ada" con 4 de 5 reglas superadas, algo así:
Solución a la misión 4
Reunión de toda la sección en un solo bloque: repeat para el marco, formatted para las líneas con huecos, %d para las cuentas, %.1f para el porcentaje y %% para el símbolo. Primero cuenta las reglas — un int que suma 1 por cada booleano cierto — y luego formatea:
Fíjate en las dos artimañas: el mismo "=".repeat(32) se pasa tres veces como argumento a los tres huecos %s del marco (las plantillas no repiten por ti: los argumentos se consumen por orden, uno por hueco); y la operación superadas * 100.0 / 5 usa 100.0 para forzar división real — con 100 entero, 4 * 100 / 5 daría 80, y el %.1f recibiría un int que no le corresponde. El %% final pinta el símbolo de porcentaje. Con locale español, la fortaleza sale con coma decimal (80,0%): el aviso de la subsección 3.9 en acción.
Misión 5 — El arbitraje final. Dos exigencias nuevas del protocolo: primera, si el usuario deja la entrada vacía o de puros espacios, el programa debe rechazarla con un mensaje antes de validar nada; segunda, si el usuario teclea dos alias posibles, el sistema se queda con el que va antes alfabéticamente.
Solución a la misión 5
La primera parte es el patrón de la subsección 3.8 con isBlank; la segunda, compareTo con su signo y el ternario de la UD1:
El <= 0 es deliberado: si los dos alias son idénticos, compareTo devuelve 0 y da igual cuál se elija, así que quedarnos con el primero es una decisión limpia. Y si quieres el arbitraje «de persona» — ignorando la caja —, ya sabes la receta de la subsección 3.8: comparar alias1.toLowerCase().compareTo(alias2.toLowerCase()).
Si has completado las cinco misiones, mira atrás: has validado entradas con isBlank, limpiado con strip, buscado con indexOf y contains, extraído con substring y charAt con su bucle, comparado con equals y compareTo, y emitido un informe con repeat, formatted, %d, %.1f y %%. Eso es, literalmente, el temario de la sección entero funcionando en un programa de veinte líneas. Y una reflexión final de profesor: casi todo lo que has escrito tenía trampa — la reasignación del strip, el -1 del indexOf, el final excluido del substring, el %% del formato — y ninguna te ha pillado, porque ya conoces el terreno. El examen de esta unidad se escribe con estas mismas piezas.
3.11 Resumen, glosario y checklist
Si tuvieras que quedarte con diez ideas de esta sección, que fueran estas:
Stringes una clase dejava.lang,finaly con más de setenta métodos públicos; sus literales son objetos y sus índices van de0alength() - 1, como los de un array.- Es inmutable: ningún método modifica la cadena que recibe el mensaje; todos devuelven cadenas nuevas. Conservar un resultado exige reasignar:
frase = frase.strip();. - La inmutabilidad hace posible el pool de cadenas: los literales idénticos comparten un único objeto en una zona especial del montón;
new String(...)fabrica un objeto fuera del pool. Por eso==a veces «funciona»: no te fíes; contenido se compara conequals. - Buscar:
contains,startsWith,endsWith(boolean) eindexOf/lastIndexOf(posición,-1si no aparece), siempre sensibles a mayúsculas; el patrón del-1es obligatorio antes de usar la posición. - Transformar:
toUpperCase,toLowerCase,strip(moderno) frente atrim(histórico),replaceliteral (dos versiones:chary texto) yreplaceAllcon regex — recetas básicas hoy, lenguaje completo en la UD5. - Extraer:
substring(inicial, final)con el final excluido (la longitud del trozo esfinal - inicial);charAt(i)devuelve unchar, que por dentro es un número;split(separador)devuelve unString[];chars()abre la puerta de los streams (UD5). - Construir:
String.join(estático, al receptor clase),repeat(instancia, Java 11), y el+con su trampa de asociatividad —"Total: " + 1 + 2daTotal: 12. - Preguntar:
equals/equalsIgnoreCasepara igualdad (con el patrón defensivo delnull),compareTopara el orden lexicográfico — las mayúsculas van antes — eisEmpty/isBlankpara distinguir""de" ". - Formatear:
String.format(estático) yformatted(instancia) son la misma máquina; los huecos son%s,%d,%f,%.2f,%ny%%;%dcon undoublerevienta en ejecución, no al compilar; y el locale español pinta la coma decimal. - Los patrones de la sección: reasignar para conservar, comprobar el
-1, normalizar y luego buscar o comparar, limpiar la entrada en la frontera. Con esos cuatro y el repertorio de métodos, las validaciones de texto de la práctica guiada se escriben solas.
Glosario de la sección
| Término | Definición en una línea |
|---|---|
| Inmutabilidad | Propiedad de String: su contenido no puede cambiar tras crearse; los métodos devuelven cadenas nuevas |
| Pool de cadenas | Zona del montón con una única copia de cada cadena literal del programa |
| String interning | Mecanismo de reutilización del pool: literales idénticos comparten objeto |
| Literal | Cadena escrita entre comillas en el código fuente |
| Reasignar | Recoger en la variable el objeto nuevo que devuelve un método (frase = frase.trim()) |
| Cadena en blanco | Cadena vacía o compuesta solo de espacios en blanco (isBlank la detecta) |
| Orden lexicográfico | Orden de las cadenas comparando códigos Unicode posición a posición (el de compareTo) |
| Expresión regular | Mini-lenguaje de patrones de texto; lo usan replaceAll y split (a fondo en UD5) |
char |
Primitivo de un único carácter; por dentro, un número de 16 bits (su código Unicode) |
| Índice | Posición de un carácter en la cadena: de 0 a length() - 1 |
StringIndexOutOfBoundsException |
Excepción al usar un índice fuera de rango en charAt o substring |
| Plantilla | Cadena con huecos (%s, %d...) que se rellenan con format o formatted |
| Especificador | Marcador de hueco: %s texto, %d entero, %f real, %n salto, %% porcentaje |
| Precisión | Número de decimales de un hueco real: %.2f pinta dos |
| Locale | Configuración regional de la máquina: decide el separador decimal de %f |
| varargs | Parámetro de número variable de argumentos, escrito ... en las firmas |
CharSequence |
Contrato «secuencia de caracteres» que String implementa; aparece en firmas |
| text block | Cadena multilínea entre """ (UD1), perfecta como plantilla de formatted |
Patrón del -1 |
Comprobar el fracaso de indexOf antes de usar la posición devuelta |
| Normalizar | Llevar las cadenas a un común (minúsculas, sin bordes) antes de buscar o comparar |
Checklist: al terminar, debes ser capaz de...
- [ ] Explicar por qué
frase.toUpperCase()no cambiafrase, y corregir un programa que olvide la reasignación (CE 2.4). - [ ] Explicar el experimento
a == b,a == c,a.equals(c)con literales ynew String, usando las palabras «pool» e «identidad» (CE 2.4, base de CE 2.1). - [ ] Escribir una búsqueda con
indexOfque compruebe el-1antes de usar la posición (CE 2.2, CE 2.6). - [ ] Predecir el resultado de un
substringde dos argumentos, con el final excluido, dibujando la fila numerada (CE 2.2). - [ ] Elegir entre
replaceyreplaceAllpara un caso dado, y decir cuál interpreta regex y dónde se estudian a fondo (CE 2.4, CE 2.7). - [ ] Recorrer una cadena carácter a carácter con
charAten un bucle, detectando mayúsculas o dígitos por rangos dechar(CE 2.2, CE 2.6). - [ ] Elegir entre
equals,equalsIgnoreCaseycompareTosegún la pregunta que se quiera hacer, y usarisBlankpara rechazar entradas vacías (CE 2.4). - [ ] Emitir un informe con
formattedusando%s,%d,%.2f,%%yrepeatpara el marco (CE 2.2, CE 2.5, CE 2.6). - [ ] Explicar por qué
"Total: " + 1 + 2imprimeTotal: 12y corregirlo con paréntesis (CE 2.2). - [ ] Leer la ficha Javadoc de un método de
Stringque no hayas visto en clase y deducir qué devuelve y qué puede lanzar (CE 2.7, CE 2.9).
Lo que hoy quedó citado, y dónde vive
| Tema mencionado hoy | Dónde se desarrolla |
|---|---|
Concatenar en bucles y StringBuilder / StringJoiner |
Sección 7 de esta unidad |
Expresiones regulares completas (Pattern, Matcher, validaciones) |
UD5, sección 10 |
Streams: chars(), Random.ints() |
UD5 |
Autoboxing detrás de format(Object...) |
Sección 8 de esta unidad |
equals y hashCode cuando los objetos trabajan como claves |
UD5, con las colecciones |
Fechas formateadas con DateTimeFormatter |
Sección 6 de esta unidad |
Interfaces como CharSequence |
UD7 |
3.12 Autoevaluación de la sección
¿Esto no era la sección 11?
Una vez más: el test oficial de repaso de la unidad vive en la sección 11, dividido por secciones para el repaso de conjunto. Este bloque es una autoevaluación rápida para hacer justo después de leer, sin material y sin presión: si fallas algo, vuelve a la subsección correspondiente antes de continuar. El orden de las preguntas sigue el orden de la sección.
Pregunta 1
Tras ejecutar String frase = "hola"; frase.toUpperCase();, ¿qué contiene la variable frase?
Pregunta 3
Con String a = "java"; String c = new String("java");, ¿qué ocurre con a == c y a.equals(c)?
Cuando las diez preguntas te salgan sin dudar, has cerrado el capítulo más transitado de la unidad: manejas la clase más usada del API con sus trampas desactivadas. Estás listo para la sección 4 — La clase Math, donde conocerás la clase de utilidad por excelencia de Java: sin objetos, sin new y con todo su repertorio estático — constantes como Math.PI, funciones como sqrt, pow o el flamante clamp, y la aritmética segura que detecta desbordamientos por ti.