W3docs

Consejos de rendimiento en Java

Consejos prácticos de rendimiento para Java: medir primero, evitar optimizaciones prematuras y micro-optimizaciones comunes.

Java es lo suficientemente rápido para casi todo, pero el código lento sigue ocurriendo — generalmente por hacer demasiado trabajo, asignar demasiada memoria o elegir la estructura de datos incorrecta. El consejo de rendimiento más importante no es ningún truco: mide antes de cambiar cualquier cosa. Este capítulo cubre cómo medir con honestidad, el puñado de optimizaciones que más frecuentemente dan resultado (construcción de strings, elección de estructura de datos, evitar asignaciones innecesarias) y los errores que hacen que el código "rápido" ingenuo sea realmente lento.

Medir primero, optimizar después

Adivinar sobre el rendimiento es como perder horas acelerando código que nunca fue lento. Perfila una carga de trabajo real, encuentra la ruta caliente (la pequeña fracción de código donde se gasta la mayor parte del tiempo) y solo entonces optimízala. Para experimentos rápidos, System.nanoTime() proporciona un delta de tiempo de pared; para benchmarks serios usa una herramienta como JMH (Java Microbenchmark Harness), que calienta el JIT y tiene en cuenta el ruido de medición.

long start = System.nanoTime();
doWork();
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("Took " + elapsedMs + " ms");

Dos reglas acompañan esto. Primero, evita la optimización prematura — el código claro que es suficientemente rápido supera al código inteligente que es difícil de leer. Segundo, el compilador JIT de la JVM optimiza el código caliente en tiempo de ejecución, por lo que un método solo alcanza su velocidad máxima después de haberse ejecutado muchas veces; una sola llamada cronometrada dice muy poco.

Construir strings con StringBuilder

Dado que los strings son inmutables, s += x dentro de un bucle crea un string completamente nuevo y copia todos los caracteres anteriores en cada iteración — eso es un trabajo O(n²). StringBuilder mantiene un búfer ampliable y agrega en su lugar, convirtiendo el mismo trabajo en O(n).

// Slow: a new String allocated every iteration
String csv = "";
for (String field : fields) {
    csv += field + ",";
}

// Fast: one buffer, appended in place
StringBuilder sb = new StringBuilder();
for (String field : fields) {
    sb.append(field).append(',');
}
String csv2 = sb.toString();

Un único a + b + c fuera de un bucle está bien — el compilador ya lo convierte en un solo StringBuilder (consulta concatenación de strings para ver qué hace el compilador). El problema es la concatenación dentro de un bucle, donde cada pasada añade una copia completa adicional.

Elegir la estructura de datos correcta

Las mayores ganancias suelen venir de las decisiones algorítmicas, no de los ajustes menores. Buscar un valor en un ArrayList recorre cada elemento (O(n)); un HashMap o HashSet lo hace en tiempo aproximadamente constante (O(1)). Elige la colección que coincida con la forma en que realmente accedes a los datos.

NecesidadUsarCosto de búsqueda
Acceso por índice, agregar al finalArrayListO(1) por índice, O(n) por valor
Búsqueda clave/valorHashMapO(1) promedio
Prueba de pertenencia, sin duplicadosHashSetO(1) promedio
Claves ordenadasTreeMapO(log n)
Inserción/eliminación frecuente en los extremosArrayDequeO(1) en los extremos

Si conoces el tamaño final, pásalo al constructor: new ArrayList<>(10_000) o new HashMap<>(capacity). Esto evita la reasignación y copia repetidas que ocurren a medida que una colección crece.

Evitar la creación innecesaria de objetos

Cada objeto asignado debe ser recolectado después, y la recolección de basura no es gratuita. Reutiliza valores inmutables, prefiere primitivos sobre sus envoltorios encuadrados en bucles ajustados y no crees objetos que descartas inmediatamente.

// Autoboxing: every += boxes a new Integer
Long total = 0L;
for (int i = 0; i < n; i++) total += i;   // slow, allocates boxes

// Primitive: no allocation at all
long sum = 0L;
for (int i = 0; i < n; i++) sum += i;     // fast

Otras ganancias fáciles: almacena en caché los objetos Pattern compilados en lugar de llamar a String.matches() en un bucle, reutiliza un DateTimeFormatter (es seguro para hilos e inmutable) y favorece el for mejorado sobre los streams en los bucles internos más calientes donde importa la asignación.

java— editable, runs on the server

Qué aprender de la ejecución:

  • Same result? true demuestra que StringBuilder produce exactamente el mismo string que +=, por lo que cambiarlo solo afecta la velocidad, nunca la corrección.
  • El ratio "x slower" impreso muestra que la concatenación en bucle cuesta muchas veces más que agregar a un búfer, porque cada += copia todo el string hasta ese punto.
  • Both lists size 100000: true confirma que un ArrayList pre-dimensionado termina siendo idéntico a uno que creció — la pista del constructor afecta la asignación, no el contenido.
  • Pre-sized faster? true muestra que indicarle a ArrayList su capacidad de antemano evita los pasos repetidos de redimensionamiento y copia.
  • Map lookups found: 50000 in ... ms demuestra que 50,000 búsquedas en HashMap terminan en aproximadamente un milisegundo, la recompensa de elegir acceso O(1) sobre un recorrido de lista O(n).

Errores comunes

Algunos errores convierten el código "obviamente más rápido" en lo contrario:

  • Confiar en una sola ejecución cronometrada. El JIT aún no se ha calentado y el sistema operativo puede haber programado otra cosa a mitad de la medición. Repite el trabajo miles de veces, o usa JMH, antes de creer un número.
  • Micro-optimizar código frío. Un método que se ejecuta una vez al inicio no gana nada con un bucle más ajustado. Dedica esfuerzo solo a la ruta caliente que el perfilador señala.
  • Construir strings con + en un bucle. La ralentización evitable más común — usa StringBuilder siempre que concatenes dentro de un bucle.
  • Autoboxing oculto. Un List<Integer>, Map<Integer, Integer> o un acumulador Long extraviado encuadra cada valor. En un bucle numérico ajustado, prefiere primitivos y arrays de primitivos.
  • Optimizar antes de que funcione. Correcto primero, rápido después. El código claro que puedes perfilar supera al código inteligente sobre el que no puedes razonar.

Resumen

  • Mide antes de cambiar cualquier cosa — perfila una carga de trabajo real y optimiza solo la ruta caliente.
  • Construye strings con StringBuilder, no con +=, dentro de bucles.
  • Ajusta la colección al patrón de acceso: HashMap/HashSet para búsquedas, ArrayList para acceso indexado; pre-dimensiona cuando se conoce la cantidad.
  • Evita la asignación innecesaria: prefiere primitivos, reutiliza objetos inmutables y recuerda que cada objeto añade trabajo de recolección de basura.

Práctica

Práctica
¿Por qué usar '+=' para construir un String dentro de un bucle es lento comparado con StringBuilder?
¿Por qué usar '+=' para construir un String dentro de un bucle es lento comparado con StringBuilder?
Was this page helpful?