Cookbook de trampas comunes en Java
Este documento recopila errores comunes y comportamientos sutiles en Java que pueden generar bugs difíciles de detectar. Cada entrada sigue el esquema problema → solución → discusión para que puedas identificar, entender y evitar estos fallos en tu código.
1. Comparación de Integer fuera del rango cacheado
Problema: El operador == devuelve false para enteros mayores a 127 (o menores a -128) aunque los valores numéricos sean iguales.
Solución: Usa .equals() para comparar valores de Integer. Para rendimiento o simplicidad, usa int si no necesitas nulos.
Discusión: Java cachea los Integer en el rango [-128, 127]. Fuera de ese rango, Integer.valueOf() devuelve instancias distintas; == compara referencias, no contenido.
2. Comparación de String con ==
Problema: == entre cadenas a veces da true y otras false para el mismo texto, lo que confunde a principiantes.
Solución: Compara cadenas con .equals() o .equalsIgnoreCase().
Discusión: El string pool puede hacer que dos literales apunten a la misma referencia, pero objetos creados con new String() no. == compara referencias; .equals() compara contenido.
3. Modificar una colección durante la iteración
Problema: Eliminar o añadir elementos a una colección mientras se recorre con for-each lanza ConcurrentModificationException.
Solución: Usa un Iterator y su remove(), o recopila primero y modifica después; alternativamente, usa colecciones concurrentes.
Discusión: Muchas colecciones detectan cambios estructurales durante la iteración. Las colecciones concurrentes (p. ej., CopyOnWriteArrayList) tienen semánticas diferentes y coste adicional.
4. NullPointerException al hacer unboxing
Problema: Convertir un Integer null a int produce NullPointerException.
Solución: Valida null antes del unboxing o usa OptionalInt / valores por defecto.
Discusión: El unboxing invoca internamente intValue() sobre la referencia; si es null, falla. Evítalo donde sea posible usando tipos primitivos.
5. Arrays no hacen copia profunda
Problema: Asignar un array a otra variable copia la referencia; cambios en una vista afectan a la otra.
Solución: Usa Arrays.copyOf() o clone() para copiar el array; para matrices/objetos, implementa una copia profunda explícita.
Discusión: La copia por referencia es rápida pero sorprende a quienes esperan independencia de datos. Para estructuras anidadas, copia cada nivel.
6. Falta de break en switch (fall-through)
Problema: Olvidar break ejecuta casos adicionales inesperadamente.
Solución: Añade break o usa switch con expresiones (Java 14+) y ->, que no hace fall-through por defecto.
Discusión: El fall-through es útil en casos agrupados, pero suele ser fuente de bugs. Las expresiones switch modernas reducen este riesgo.
7. Llamadas virtuales en constructores (inicialización incompleta)
Problema: Llamar a métodos sobreescritos desde un constructor puede usar campos aún no inicializados de la subclase.
Solución: No invoques métodos sobreescritos en constructores; usa métodos final o inicialización explícita.
Discusión: El constructor de la superclase se ejecuta antes de inicializar los campos de la subclase. La llamada virtual accede a estado incompleto.
8. Arrays de tipos genéricos
Problema: No se pueden crear arrays de tipos genéricos concretos, p. ej., new ArrayList<String>[10].
Solución: Usa List<?>[] o preferentemente colecciones, evitando arrays de genéricos.
Discusión: Los arrays son covariantes y reificados; los genéricos usan borrado de tipos. La mezcla rompe seguridad de tipos en tiempo de ejecución.
9. Claves mutables en HashMap/HashSet
Problema: Modificar una clave después de insertarla impide encontrarla de nuevo.
Solución: Usa claves inmutables (por ejemplo, record o clases inmutables) o no modifiques las claves tras usarlas.
Discusión: hashCode() y equals() determinan la ubicación. Si cambian, el mapa pierde la referencia a la entrada.
10. Optional.get() sin verificar presencia
Problema: Llamar a get() sobre un Optional vacío lanza NoSuchElementException.
Solución: Usa orElse(), orElseGet(), orElseThrow() o ifPresent().
Discusión: Optional pretende evitar null; usar get() rompe su intención. Prefiere API expresivas.
11. Shadowing de variables (ocultamiento de campos)
Problema: El parámetro del método oculta el campo de instancia y asignaciones como nombre = nombre; no hacen nada útil.
Solución: Usa this.nombre = nombre; o cambia el nombre del parámetro.
Discusión: El sombreado dificulta el mantenimiento y causa bugs silenciosos. Activar inspecciones del IDE ayuda a detectarlo.
12. Concurrencia: falta de volatile (visibilidad)
Problema: Un hilo no observa cambios de otro hilo sobre una variable sin sincronización.
Solución: Declara la variable como volatile o sincroniza accesos (synchronized, Lock).
Discusión: El modelo de memoria de Java permite reordenamientos y cachés por hilo. volatile garantiza visibilidad y orden de publicación/lectura.
13. Concurrencia: incrementos no atómicos
Problema: Operaciones como valor++ pierden actualizaciones bajo concurrencia.
Solución: Usa AtomicInteger.incrementAndGet(), bloques synchronized o acumuladores (LongAdder).
Discusión: valor++ implica leer-modificar-escribir. Sin exclusión mutua, dos hilos pueden sobrescribir resultados.
14. Calendar: mes base cero
Problema: Calendar#set(año, mes, día) interpreta enero como 0; 10 es noviembre, no octubre.
Solución: Usa constantes (Calendar.OCTOBER) o migra a java.time.
Discusión: La API antigua de fechas es propensa a errores. java.time (JSR-310) es más clara e inmutable.
15. LocalDate.parse() con formatos no ISO
Problema: LocalDate.parse("05/10/2025") lanza DateTimeParseException.
Solución: Define un formateador: DateTimeFormatter.ofPattern("dd/MM/yyyy") y pásalo a parse().
Discusión: Por defecto, parse() espera yyyy-MM-dd. Para otros formatos debes indicar el patrón explícitamente.
