Capítulo 5: Depuración y Revisión de Código
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
| Error | Problema | Consecuencia | Corrección |
|---|---|---|---|
| 1 | <= en lugar de < | ArrayIndexOutOfBoundsException | Cambiar a < |
| 2 | =+ en lugar de += | =+ asigna +valor (positivo), no acumula | Cambiar a += |
| 3 | == compara referencias | Dos String idénticos pueden tener referencias diferentes | Usar .equals() |
| 4 | No hay verificación de null | NullPointerException si products es null | Agregar una guard clause |
=+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.
"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
| # | Problema | Corrección | Principio |
|---|---|---|---|
| 1 | ArrayList en la firma | Usar List (interfaz) | Programar contra la interfaz |
| 2 | Método static | Quitar static | Testabilidad (mocking) |
| 3 | Sin guard clause para null | Agregar if (data == null) return List.of() | Seguridad ante null |
| 4 | if anidados (3 niveles) | Stream pipeline | Legibilidad |
| 5 | s.equals("SKIP") | "SKIP".equals(s) | Evitar NPE |
| 6 | s.length() > 0 | s.isEmpty() | Código idiomático |
| 7 | ArrayList mutable retornada | toList() retorna una lista inmutable | Inmutabilidad |
"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);
}
}
"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:
- Leer
count - Sumar 1
- 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;
}
}
"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
| # | Problema | Por qué es grave |
|---|---|---|
| 1 | catch (Exception e) | Atrapa todo, incluso NullPointerException, OutOfMemoryError |
| 2 | Excepción tragada | El llamador nunca sabe que el pago falló |
| 3 | System.out.println | Sin log estructurado, sin stack trace, perdido en producción |
| 4 | Sin rollback | Si el charge tiene éxito pero updateStatus falla, datos inconsistentes |
"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étodo | Problemas |
|---|---|
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ía | Puntos 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? |