Java

Tutorial Java: Métodos de la clase String

Java métodos String: manipulación de cadenas. Domina la manipulación de cadenas en Java utilizando métodos de String con ejemplos detallados.

Aprende Java y certifícate

Métodos de manipulación básica (length, charAt, substring)

La clase String en Java proporciona un conjunto de métodos fundamentales para manipular cadenas de texto. Estos métodos básicos permiten examinar y extraer información de las cadenas sin modificar la cadena original, ya que los objetos String son inmutables en Java.

Método length()

El método length() se utiliza para obtener el número de caracteres que componen una cadena. Este método no recibe parámetros y devuelve un valor entero que representa la longitud de la cadena.

String texto = "Programación en Java";
int longitud = texto.length(); // Devuelve 20
System.out.println("La cadena tiene " + longitud + " caracteres");

length() cuenta todos los caracteres, incluyendo espacios en blanco y caracteres especiales. Este método resulta esencial cuando se necesita:

  • Validar la longitud de entradas de usuario
  • Iterar sobre cada carácter de una cadena
  • Verificar si una cadena está vacía (cuando length() devuelve 0)
String password = "abc123";
if (password.length() < 8) {
    System.out.println("La contraseña debe tener al menos 8 caracteres");
}

Método charAt()

El método charAt(int index) permite acceder a un carácter específico dentro de una cadena mediante su posición o índice. Los índices en Java comienzan en 0, por lo que el primer carácter está en la posición 0, el segundo en la posición 1, y así sucesivamente.

String palabra = "Java";
char primeraLetra = palabra.charAt(0); // Devuelve 'J'
char ultimaLetra = palabra.charAt(palabra.length() - 1); // Devuelve 'a'

Si se intenta acceder a un índice fuera del rango válido (negativo o mayor o igual a la longitud de la cadena), se lanza una excepción StringIndexOutOfBoundsException. Por ello, se recomienda verificar siempre que el índice sea válido antes de llamar a este método.

String texto = "Hola";
int indice = 10;

if (indice >= 0 && indice < texto.length()) {
    char caracter = texto.charAt(indice);
    System.out.println("El carácter en la posición " + indice + " es: " + caracter);
} else {
    System.out.println("Índice fuera de rango");
}

El método charAt() se utiliza a menudo para:

  • Recorrer una cadena carácter por carácter
  • Verificar si una cadena contiene ciertos caracteres específicos
  • Construir algoritmos de procesamiento de texto
String frase = "Java es un lenguaje de programación";
int contadorVocales = 0;

for (int i = 0; i < frase.length(); i++) {
    char c = Character.toLowerCase(frase.charAt(i));
    if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
        contadorVocales++;
    }
}

System.out.println("La frase contiene " + contadorVocales + " vocales");

Método substring()

El método substring() permite extraer una subcadena de una cadena existente. Existen dos variantes de este método:

  • substring(int beginIndex): Extrae desde la posición indicada hasta el final de la cadena.
  • substring(int beginIndex, int endIndex): Extrae desde la posición inicial hasta la posición final (sin incluir el carácter en la posición final).
String mensaje = "Aprendiendo Java";

// Extrae desde la posición 12 hasta el final
String lenguaje = mensaje.substring(12); // Devuelve "Java"

// Extrae desde la posición 0 hasta la 11 (sin incluir el carácter en la posición 11)
String accion = mensaje.substring(0, 11); // Devuelve "Aprendiendo"

El índice final no se incluye en el resultado. Si se quiere incluir el carácter en la posición n, se debe usar n+1 como índice final.

Al igual que con charAt(), si se proporcionan índices fuera del rango válido, se lanza una StringIndexOutOfBoundsException. Además, si el índice inicial es mayor que el índice final, también se produce una excepción.

String url = "https://www.ejemplo.com/recursos/archivo.pdf";

// Extraer el protocolo
int indiceSeparador = url.indexOf("://");
if (indiceSeparador != -1) {
    String protocolo = url.substring(0, indiceSeparador);
    System.out.println("Protocolo: " + protocolo); // Muestra "https"
}

// Extraer la extensión del archivo
int indicePunto = url.lastIndexOf(".");
if (indicePunto != -1) {
    String extension = url.substring(indicePunto + 1);
    System.out.println("Extensión: " + extension); // Muestra "pdf"
}

El método substring() se utiliza comúnmente para:

  • Extraer partes significativas de una cadena (como dominios de URLs)
  • Procesar formatos específicos (como fechas o códigos)
  • Implementar algoritmos de análisis de texto
// Extraer componentes de una fecha en formato "dd/mm/aaaa"
String fecha = "23/04/2023";
String dia = fecha.substring(0, 2);
String mes = fecha.substring(3, 5);
String anio = fecha.substring(6);

System.out.println("Día: " + dia);
System.out.println("Mes: " + mes);
System.out.println("Año: " + anio);

Combinación de métodos básicos

Estos métodos de manipulación básica se pueden combinar para realizar operaciones más complejas sobre cadenas de texto:

String correo = "usuario@dominio.com";

// Verificar si es un correo válido (simplificado)
boolean esValido = correo.length() > 5 && 
                  correo.indexOf('@') > 0 && 
                  correo.indexOf('@') < correo.length() - 3;

// Extraer nombre de usuario y dominio
if (esValido) {
    int posicionArroba = correo.indexOf('@');
    String usuario = correo.substring(0, posicionArroba);
    String dominio = correo.substring(posicionArroba + 1);
    
    System.out.println("Usuario: " + usuario);
    System.out.println("Dominio: " + dominio);
    
    // Verificar primera letra del usuario
    char primeraLetra = usuario.charAt(0);
    if (Character.isLetter(primeraLetra)) {
        System.out.println("El nombre de usuario comienza con una letra");
    }
}

En este ejemplo se combinan los métodos length(), indexOf(), substring() y charAt() para analizar y validar una dirección de correo electrónico de forma básica.

Métodos de búsqueda y comparación

La clase String en Java ofrece un conjunto de métodos para buscar información dentro de cadenas y realizar comparaciones entre ellas. Estos métodos son fundamentales para el procesamiento de texto y la validación de datos en aplicaciones.

Métodos de búsqueda

Los métodos de búsqueda permiten localizar subcadenas o caracteres específicos dentro de un String. Estos métodos devuelven la posición donde se encuentra el elemento buscado o un valor negativo si no se encuentra.

indexOf() y lastIndexOf()

El método indexOf() busca la primera ocurrencia de un carácter o subcadena, mientras que lastIndexOf() busca la última ocurrencia:

String texto = "Java es un lenguaje versátil. Java es popular.";

// Búsqueda básica
int primera = texto.indexOf("Java");       // Devuelve 0
int ultima = texto.lastIndexOf("Java");    // Devuelve 25

// Búsqueda a partir de una posición específica
int segunda = texto.indexOf("Java", 1);    // Devuelve 25
int anterior = texto.lastIndexOf("es", 20); // Devuelve 6

Estos métodos son extremadamente útiles para analizar y extraer partes de textos estructurados:

String html = "<div><p>Contenido del párrafo</p></div>";

int inicioParrafo = html.indexOf("<p>") + 3;
int finParrafo = html.indexOf("</p>");

if (inicioParrafo != -1 && finParrafo != -1) {
    String contenido = html.substring(inicioParrafo, finParrafo);
    System.out.println("Texto extraído: " + contenido);
}

contains()

El método contains() verifica si una cadena contiene una subcadena específica, devolviendo un valor booleano:

String mensaje = "Aprende a programar en Java";
boolean contienePalabra = mensaje.contains("Java");    // true
boolean contienePython = mensaje.contains("Python");   // false

Se utiliza a menudo para validaciones simples:

String correo = "usuario@dominio.com";

if (!correo.contains("@")) {
    System.out.println("Correo electrónico inválido");
} else {
    System.out.println("Formato de correo válido");
}

startsWith() y endsWith()

Estos métodos verifican si una cadena comienza o termina con una subcadena específica:

String archivo = "documento.pdf";

if (archivo.startsWith("doc")) {
    System.out.println("El archivo comienza con 'doc'");
}

if (archivo.endsWith(".pdf")) {
    System.out.println("Es un archivo PDF");
}

También se pueden especificar posiciones de inicio para startsWith():

String url = "https://www.ejemplo.com";
boolean esSegura = url.startsWith("https");  // true

String ruta = "/usuarios/perfil/123";
boolean esPerfil = ruta.startsWith("/perfil", 9);  // true

Métodos de comparación

Los métodos de comparación permiten determinar la relación entre cadenas, lo que es esencial para ordenamiento, búsqueda y validación.

equals() y equalsIgnoreCase()

El método equals() compara si dos cadenas tienen exactamente el mismo contenido, mientras que equalsIgnoreCase() realiza la misma comparación ignorando mayúsculas y minúsculas:

String usuario1 = "admin";
String usuario2 = "Admin";

boolean sonIguales = usuario1.equals(usuario2);             // false
boolean sonIgualesIgnorandoMayus = usuario1.equalsIgnoreCase(usuario2);  // true

Nunca se debe usar el operador == para comparar el contenido de Strings, ya que este compara referencias de objetos, no su contenido:

String a = "hola";
String b = new String("hola");

System.out.println(a == b);           // false (referencias distintas)
System.out.println(a.equals(b));      // true (mismo contenido)

compareTo() y compareToIgnoreCase()

Estos métodos comparan cadenas lexicográficamente (orden alfabético) y devuelven un entero que indica su relación:

String palabra1 = "banana";
String palabra2 = "manzana";

int resultado = palabra1.compareTo(palabra2);  // Negativo: palabra1 va antes

El valor devuelto indica:

  • Negativo: la primera cadena va antes alfabéticamente
  • Cero: las cadenas son iguales
  • Positivo: la primera cadena va después alfabéticamente

Estos métodos son fundamentales para ordenar colecciones de cadenas:

List<String> frutas = new ArrayList<>(List.of("Manzana", "banana", "Pera", "kiwi"));

// Ordenar ignorando mayúsculas y minúsculas
frutas.sort((a, b) -> a.compareToIgnoreCase(b));

System.out.println(frutas);  // [banana, kiwi, Manzana, Pera]

regionMatches()

Este método permite comparar regiones específicas de dos cadenas, ofreciendo gran flexibilidad:

String texto1 = "Programación en Java";
String texto2 = "Curso de JAVA avanzado";

// Comparar "Java" con "JAVA" ignorando mayúsculas
boolean coincide = texto1.regionMatches(
    true,       // ignorar mayúsculas/minúsculas
    15,         // índice de inicio en texto1
    texto2,     // segunda cadena
    9,          // índice de inicio en texto2
    4           // número de caracteres a comparar
);

System.out.println(coincide);  // true

Patrones de uso comunes

Validación de formatos

Se pueden combinar métodos de búsqueda y comparación para validar formatos específicos:

public boolean esFormatoFechaValido(String fecha) {
    // Verificar formato DD/MM/AAAA
    if (fecha.length() != 10) return false;
    if (fecha.charAt(2) != '/' || fecha.charAt(5) != '/') return false;
    
    try {
        int dia = Integer.parseInt(fecha.substring(0, 2));
        int mes = Integer.parseInt(fecha.substring(3, 5));
        int anio = Integer.parseInt(fecha.substring(6));
        
        return dia >= 1 && dia <= 31 && mes >= 1 && mes <= 12 && anio > 0;
    } catch (NumberFormatException e) {
        return false;
    }
}

Búsqueda de patrones simples

public List<Integer> encontrarTodasLasOcurrencias(String texto, String patron) {
    List<Integer> posiciones = new ArrayList<>();
    int indice = 0;
    
    while ((indice = texto.indexOf(patron, indice)) != -1) {
        posiciones.add(indice);
        indice += patron.length();
    }
    
    return posiciones;
}

Extracción de información estructurada

public Map<String, String> extraerParametrosURL(String url) {
    Map<String, String> parametros = new HashMap<>();
    
    int indiceQuery = url.indexOf('?');
    if (indiceQuery == -1) return parametros;
    
    String query = url.substring(indiceQuery + 1);
    String[] pares = query.split("&");
    
    for (String par : pares) {
        int indiceIgual = par.indexOf('=');
        if (indiceIgual != -1) {
            String clave = par.substring(0, indiceIgual);
            String valor = par.substring(indiceIgual + 1);
            parametros.put(clave, valor);
        }
    }
    
    return parametros;
}

Consideraciones de rendimiento

Al trabajar con métodos de búsqueda y comparación, se deben tener en cuenta algunas consideraciones de rendimiento:

  • Los métodos indexOf() y contains() tienen una complejidad de O(n*m) donde n es la longitud de la cadena y m la longitud del patrón.
  • Para búsquedas complejas o múltiples en el mismo texto, se recomienda utilizar Pattern y Matcher de la API de expresiones regulares.
  • Las comparaciones con equalsIgnoreCase() son más costosas que con equals() debido a la conversión de caracteres.
// Para búsquedas repetitivas, compilar el patrón mejora el rendimiento
Pattern patron = Pattern.compile("Java", Pattern.CASE_INSENSITIVE);
Matcher matcher = patron.matcher(textoLargo);

while (matcher.find()) {
    System.out.println("Encontrado en posición: " + matcher.start());
}

Métodos de transformación (toUpperCase, replace, split)

La clase String en Java ofrece un conjunto de métodos de transformación que permiten modificar el contenido de las cadenas de texto. Debido a la inmutabilidad de los objetos String, estos métodos no alteran la cadena original, sino que devuelven una nueva cadena con las transformaciones aplicadas.

Conversión de mayúsculas y minúsculas

Los métodos toUpperCase() y toLowerCase() permiten convertir todos los caracteres de una cadena a mayúsculas o minúsculas respectivamente.

String texto = "Java Programming";

String mayusculas = texto.toUpperCase(); // "JAVA PROGRAMMING"
String minusculas = texto.toLowerCase(); // "java programming"

System.out.println(mayusculas);
System.out.println(minusculas);

Estos métodos son útiles para realizar comparaciones sin distinguir entre mayúsculas y minúsculas:

String entrada = "Java";
String busqueda = "java";

// Forma incorrecta (distingue mayúsculas/minúsculas)
if (entrada.equals(busqueda)) {
    System.out.println("Coincidencia encontrada");
}

// Forma correcta (no distingue mayúsculas/minúsculas)
if (entrada.toLowerCase().equals(busqueda.toLowerCase())) {
    System.out.println("Coincidencia encontrada");
}

También se puede especificar una configuración regional (Locale) para manejar correctamente caracteres específicos de diferentes idiomas:

String texto = "españa";
String mayusculasEspañol = texto.toUpperCase(Locale.forLanguageTag("es-ES")); // "ESPAÑA"

Reemplazo de caracteres y subcadenas

Java ofrece varios métodos para reemplazar caracteres o subcadenas dentro de un String.

replace()

El método replace() tiene dos variantes:

  • replace(char oldChar, char newChar): Reemplaza todas las ocurrencias de un carácter por otro.
  • replace(CharSequence target, CharSequence replacement): Reemplaza todas las ocurrencias de una subcadena por otra.
String texto = "Hello World";

// Reemplazo de caracteres
String textoModificado = texto.replace('l', 'L'); // "HeLLo WorLd"

// Reemplazo de subcadenas
String nuevoTexto = texto.replace("World", "Java"); // "Hello Java"

replaceFirst() y replaceAll()

Estos métodos utilizan expresiones regulares para realizar reemplazos más avanzados:

String codigo = "var x = 10; var y = 20;";

// Reemplaza solo la primera ocurrencia
String primerReemplazo = codigo.replaceFirst("var", "let"); // "let x = 10; var y = 20;"

// Reemplaza todas las ocurrencias
String todosReemplazos = codigo.replaceAll("var", "let"); // "let x = 10; let y = 20;"

El método replaceAll() es muy potente cuando se combina con expresiones regulares:

String texto = "El precio es 15.99€ y el descuento es 3.50€";

// Reemplazar todos los números decimales por su valor redondeado
String resultado = texto.replaceAll("(\\d+)\\.\\d+", "$1");
System.out.println(resultado); // "El precio es 15€ y el descuento es 3€"

Casos de uso comunes

// Normalización de texto para búsquedas
String busqueda = " Machine Learning ";
String normalizado = busqueda.trim().toLowerCase().replace(" ", "-");
System.out.println(normalizado); // "machine-learning"

// Sanitización básica de entrada HTML
String comentarioUsuario = "<script>alert('XSS')</script>Buen artículo!";
String seguro = comentarioUsuario.replace("<", "&lt;").replace(">", "&gt;");
System.out.println(seguro); // "&lt;script&gt;alert('XSS')&lt;/script&gt;Buen artículo!"

División de cadenas con split()

El método split(String regex) divide una cadena en un array de subcadenas utilizando una expresión regular como delimitador.

String frase = "Java es un lenguaje de programación versátil";
String[] palabras = frase.split(" ");

// Resultado: ["Java", "es", "un", "lenguaje", "de", "programación", "versátil"]
for (String palabra : palabras) {
    System.out.println(palabra);
}

Se puede limitar el número de divisiones especificando un segundo parámetro:

String datos = "Juan,25,Madrid,Ingeniero";
String[] campos = datos.split(",", 3);

// Resultado: ["Juan", "25", "Madrid,Ingeniero"]
for (String campo : campos) {
    System.out.println(campo);
}

Consideraciones con caracteres especiales

Cuando se utilizan caracteres que tienen significado especial en expresiones regulares (como ., *, +, ?, etc.), se deben escapar con \\:

String ruta = "C:\\Documentos\\Java\\ejemplos.txt";
String[] partes = ruta.split("\\\\");

// Resultado: ["C:", "Documentos", "Java", "ejemplos.txt"]

Aplicaciones prácticas

El método split() es fundamental para procesar datos estructurados:

// Procesamiento de CSV
String linea = "Juan,Pérez,30,Madrid";
String[] datos = linea.split(",");
String nombre = datos[0];
String apellido = datos[1];
int edad = Integer.parseInt(datos[2]);
String ciudad = datos[3];

// Análisis de logs
String logEntry = "2023-05-15 14:30:45 [ERROR] Connection refused: 192.168.1.1:8080";
String[] partes = logEntry.split(" ", 4);
String fecha = partes[0];
String hora = partes[1];
String nivel = partes[2].replace("[", "").replace("]", "");
String mensaje = partes[3];

Combinación de métodos de transformación

Los métodos de transformación se pueden encadenar para realizar operaciones complejas en una sola línea:

String entrada = "  usuario@DOMINIO.com  ";

String normalizado = entrada.trim()           // Eliminar espacios
                           .toLowerCase()     // Convertir a minúsculas
                           .replace(" ", ""); // Eliminar espacios internos

System.out.println(normalizado); // "usuario@dominio.com"

Un ejemplo más complejo que combina varios métodos:

public String formatearNombreArchivo(String nombreOriginal) {
    return nombreOriginal.trim()
                        .toLowerCase()
                        .replaceAll("[áàäâã]", "a")
                        .replaceAll("[éèëê]", "e")
                        .replaceAll("[íìïî]", "i")
                        .replaceAll("[óòöôõ]", "o")
                        .replaceAll("[úùüû]", "u")
                        .replaceAll("[^a-z0-9\\.]", "-")
                        .replaceAll("-+", "-");
}

// Uso:
String archivo = " Currículum Vitáe (2023).pdf ";
String formateado = formatearNombreArchivo(archivo);
// Resultado: "curriculum-vitae-2023.pdf"

Procesamiento de texto estructurado

La combinación de split() con otros métodos de transformación permite procesar texto estructurado:

String json = "{\"nombre\":\"Juan\",\"edad\":30,\"ciudad\":\"Madrid\"}";

// Extraer pares clave-valor de forma básica
String contenido = json.substring(1, json.length() - 1); // Eliminar llaves
String[] pares = contenido.split(",");

Map<String, String> datos = new HashMap<>();
for (String par : pares) {
    String[] keyValue = par.split(":");
    String clave = keyValue[0].replace("\"", "");
    String valor = keyValue[1].replace("\"", "");
    datos.put(clave, valor);
}

System.out.println("Nombre: " + datos.get("nombre"));
System.out.println("Edad: " + datos.get("edad"));

Consideraciones de rendimiento

Al trabajar con transformaciones de cadenas, se deben tener en cuenta algunas consideraciones:

  • Las operaciones de transformación crean nuevos objetos String, lo que puede afectar al rendimiento si se realizan muchas operaciones consecutivas.
  • Para transformaciones múltiples, es más eficiente utilizar StringBuilder:
// Enfoque ineficiente
String resultado = "";
for (int i = 0; i < 1000; i++) {
    resultado += "a";
}

// Enfoque eficiente
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append("a");
}
String resultado = sb.toString();
  • El método split() con expresiones regulares complejas puede ser costoso para cadenas muy largas. En estos casos, se puede considerar el uso de StringTokenizer o Scanner como alternativas.

Rendimiento y optimización con String, StringBuilder y StringBuffer

Java ofrece tres clases principales para el manejo de texto, cada una con características específicas que afectan directamente al rendimiento de nuestras aplicaciones.

Inmutabilidad de String y sus implicaciones

La clase String en Java es inmutable, lo que significa que una vez creado un objeto String, su contenido no puede ser modificado. Cada operación que parece modificar una cadena en realidad crea un nuevo objeto String.

String nombre = "Java";
nombre = nombre + " Programming"; // Crea un nuevo objeto String

Esta inmutabilidad tiene consecuencias:

  • Seguridad: Los objetos String pueden compartirse de forma segura entre diferentes partes de una aplicación.
  • Hashcode: El valor hash de un String se calcula una sola vez y se almacena en caché.
  • Pool de Strings: Java mantiene un pool de Strings para reutilizar objetos con el mismo contenido.

Sin embargo, la inmutabilidad también presenta desafíos de rendimiento:

String resultado = "";
for (int i = 0; i < 10000; i++) {
    resultado += i; // Crea 10000 objetos String intermedios
}

Este código crea miles de objetos String temporales que deben ser recolectados por el garbage collector, lo que afecta negativamente al rendimiento.

StringBuilder: la alternativa mutable

La clase StringBuilder fue diseñada específicamente para abordar las limitaciones de rendimiento de String cuando se realizan múltiples modificaciones. A diferencia de String, StringBuilder es mutable, lo que significa que puede modificar su contenido sin crear nuevos objetos.

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i); // Modifica el mismo objeto StringBuilder
}
String resultado = sb.toString(); // Conversión final a String

Los métodos más utilizados de StringBuilder incluyen:

  • append(): Añade contenido al final del StringBuilder.
  • insert(): Inserta contenido en una posición específica.
  • delete(): Elimina caracteres en un rango determinado.
  • replace(): Reemplaza caracteres en un rango por una nueva cadena.
  • reverse(): Invierte el contenido del StringBuilder.
StringBuilder builder = new StringBuilder("Hola");
builder.append(" Mundo")       // "Hola Mundo"
       .insert(0, "¡")         // "¡Hola Mundo"
       .append("!")            // "¡Hola Mundo!"
       .delete(5, 10)          // "¡Hola!"
       .replace(1, 5, "Java"); // "¡Java!"

String resultado = builder.toString(); // Conversión final a String

StringBuffer: la alternativa thread-safe

StringBuffer ofrece la misma funcionalidad que StringBuilder, pero con una diferencia crucial: es thread-safe. Todos sus métodos están sincronizados, lo que garantiza la seguridad en entornos multihilo.

StringBuffer buffer = new StringBuffer();
// Seguro para usar en múltiples hilos
buffer.append("Thread-safe ");
buffer.append("string manipulation");

La sincronización tiene un costo en términos de rendimiento, por lo que StringBuffer es más lento que StringBuilder en aplicaciones de un solo hilo.

Comparativa de rendimiento

Para entender mejor las diferencias de rendimiento, veamos una comparación práctica:

// Medición con String
long inicio = System.nanoTime();
String s = "";
for (int i = 0; i < 100000; i++) {
    s += "a";
}
long fin = System.nanoTime();
System.out.println("String: " + (fin - inicio) / 1000000 + " ms");

// Medición con StringBuilder
inicio = System.nanoTime();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
    sb.append("a");
}
String resultadoSb = sb.toString();
fin = System.nanoTime();
System.out.println("StringBuilder: " + (fin - inicio) / 1000000 + " ms");

// Medición con StringBuffer
inicio = System.nanoTime();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < 100000; i++) {
    sbf.append("a");
}
String resultadoSbf = sbf.toString();
fin = System.nanoTime();
System.out.println("StringBuffer: " + (fin - inicio) / 1000000 + " ms");

Los resultados típicos muestran que StringBuilder es más rápido que String para operaciones múltiples, y StringBuffer es ligeramente más lento que StringBuilder debido a la sincronización.

Optimización de la capacidad inicial

Tanto StringBuilder como StringBuffer permiten especificar una capacidad inicial, lo que puede mejorar el rendimiento cuando se conoce aproximadamente el tamaño final de la cadena:

// Sin capacidad inicial especificada
StringBuilder sb1 = new StringBuilder();

// Con capacidad inicial de 1000 caracteres
StringBuilder sb2 = new StringBuilder(1000);

Cuando no se especifica una capacidad, se utiliza un valor predeterminado (generalmente 16 caracteres). Si las operaciones de append exceden esta capacidad, se debe realizar una expansión interna (realocar memoria y copiar el contenido), lo que afecta al rendimiento.

// Optimización para concatenar 1000 números
StringBuilder optimizado = new StringBuilder(5000); // Estimación del tamaño final
for (int i = 0; i < 1000; i++) {
    optimizado.append(i).append(", ");
}

Cuándo usar cada clase

La elección entre String, StringBuilder y StringBuffer depende del contexto específico:

  • String: Ideal para cadenas que no cambiarán o cambiarán muy poco. También es la mejor opción cuando se necesita pasar cadenas entre métodos o almacenarlas en colecciones.
  • StringBuilder: La mejor opción para manipulaciones intensivas de cadenas en un entorno de un solo hilo. Ofrece el mejor rendimiento para concatenaciones y modificaciones.
  • StringBuffer: Necesario cuando se manipulan cadenas en un entorno multihilo donde varios hilos pueden modificar la misma cadena simultáneamente.

Patrones de optimización comunes

Concatenación en bucles

// Mal (crea muchos objetos String)
String resultado = "";
for (String item : items) {
    resultado += item + ", ";
}

// Bien (usa un solo StringBuilder)
StringBuilder sb = new StringBuilder();
for (String item : items) {
    sb.append(item).append(", ");
}
String resultado = sb.toString();

Construcción de consultas SQL

// Construcción eficiente de una consulta SQL dinámica
StringBuilder query = new StringBuilder("SELECT * FROM usuarios WHERE 1=1");
if (nombre != null) {
    query.append(" AND nombre = '").append(nombre).append("'");
}
if (edad > 0) {
    query.append(" AND edad > ").append(edad);
}
String sqlFinal = query.toString();

Generación de documentos

// Generación eficiente de un documento HTML
StringBuilder html = new StringBuilder(1024); // Capacidad inicial estimada
html.append("<!DOCTYPE html>\n")
    .append("<html>\n")
    .append("  <head>\n")
    .append("    <title>").append(titulo).append("</title>\n")
    .append("  </head>\n")
    .append("  <body>\n")
    .append("    <h1>").append(encabezado).append("</h1>\n");

// Añadir contenido dinámico
for (String parrafo : parrafos) {
    html.append("    <p>").append(parrafo).append("</p>\n");
}

html.append("  </body>\n")
    .append("</html>");

String documentoFinal = html.toString();

Optimizaciones avanzadas

Reutilización de StringBuilder

En operaciones repetitivas, se puede reutilizar el mismo objeto StringBuilder para evitar crear instancias innecesarias:

StringBuilder reutilizable = new StringBuilder(100);
List<String> resultados = new ArrayList<>();

for (int i = 0; i < 1000; i++) {
    reutilizable.setLength(0); // Vacía el StringBuilder sin crear uno nuevo
    reutilizable.append("Prefijo-").append(i).append("-Sufijo");
    resultados.add(reutilizable.toString());
}

Concatenación de cadenas en expresiones

El compilador de Java optimiza automáticamente algunas concatenaciones simples de cadenas utilizando StringBuilder internamente:

// El compilador optimiza esto automáticamente usando StringBuilder
String mensaje = "Hola " + nombre + ", tienes " + edad + " años.";

Sin embargo, esta optimización no se aplica en bucles o expresiones complejas, donde se debe usar StringBuilder explícitamente.

Uso de String.join() para concatenaciones simples

Para concatenaciones simples con un delimitador común, Java ofrece el método String.join() que es más legible y eficiente que la concatenación manual:

// Más eficiente y legible que concatenación manual
String[] palabras = {"Java", "es", "un", "lenguaje", "de", "programación"};
String frase = String.join(" ", palabras);

// También funciona con colecciones
List<String> nombres = List.of("Ana", "Juan", "María");
String listaDeNombres = String.join(", ", nombres);

Consideraciones de memoria

Además del rendimiento en tiempo de ejecución, es importante considerar el uso de memoria:

  • String: Cada operación crea un nuevo objeto, lo que puede llevar a un alto consumo de memoria si se realizan muchas operaciones.
  • StringBuilder/StringBuffer: Utilizan un buffer interno que puede crecer según sea necesario, lo que generalmente resulta en un menor consumo de memoria para operaciones múltiples.
// Monitoreo básico de memoria
Runtime runtime = Runtime.getRuntime();
long memoriaAntes = runtime.totalMemory() - runtime.freeMemory();

// Operaciones con cadenas...

long memoriaDespues = runtime.totalMemory() - runtime.freeMemory();
System.out.println("Memoria utilizada: " + (memoriaDespues - memoriaAntes) / 1024 + " KB");

Formateo de Strings

El formateo de cadenas permite crear textos estructurados combinando valores variables con plantillas predefinidas. Java ofrece diversas técnicas para dar formato a las cadenas, desde métodos tradicionales hasta enfoques más modernos, cada uno con sus propias ventajas en diferentes contextos.

Método String.format()

El método String.format() proporciona una forma flexible de crear cadenas formateadas utilizando especificadores de formato similares a los de C. Este método acepta una cadena de formato y una serie de argumentos que se incorporan en la cadena resultante.

String nombre = "Ana";
int edad = 28;
double salario = 2500.75;

String mensaje = String.format("Empleado: %s, Edad: %d, Salario: %.2f€", nombre, edad, salario);
System.out.println(mensaje); // Empleado: Ana, Edad: 28, Salario: 2500,75€

Los especificadores de formato más comunes incluyen:

  • %s - Para cadenas
  • %d - Para enteros
  • %f - Para números de punto flotante
  • %b - Para valores booleanos
  • %c - Para caracteres
  • %t - Para fechas y horas (con subformatos)
  • %% - Para imprimir el símbolo %

Se pueden aplicar modificadores a estos especificadores para controlar con precisión la salida:

// Ancho y alineación
String alineado = String.format("|%-10s|%10s|", "izquierda", "derecha");
System.out.println(alineado); // |izquierda  |    derecha|

// Precisión decimal
double pi = Math.PI;
String piFormateado = String.format("Pi con 2 decimales: %.2f", pi);
System.out.println(piFormateado); // Pi con 2 decimales: 3,14

// Formato de números con separadores de miles
long poblacion = 47_350_000;
String pobFormateada = String.format("Población: %,d habitantes", poblacion);
System.out.println(pobFormateada); // Población: 47.350.000 habitantes

Formateo de fechas y horas

El formateo de fechas y horas merece especial atención por su complejidad. El especificador %t se combina con letras adicionales para controlar el formato:

LocalDateTime ahora = LocalDateTime.now();
String fechaFormateada = String.format("Fecha: %tD, Hora: %tT", ahora, ahora);
System.out.println(fechaFormateada); // Fecha: 05/15/23, Hora: 14:30:45

// Formatos más específicos
String formatoCompleto = String.format(
    "Día: %td, Mes: %tB, Año: %tY, Hora: %tH:%tM",
    ahora, ahora, ahora, ahora, ahora
);

Para evitar repetir la referencia al mismo objeto, se puede usar el índice de argumento:

String formatoMejorado = String.format(
    "Fecha: %1$td/%1$tm/%1$tY, Hora: %1$tH:%1$tM:%1$tS",
    ahora
);

Método printf() para salida formateada

El método System.out.printf() utiliza la misma sintaxis de formateo que String.format(), pero imprime directamente en la salida estándar en lugar de devolver una cadena:

double precio = 19.99;
int cantidad = 3;
System.out.printf("Subtotal: %.2f€ × %d unidades = %.2f€%n", 
                  precio, cantidad, precio * cantidad);
// Subtotal: 19,99€ × 3 unidades = 59,97€

El especificador %n se utiliza para insertar un salto de línea independiente de la plataforma, lo que es preferible a usar \n directamente.

Formateo con String.formatted() (Java 15+)

A partir de Java 15, se introdujo el método de instancia formatted() que simplifica el formateo al permitir llamarlo directamente sobre una cadena de formato:

String plantilla = "Usuario: %s (ID: %d)";
String resultado = plantilla.formatted("carlos_dev", 12345);
System.out.println(resultado); // Usuario: carlos_dev (ID: 12345)

Este enfoque es más conciso y mejora la legibilidad del código en muchos casos.

Formateo con StringBuilder

Para escenarios donde se necesita construir cadenas formateadas de manera dinámica, StringBuilder ofrece métodos de formateo similares:

StringBuilder sb = new StringBuilder();
Formatter formatter = new Formatter(sb);
formatter.format("Lista de precios:%n");

String[] productos = {"Teclado", "Ratón", "Monitor"};
double[] precios = {45.99, 22.50, 199.99};

for (int i = 0; i < productos.length; i++) {
    formatter.format("- %-10s %8.2f€%n", productos[i], precios[i]);
}

System.out.println(sb.toString());
// Lista de precios:
// - Teclado        45,99€
// - Ratón          22,50€
// - Monitor       199,99€

Plantillas de texto (Java 21+)

Java 21 introdujo las plantillas de texto (Text Blocks con interpolación), una característica que simplifica el formateo de cadenas:

String nombre = "María";
int puntuacion = 95;
String mensaje = STR."¡Felicidades \{nombre}! Has obtenido \{puntuacion} puntos.";
System.out.println(mensaje); // ¡Felicidades María! Has obtenido 95 puntos.

Las plantillas de texto permiten la interpolación de expresiones directamente en la cadena, lo que resulta en un código más legible y menos propenso a errores que los métodos tradicionales de formateo.

int x = 10;
int y = 20;
String operacion = STR."\{x} + \{y} = \{x + y}";
System.out.println(operacion); // 10 + 20 = 30

// Expresiones más complejas
LocalDate fecha = LocalDate.now();
String informe = STR."""
    Informe generado el \{fecha.format(DateTimeFormatter.ISO_LOCAL_DATE)}
    Temperatura: \{String.format("%.1f", 22.75)}°C
    Estado: \{Math.random() > 0.5 ? "Óptimo" : "Requiere revisión"}
    """;

MessageFormat para localización

La clase MessageFormat es útil para crear mensajes localizados y parametrizados:

String patron = "El archivo {0} tiene {1,number,integer} bytes y fue modificado el {2,date,long}.";
Object[] valores = {"config.xml", 1234, new Date()};
String mensaje = MessageFormat.format(patron, valores);
System.out.println(mensaje);
// El archivo config.xml tiene 1.234 bytes y fue modificado el 15 de mayo de 2023.

MessageFormat se integra con el sistema de internacionalización de Java, permitiendo adaptar los mensajes a diferentes idiomas y convenciones regionales:

// Formateo según configuración regional
MessageFormat mf = new MessageFormat("El precio es {0,number,currency}.", new Locale("es", "ES"));
String precioEspañol = mf.format(new Object[]{29.99});
System.out.println(precioEspañol); // El precio es 29,99 €.

mf = new MessageFormat("The price is {0,number,currency}.", Locale.US);
String precioUSA = mf.format(new Object[]{29.99});
System.out.println(precioUSA); // The price is $29.99.

Formateo de números con DecimalFormat

Para un control más preciso sobre el formato de números, la clase DecimalFormat ofrece opciones avanzadas:

// Formato de porcentajes
DecimalFormat dfPorcentaje = new DecimalFormat("##0.00'%'");
System.out.println(dfPorcentaje.format(0.7625)); // 76,25%

// Formato de moneda personalizado
DecimalFormat dfMoneda = new DecimalFormat("###,###.## €");
System.out.println(dfMoneda.format(1250.75)); // 1.250,75 €

// Números con prefijos y sufijos
DecimalFormat dfTamaño = new DecimalFormat("### KB");
System.out.println(dfTamaño.format(512)); // 512 KB

Los patrones de formato en DecimalFormat utilizan símbolos especiales:

  • 0 - Dígito obligatorio
  • # - Dígito opcional
  • . - Separador decimal
  • , - Separador de agrupación
  • ' - Escape para texto literal
  • % - Multiplicar por 100 y mostrar como porcentaje
  • - Multiplicar por 1000 y mostrar como por mil
// Diferentes patrones para el mismo número
double valor = 12345.6789;
String[] patrones = {"###,###.###", "000,000.000", "#,##0.00", "0.0"};

for (String patron : patrones) {
    DecimalFormat df = new DecimalFormat(patron);
    System.out.println(patron + ": " + df.format(valor));
}
// ###,###.###: 12.345,679
// 000,000.000: 012.345,679
// #,##0.00: 12.345,68
// 0.0: 12345,7

Casos de uso prácticos

Generación de informes tabulares

String[] encabezados = {"Producto", "Precio", "Stock", "Valor"};
Object[][] datos = {
    {"Laptop", 899.99, 15, 899.99 * 15},
    {"Smartphone", 499.50, 30, 499.50 * 30},
    {"Auriculares", 79.99, 50, 79.99 * 50}
};

// Formato de tabla
System.out.printf("| %-15s | %10s | %5s | %12s |%n", 
                 (Object[])encabezados);
System.out.println("|-----------------|------------|-------|--------------|");

for (Object[] fila : datos) {
    System.out.printf("| %-15s | %,10.2f€ | %5d | %,12.2f€ |%n", 
                     fila[0], fila[1], fila[2], fila[3]);
}

Formateo de datos científicos

double[] mediciones = {0.0000123, 4.567e6, 2.998e8};
String[] unidades = {"s", "Hz", "m/s"};

for (int i = 0; i < mediciones.length; i++) {
    // Formato científico
    System.out.printf("Valor: %e %s%n", mediciones[i], unidades[i]);
    // Formato adaptativo
    System.out.printf("Valor: %g %s%n", mediciones[i], unidades[i]);
}

Generación de URLs y consultas

String baseUrl = "https://api.ejemplo.com/buscar";
String query = "java programación";
int pagina = 2;
int resultadosPorPagina = 25;

String url = String.format("%s?q=%s&page=%d&size=%d", 
                          baseUrl, 
                          URLEncoder.encode(query, StandardCharsets.UTF_8),
                          pagina,
                          resultadosPorPagina);
System.out.println(url);

Consideraciones de rendimiento

El formateo de cadenas tiene implicaciones de rendimiento que deben considerarse en aplicaciones críticas:

  • String.format() es conveniente pero relativamente costoso en términos de rendimiento debido a la necesidad de analizar la cadena de formato y convertir los argumentos.
  • Para operaciones de formateo intensivas dentro de bucles, considere reutilizar un Formatter o StringBuilder.
  • Las plantillas de texto (Java 21+) ofrecen un rendimiento mejorado en comparación con String.format() para muchos casos de uso.
// Comparación de rendimiento básica
long inicio, fin;

inicio = System.nanoTime();
for (int i = 0; i < 10000; i++) {
    String.format("%d + %d = %d", i, i+1, i*2+1);
}
fin = System.nanoTime();
System.out.println("String.format: " + (fin - inicio) / 1_000_000 + " ms");

// Con Java 21+
inicio = System.nanoTime();
for (int i = 0; i < 10000; i++) {
    STR."\{i} + \{i+1} = \{i*2+1}";
}
fin = System.nanoTime();
System.out.println("Plantillas de texto: " + (fin - inicio) / 1_000_000 + " ms");

Buenas prácticas

  • Elija el método adecuado según el contexto: String.format() para casos generales, plantillas de texto para código más legible, y MessageFormat para contenido localizable.
  • Reutilice patrones de formato cuando sea posible para mejorar el rendimiento.
  • Considere la localización desde el principio si su aplicación será utilizada internacionalmente.
  • Valide los datos de entrada antes de formatearlos para evitar excepciones inesperadas.
  • Utilice constantes para patrones de formato complejos o reutilizados.
// Buena práctica: constantes para patrones reutilizados
private static final String FORMATO_MONEDA = "%,.2f€";
private static final String FORMATO_FECHA = "%1$td/%1$tm/%1$tY";
private static final DecimalFormat FORMATO_PORCENTAJE = new DecimalFormat("##0.00'%'");

// Uso
String precio = String.format(FORMATO_MONEDA, 1299.99);
String fecha = String.format(FORMATO_FECHA, LocalDate.now());
String tasa = FORMATO_PORCENTAJE.format(0.2175);
Aprende Java online

Otros ejercicios de programación de Java

Evalúa tus conocimientos de esta lección Métodos de la clase String con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Streams: match

Test

Gestión de errores y excepciones

Código

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Clases abstractas

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

API java.nio 2

Puzzle

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Interfaces

Código

Enumeraciones Enums

Código

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

CRUD de productos en Java

Proyecto

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Herencia

Código

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Mapas

Código

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

Todas las lecciones de Java

Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Instalación De Java

Introducción Y Entorno

Configuración De Entorno Java

Introducción Y Entorno

Tipos De Datos

Sintaxis

Variables

Sintaxis

Operadores

Sintaxis

Estructuras De Control

Sintaxis

Funciones

Sintaxis

Recursión

Sintaxis

Arrays Y Matrices

Sintaxis

Excepciones

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Encapsulación

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Sobrecarga De Métodos

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

La Clase Scanner

Programación Orientada A Objetos

Métodos De La Clase String

Programación Orientada A Objetos

Excepciones

Programación Orientada A Objetos

Records

Programación Orientada A Objetos

Pattern Matching

Programación Orientada A Objetos

Inferencia De Tipos Con Var

Programación Orientada A Objetos

Enumeraciones Enums

Programación Orientada A Objetos

Generics

Programación Orientada A Objetos

Clases Sealed

Programación Orientada A Objetos

Listas

Framework Collections

Conjuntos

Framework Collections

Mapas

Framework Collections

Funciones Lambda

Programación Funcional

Interfaz Funcional Consumer

Programación Funcional

Interfaz Funcional Predicate

Programación Funcional

Interfaz Funcional Supplier

Programación Funcional

Interfaz Funcional Function

Programación Funcional

Métodos Referenciados

Programación Funcional

Creación De Streams

Programación Funcional

Operaciones Intermedias Con Streams: Map()

Programación Funcional

Operaciones Intermedias Con Streams: Filter()

Programación Funcional

Operaciones Intermedias Con Streams: Distinct()

Programación Funcional

Operaciones Finales Con Streams: Collect()

Programación Funcional

Operaciones Finales Con Streams: Min Max

Programación Funcional

Operaciones Intermedias Con Streams: Flatmap()

Programación Funcional

Operaciones Intermedias Con Streams: Sorted()

Programación Funcional

Operaciones Finales Con Streams: Reduce()

Programación Funcional

Operaciones Finales Con Streams: Foreach()

Programación Funcional

Operaciones Finales Con Streams: Count()

Programación Funcional

Operaciones Finales Con Streams: Match

Programación Funcional

Api Optional

Programación Funcional

Transformación

Programación Funcional

Reducción Y Acumulación

Programación Funcional

Mapeo

Programación Funcional

Streams Paralelos

Programación Funcional

Agrupación Y Partición

Programación Funcional

Filtrado Y Búsqueda

Programación Funcional

Api Java.nio 2

Entrada Y Salida Io

Fundamentos De Io

Entrada Y Salida Io

Leer Y Escribir Archivos

Entrada Y Salida Io

Httpclient Moderno

Entrada Y Salida Io

Clases De Nio2

Entrada Y Salida Io

Api Java.time

Api Java.time

Localtime

Api Java.time

Localdatetime

Api Java.time

Localdate

Api Java.time

Executorservice

Concurrencia

Virtual Threads (Project Loom)

Concurrencia

Future Y Completablefuture

Concurrencia

Spring Framework

Frameworks Para Java

Micronaut

Frameworks Para Java

Maven

Frameworks Para Java

Gradle

Frameworks Para Java

Lombok Para Java

Frameworks Para Java

Quarkus

Frameworks Para Java

Ecosistema Jakarta Ee De Java

Frameworks Para Java

Introducción A Junit 5

Testing

Accede GRATIS a Java y certifícate

Certificados de superación de Java

Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la inmutabilidad de los objetos String en Java
  • Usar el método length() para medir la longitud de una cadena
  • Acceder a caracteres específicos con charAt()
  • Extraer subcadenas utilizando substring()
  • Manejar excepciones comunes como StringIndexOutOfBoundsException
  • Aplicar estos métodos para procesar y validar cadenas de texto