Capítulo 5: Depuração & Revisão de Código
Estes exercícios simulam perguntas de entrevista onde você recebe um código bugado ou mal escrito e deve encontrar os problemas e corrigi-los. Esse formato é muito popular em grandes empresas (estilo Sonar/Codility).
Exercício 1 — Encontrar e corrigir 4 bugs
"Este código tem 4 bugs. Encontre-os e corrija-os."
O código bugado
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
}
}
O código corrigido
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: < ao invés de <=
Product p = products.get(i);
if (p.getPrice() > 0) {
total += p.getPrice(); // FIX 2: += ao invés de =+
}
}
double discount = 0;
if ("SAVE20".equals(coupon)) { // FIX 3: .equals() ao invés de ==
discount = total * 0.20;
}
return total - discount;
}
}
Explicação dos bugs
| Bug | Problema | Consequência | Correção |
|---|---|---|---|
| 1 | <= ao invés de < | ArrayIndexOutOfBoundsException | Trocar para < |
| 2 | =+ ao invés de += | =+ atribui +valor (positivo), não acumula | Trocar para += |
| 3 | == compara referências | Duas String idênticas podem ter referências diferentes | Usar .equals() |
| 4 | Sem verificação de null | NullPointerException se products for null | Adicionar guard clause |
=+vs+=: Um atribui, o outro soma. Muito sutil visualmente.==vs.equals(): Em Java,==compara endereços de memória, não o conteúdo.- Off-by-one (
<=vs<): O erro mais clássico em programação.
"Encontrei quatro bugs: erro off-by-one no loop, atribuição ao invés de adição, comparação de referência ao invés de .equals(), e ausência de verificação de null. Esses são clássicos pitfalls de Java."
Exercício 2 — Revisão completa de código
"Faça uma revisão deste código. O que você melhoraria?"
O código para 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;
}
}
O código melhorado
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 os problemas identificados
| # | Problema | Correção | Princípio |
|---|---|---|---|
| 1 | ArrayList na assinatura | Usar List (interface) | Programar contra interface |
| 2 | Método static | Remover static | Testabilidade (mocking) |
| 3 | Sem guard clause para null | Adicionar if (data == null) return List.of() | Null safety |
| 4 | if aninhados (3 níveis) | Stream pipeline | Legibilidade |
| 5 | s.equals("SKIP") | "SKIP".equals(s) | Evitar NPE |
| 6 | s.length() > 0 | s.isEmpty() | Código idiomático |
| 7 | ArrayList mutável retornada | toList() retorna lista imutável | Imutabilidade |
"Eu refatoraria para usar streams pela legibilidade, programaria para interfaces e adicionaria null safety. Esse é exatamente o tipo de código que o Sonar apontaria — condicionais aninhadas e estado mutável."
Exercício 3 — Encontrar o vazamento de memória
"Este código causa um vazamento de memória. Encontre-o."
O 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
}
O código corrigido
@Service
public class CacheService {
private final Map<String, Object> cache = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return size() > 1000; // Limite a 1000 entradas
}
};
// Ou melhor, usar Caffeine/Redis
}
Ainda melhor: 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);
}
}
"Um cache HashMap sem política de expiração é um vazamento de memória esperando para acontecer. Eu usaria Spring Cache com Caffeine ou Redis, que gerenciam TTL e tamanho máximo automaticamente."
Exercício 4 — Encontrar o problema de concorrência
"Este código é thread-safe? Encontre o problema."
O código problemático
@Service
public class CounterService {
private int count = 0;
public void increment() {
count++; // NÃO é thread-safe!
}
public int getCount() {
return count;
}
}
Problema: count++ não é atômico. Na verdade, são 3 operações:
- Ler
count - Somar 1
- Escrever o novo valor
Se dois threads fizerem count++ ao mesmo tempo, um incremento pode ser perdido.
As soluções
// Solução 1: AtomicInteger
@Service
public class CounterService {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
// Solução 2: synchronized
@Service
public class CounterService {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
"count++ não é atômico — é read-modify-write. Eu usaria AtomicInteger para thread safety sem lock, ou synchronized se precisar proteger uma seção crítica maior."
Exercício 5 — Corrigir má gestão de exceções
"O que está errado com este tratamento de exceções?"
O código ruim
@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 muito amplo
// PROBLEMA 2: engole a exceção silenciosamente
// PROBLEMA 3: log inútil
System.out.println("Error: " + e.getMessage());
}
}
}
O código corrigido
@Service
@Slf4j
public class PaymentService {
@Transactional
public void processPayment(PaymentRequest req) {
try {
gateway.charge(req.getAmount());
} catch (PaymentDeclinedException e) {
log.warn("Pagamento recusado para o pedido {}: {}", req.getOrderId(), e.getMessage());
throw new BusinessException("Pagamento foi recusado", e);
} catch (GatewayTimeoutException e) {
log.error("Timeout do gateway para o pedido {}", req.getOrderId(), e);
throw new ServiceUnavailableException("Gateway de pagamento está fora do ar", e);
}
orderService.updateStatus(req.getOrderId(), "PAID");
}
}
Problemas identificados
| # | Problema | Por que é grave |
|---|---|---|
| 1 | catch (Exception e) | Captura tudo, inclusive NullPointerException, OutOfMemoryError |
| 2 | Exceção engolida | O chamador nunca sabe que o pagamento falhou |
| 3 | System.out.println | Sem log estruturado, sem stack trace, perdido em produção |
| 4 | Sem rollback | Se o charge der certo mas o updateStatus falhar, dados inconsistentes |
"Nunca capture Exception genérico. Capture exceções específicas, faça log com o nível adequado (warn vs error), relance como exceções de negócio e use @Transactional para consistência dos dados."
Exercício 6 — Identificar code smells
"Liste todos os problemas neste código."
O código para analisar
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;
}
}
Os problemas
| Método | Problemas |
|---|---|
x() | Nome incompreensível, concatenação em loop (O(n²)), sem verificação de null |
check() | == true redundante, if/else return true/false pode ser expressão |
calc() | == ao invés de .equals(), divisão por zero, retorna 0 silenciosamente |
O código corrigido
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("Divisão por zero");
yield a / b;
}
default -> throw new IllegalArgumentException("Operador desconhecido: " + operator);
};
}
}
Checklist de revisão de código
Quando pedirem para você "revisar" um código em entrevista, verifique sistematicamente:
| Categoria | Pontos a verificar |
|---|---|
| Null safety | Verificações de null nas entradas? Optional usado? |
| Nomenclatura | Nomes explícitos? Convenções Java (camelCase)? |
| Exceções | Catch específico? Nenhuma exceção engolida? |
| Strings | .equals() e não ==? StringBuilder para concatenação em loop? |
| Coleções | Interfaces (List, não ArrayList)? Imutáveis quando possível? |
| SOLID | Uma única responsabilidade? Depende de abstrações? |
| Thread safety | Variáveis compartilhadas protegidas? |
| Performance | Sem N+1? Sem loop O(n²) escondido? |
| Segurança | Sem SQL injection? Entradas validadas? |