Saltar al contenido principal

Capítulo 5: Depuración y Revisión de Código

Objetivo

Estos ejercicios simulan preguntas de entrevista donde se le presenta código con errores o mal escrito y se le pide encontrar los problemas y corregirlos. Es un formato muy popular en grandes empresas (estilo Sonar/Codility).


Ejercicio 1 — Encontrar y corregir 4 errores​

"Este código tiene 4 errores. Encuéntrelos y corríjalos."

El código con errores​

public class DiscountCalculator {

public double calculateDiscount(List<Product> products, String coupon) {
double total = 0;
for (int i = 0; i <= products.size(); i++) { // BUG 1
Product p = products.get(i);
if (p.getPrice() > 0) {
total =+ p.getPrice(); // BUG 2
}
}
double discount = 0;
if (coupon == "SAVE20") { // BUG 3
discount = total * 0.20;
}
return total - discount; // BUG 4: pas de null check
}
}

El código corregido​

public class DiscountCalculator {

public double calculateDiscount(List<Product> products, String coupon) {
if (products == null || products.isEmpty()) return 0; // FIX 4

double total = 0;
for (int i = 0; i < products.size(); i++) { // FIX 1: < en lugar de <=
Product p = products.get(i);
if (p.getPrice() > 0) {
total += p.getPrice(); // FIX 2: += en lugar de =+
}
}
double discount = 0;
if ("SAVE20".equals(coupon)) { // FIX 3: .equals() en lugar de ==
discount = total * 0.20;
}
return total - discount;
}
}

Explicación de los errores​

ErrorProblemaConsecuenciaCorrección
1<= en lugar de <ArrayIndexOutOfBoundsExceptionCambiar a <
2=+ en lugar de +==+ asigna +valor (positivo), no acumulaCambiar a +=
3== compara referenciasDos String idénticos pueden tener referencias diferentesUsar .equals()
4No hay verificación de nullNullPointerException si products es nullAgregar una guard clause
Trampas favoritas de los entrevistadores
  • =+ vs +=: Uno asigna, el otro suma. Muy sutil visualmente.
  • == vs .equals(): En Java, == compara direcciones de memoria, no el contenido.
  • Off-by-one (<= vs <): El error más clásico en programación.
Frase de entrevista

"Encontré cuatro errores: error off-by-one en el bucle, asignación en vez de suma, comparación de referencias en vez de .equals(), y falta de seguridad ante null. Son trampas clásicas de Java."


Ejercicio 2 — Revisión completa de código​

"Haga una revisión de este código. ¿Qué mejoraría?"

El código a examinar​

public class DataProcessor {
public static ArrayList<String> process(ArrayList<String> data) {
ArrayList<String> result = new ArrayList<>();
for (int i = 0; i < data.size(); i++) {
String s = data.get(i);
if (s != null) {
if (s.length() > 0) {
if (!s.equals("SKIP")) {
result.add(s.toUpperCase());
}
}
}
}
return result;
}
}

El código mejorado​

public class DataProcessor {

public List<String> process(List<String> data) {
if (data == null) return List.of();

return data.stream()
.filter(Objects::nonNull)
.filter(s -> !s.isEmpty())
.filter(s -> !"SKIP".equals(s))
.map(String::toUpperCase)
.toList();
}
}

Todos los problemas identificados​

#ProblemaCorrecciónPrincipio
1ArrayList en la firmaUsar List (interfaz)Programar contra la interfaz
2Método staticQuitar staticTestabilidad (mocking)
3Sin guard clause para nullAgregar if (data == null) return List.of()Seguridad ante null
4if anidados (3 niveles)Stream pipelineLegibilidad
5s.equals("SKIP")"SKIP".equals(s)Evitar NPE
6s.length() > 0s.isEmpty()Código idiomático
7ArrayList mutable retornadatoList() retorna una lista inmutableInmutabilidad
Frase de entrevista

"Refactorizaría usando streams para mayor legibilidad, programar contra interfaces y añadir seguridad ante null. Este es exactamente el tipo de código que Sonar marcaría — condicionales anidados y estado mutable."


Ejercicio 3 — Encontrar la fuga de memoria​

"Este código causa una fuga de memoria. Encuéntrela."

El código problemático​

@Service
public class CacheService {
private final Map<String, Object> cache = new HashMap<>();

public void put(String key, Object value) {
cache.put(key, value);
}

public Object get(String key) {
return cache.get(key);
}

// Pas de méthode pour vider le cache !
// Le Map grandit indéfiniment → OutOfMemoryError
}

El código corregido​

@Service
public class CacheService {
private final Map<String, Object> cache = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return size() > 1000; // Limitar a 1000 entradas
}
};

// O mejor, usar Caffeine/Redis
}

Aún mejor: usar Spring Cache​

@Service
public class ProductService {

@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
return productRepository.findById(id).orElseThrow();
}

@CacheEvict(value = "products", key = "#id")
public void delete(Long id) {
productRepository.deleteById(id);
}
}
Frase de entrevista

"Un cache HashMap sin política de expulsión es una fuga de memoria segura. Usaría Spring Cache con Caffeine o Redis, que gestionan TTL y tamaño máximo automáticamente."


Ejercicio 4 — Encontrar el problema de concurrencia​

"¿Este código es thread-safe? Encuentre el problema."

El código problemático​

@Service
public class CounterService {
private int count = 0;

public void increment() {
count++; // ¡NO es thread-safe!
}

public int getCount() {
return count;
}
}

Problema: count++ no es atómico. En realidad, son 3 operaciones:

  1. Leer count
  2. Sumar 1
  3. Escribir el nuevo valor

Si dos hilos hacen count++ al mismo tiempo, se puede perder un incremento.

Las soluciones​

// Solución 1: AtomicInteger
@Service
public class CounterService {
private final AtomicInteger count = new AtomicInteger(0);

public void increment() {
count.incrementAndGet();
}

public int getCount() {
return count.get();
}
}

// Solución 2: synchronized
@Service
public class CounterService {
private int count = 0;

public synchronized void increment() {
count++;
}

public synchronized int getCount() {
return count;
}
}
Frase de entrevista

"count++ no es atómico — es read-modify-write. Usaría AtomicInteger para seguridad de hilos sin bloqueo, o synchronized si necesito proteger una sección crítica más grande."


Ejercicio 5 — Corregir una mala gestión de excepciones​

"¿Qué está mal con esta gestión de excepciones?"

El código incorrecto​

@Service
public class PaymentService {

public void processPayment(PaymentRequest req) {
try {
gateway.charge(req.getAmount());
orderService.updateStatus(req.getOrderId(), "PAID");
} catch (Exception e) {
// PROBLEMA 1: catch demasiado amplio
// PROBLEMA 2: se traga la excepción silenciosamente
// PROBLEMA 3: log innecesario
System.out.println("Error: " + e.getMessage());
}
}
}

El código corregido​

@Service
@Slf4j
public class PaymentService {

@Transactional
public void processPayment(PaymentRequest req) {
try {
gateway.charge(req.getAmount());
} catch (PaymentDeclinedException e) {
log.warn("Payment declined for order {}: {}", req.getOrderId(), e.getMessage());
throw new BusinessException("Payment was declined", e);
} catch (GatewayTimeoutException e) {
log.error("Gateway timeout for order {}", req.getOrderId(), e);
throw new ServiceUnavailableException("Payment gateway is down", e);
}

orderService.updateStatus(req.getOrderId(), "PAID");
}
}

Problemas identificados​

#ProblemaPor qué es grave
1catch (Exception e)Atrapa todo, incluso NullPointerException, OutOfMemoryError
2Excepción tragadaEl llamador nunca sabe que el pago falló
3System.out.printlnSin log estructurado, sin stack trace, perdido en producción
4Sin rollbackSi el charge tiene éxito pero updateStatus falla, datos inconsistentes
Frase de entrevista

"Nunca capture Exception genérico. Capture excepciones específicas, registre con la severidad adecuada (warn vs error), relance como excepciones de negocio y use @Transactional para consistencia de datos."


Ejercicio 6 — Identificar code smells​

"Enumere todos los problemas en este código."

El código a analizar​

public class Utils {

public static String x(String s) {
String r = "";
for (int i = s.length() - 1; i >= 0; i--) {
r = r + s.charAt(i);
}
return r;
}

public static boolean check(String email) {
if (email.contains("@") == true) {
if (email.contains(".") == true) {
return true;
} else {
return false;
}
} else {
return false;
}
}

public static int calc(int a, int b, String op) {
if (op == "+") return a + b;
if (op == "-") return a - b;
if (op == "*") return a * b;
if (op == "/") return a / b;
return 0;
}
}

Los problemas​

MétodoProblemas
x()Nombre incomprensible, concatenación en bucle (O(n²)), sin verificación de null
check()== true redundante, if/else return true/false reemplazable por expresión
calc()== en lugar de .equals(), división por cero, retorna 0 silenciosamente

El código corregido​

public class StringUtils {

public static String reverse(String input) {
if (input == null) return null;
return new StringBuilder(input).reverse().toString();
}
}

public class EmailValidator {

public static boolean isValid(String email) {
return email != null && email.contains("@") && email.contains(".");
}
}

public class Calculator {

public static int calculate(int a, int b, String operator) {
return switch (operator) {
case "+" -> a + b;
case "-" -> a - b;
case "*" -> a * b;
case "/" -> {
if (b == 0) throw new ArithmeticException("Division by zero");
yield a / b;
}
default -> throw new IllegalArgumentException("Unknown operator: " + operator);
};
}
}

Checklist de revisión de código​

Cuando le pidan "revisar" código en una entrevista, verifique sistemáticamente:

CategoríaPuntos a verificar
Null safety¿Verificaciones de null en las entradas? ¿Se usa Optional?
Nomenclatura¿Nombres explícitos? ¿Convenciones Java (camelCase)?
Excepciones¿Catch específico? ¿No se tragan excepciones?
Strings¿.equals() no ==? ¿StringBuilder para concatenación en bucle?
Colecciones¿Interfaces (List, no ArrayList)? ¿Inmutables cuando sea posible?
SOLID¿Una sola responsabilidad? ¿Depende de abstracciones?
Thread safety¿Variables compartidas protegidas?
Performance¿No hay N+1? ¿No hay bucle O(n²) oculto?
Seguridad¿No hay inyección SQL? ¿Entradas validadas?

⬅️ Capítulo 4: REST & Patrones | ⬅️ Volver al Módulo 18