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.
| Necesidad | Usar | Costo de búsqueda |
|---|---|---|
| Acceso por índice, agregar al final | ArrayList | O(1) por índice, O(n) por valor |
| Búsqueda clave/valor | HashMap | O(1) promedio |
| Prueba de pertenencia, sin duplicados | HashSet | O(1) promedio |
| Claves ordenadas | TreeMap | O(log n) |
| Inserción/eliminación frecuente en los extremos | ArrayDeque | O(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; // fastOtras 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.
Qué aprender de la ejecución:
Same result? truedemuestra queStringBuilderproduce 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: trueconfirma que unArrayListpre-dimensionado termina siendo idéntico a uno que creció — la pista del constructor afecta la asignación, no el contenido.Pre-sized faster? truemuestra que indicarle aArrayListsu capacidad de antemano evita los pasos repetidos de redimensionamiento y copia.Map lookups found: 50000 in ... msdemuestra que 50,000 búsquedas enHashMapterminan 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 — usaStringBuildersiempre que concatenes dentro de un bucle. - Autoboxing oculto. Un
List<Integer>,Map<Integer, Integer>o un acumuladorLongextraviado 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/HashSetpara búsquedas,ArrayListpara 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.