Java Depurar

O programa compila, roda, não dá erro nenhum… e mostra o resultado errado. Esse é o tipo de defeito mais difícil, porque o Java não aponta a linha. Depurar é o trabalho de detetive de descobrir onde o programa se desviou do que você queria. Nesta aula você aprende as ferramentas, da mais simples (println) à mais poderosa (o depurador).

Você vai aprender
  • O método de depuração: reproduzir, formular uma hipótese, observar, corrigir.
  • Usar println de forma estratégica (e tirar depois).
  • Ler um stack trace para achar a linha de uma exceção.
  • Usar o depurador de linha de comando jdb: stop at, run, print, locals, next, cont.
  • O que o depurador da IDE oferece: breakpoints, step over/into, watches.
  • Verificar suposições com assert e a opção -ea.
  • Registrar o que acontece com logs (java.util.logging e System.Logger).

A ideia: o detetive não chuta

A palavra inglesa para defeito é bug ("inseto"); depurar é debug, "tirar os insetos". Quem começa costuma depurar "no chute": muda uma linha, roda, muda outra, roda… Funciona às vezes, mas costuma criar defeitos novos. O detetive faz diferente:

  1. Reproduzir: achar uma entrada que sempre mostra o erro.
  2. Saber o que devia acontecer: calcule à mão o resultado certo.
  3. Observar: olhar os valores das variáveis no meio do caminho, até achar o primeiro ponto em que eles divergem do esperado.
  4. Corrigir a causa (e não o sintoma), e rodar de novo. Se houver testes, eles confirmam que nada mais quebrou.

O passo 3 é o que as ferramentas desta aula fazem: deixar você ver dentro do programa enquanto ele roda.

O caso: a média errada

A Ana escreveu um programa para calcular a média das notas de um aluno: 8, 7, 10 e 6. À mão: (8 + 7 + 10 + 6) / 4 = 31 / 4 = 7.75.

public class Media {
    public static void main(String[] args) {
        int[] notas = {8, 7, 10, 6};
        int soma = 0;
        for (int i = 1; i < notas.length; i++) {
            soma += notas[i];
        }
        double media = soma / notas.length;
        System.out.println("Média: " + media);
    }
}
$ javac -g Media.java
$ java Media
Média: 5.0

5.0, e não 7.75. Nenhuma mensagem de erro. Hora de investigar. (O -g no javac guarda informações extras de depuração no .class, como os nomes das variáveis locais; você vai precisar dele no jdb.)


1. O println estratégico

A ferramenta mais simples: pôr System.out.println nos pontos onde você quer ver os valores. "Estratégico" quer dizer: não espalhe prints em todo lugar, ponha onde as suas suposições podem estar erradas, e mostre nome e valor de cada variável:

public class Media {
    public static void main(String[] args) {
        int[] notas = {8, 7, 10, 6};
        int soma = 0;
        for (int i = 1; i < notas.length; i++) {
            System.out.println("DEBUG i=" + i + " nota=" + notas[i] + " soma=" + soma);
            soma += notas[i];
        }
        System.out.println("DEBUG soma final=" + soma + " quantidade=" + notas.length);
        double media = soma / notas.length;
        System.out.println("Média: " + media);
    }
}
$ java Media.java
DEBUG i=1 nota=7 soma=0
DEBUG i=2 nota=10 soma=7
DEBUG i=3 nota=6 soma=17
DEBUG soma final=23 quantidade=4
Média: 5.0

Leia com olho de detetive: a soma deveria ser 31, mas é 23. Faltam 8, justamente a primeira nota. E a primeira linha mostra por quê: o laço começa em i=1, e os arrays começam no índice 0 (veja Arrays). Primeiro defeito achado: int i = 1 deveria ser int i = 0.

Corrigido o laço, a Ana roda de novo, agora com prints da soma e da média:

$ java Media.java
DEBUG soma=31 quantidade=4
DEBUG media=7.0
Média: 7.0

A soma agora está certa (31), mas a média deu 7.0. O problema está entre os dois prints, na linha double media = soma / notas.length;. soma e notas.length são int, então a divisão é inteira: 31 / 4 = 7, e só depois o 7 vira 7.0 ao ser guardado no double (veja Operadores Aritméticos). O conserto é converter antes de dividir: (double) soma / notas.length.

$ java Media.java
Média: 7.75
Boas práticas com prints de depuração
  • Comece com uma marca fácil de achar (DEBUG) e apague todos depois; prints esquecidos poluem a saída do programa.
  • Mostre o nome junto do valor: "soma=" + soma, nunca só soma.
  • Para não misturar com a saída normal, use System.err.println: ela vai para a "saída de erros", que dá para separar no terminal (java Media 2>/dev/null esconde os prints de depuração).
  • Se você se pega pondo os mesmos prints de novo e de novo, é hora de um depurador ou de logs.

Missão: este é o programa original, com os dois defeitos. Ponha prints de depuração, ache-os e corrija até a saída ser Média: 7.75. Depois, apague os prints.

public class Main {
    public static void main(String[] args) {
        int[] notas = {8, 7, 10, 6};
        int soma = 0;
        for (int i = 1; i < notas.length; i++) {
            soma += notas[i];
        }
        double media = soma / notas.length;
        System.out.println("Média: " + media);
    }
}
public class Main {
    public static void main(String[] args) {
        int[] notas = {8, 7, 10, 6};
        int soma = 0;
        for (int i = 0; i < notas.length; i++) {
            soma += notas[i];
        }
        double media = (double) soma / notas.length;
        System.out.println("Média: " + media);
    }
}

2. Ler o stack trace

Quando o defeito causa uma exceção, o Java já faz metade do trabalho de detetive: ele imprime o stack trace ("rastro da pilha"), a lista de métodos que estavam rodando no momento do erro. Aqui, a Ana separou o cálculo num método e errou a condição do laço (<= em vez de <):

public class Boletim {
    static double media(int[] notas) {
        int soma = 0;
        for (int i = 0; i <= notas.length; i++) {
            soma += notas[i];
        }
        return (double) soma / notas.length;
    }

    public static void main(String[] args) {
        int[] notas = {8, 7, 10, 6};
        System.out.println("Média: " + media(notas));
    }
}
$ java Boletim.java
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4
	at Boletim.media(Boletim.java:5)
	at Boletim.main(Boletim.java:12)
Exception in thread "main"
Uma exceção não tratada parou o programa (no thread principal).
java.lang.ArrayIndexOutOfBoundsException
O quê: índice fora dos limites do array.
Index 4 out of bounds for length 4
Detalhes: tentou usar o índice 4 num array de tamanho 4 (os índices válidos vão de 0 a 3).
at Boletim.media(Boletim.java:5)
Onde: a primeira linha at é o lugar exato do erro, o método media, linha 5 (soma += notas[i];).
at Boletim.main(Boletim.java:12)
Quem chamou: o media foi chamado pelo main, na linha 12. Lendo de cima para baixo, você vai "voltando" pelo caminho do programa.

Com o stack trace, a pergunta deixa de ser "onde está o erro?" e vira "por que i chegou a 4 na linha 5?". Resposta: o <= deixa o laço rodar com i == notas.length. Em programas grandes, o stack trace tem dezenas de linhas, muitas de bibliotecas (at java.base/…, at org.junit…); procure a primeira linha que é do seu código. A aula O que são Exceções explica as partes com mais calma, e a Exceções Comuns lista as mais frequentes.

Missão: rode o Boletim, leia o stack trace no editor, vá até a linha apontada e corrija o laço para a saída ser Média: 7.75.

public class Main {
    static double media(int[] notas) {
        int soma = 0;
        for (int i = 0; i <= notas.length; i++) {
            soma += notas[i];
        }
        return (double) soma / notas.length;
    }

    public static void main(String[] args) {
        int[] notas = {8, 7, 10, 6};
        System.out.println("Média: " + media(notas));
    }
}
public class Main {
    static double media(int[] notas) {
        int soma = 0;
        for (int i = 0; i < notas.length; i++) {
            soma += notas[i];
        }
        return (double) soma / notas.length;
    }

    public static void main(String[] args) {
        int[] notas = {8, 7, 10, 6};
        System.out.println("Média: " + media(notas));
    }
}

3. O depurador: jdb

Um depurador (debugger) roda o seu programa em câmera lenta. Você marca uma linha com um ponto de parada (breakpoint); quando o programa chega lá, ele congela, e você pode olhar todas as variáveis, avançar uma linha por vez, e continuar. Nada de editar o código para pôr prints.

O JDK traz um depurador de linha de comando, o jdb (Java Debugger). Ele é simples e pouco usado no dia a dia (as IDEs têm depuradores visuais, na próxima seção), mas mostra muito bem como todo depurador funciona. Uma sessão real com o Media.java original (o do laço começando em 1), compilado com javac -g:

$ jdb Media
Initializing jdb ...
> stop at Media:6
Deferring breakpoint Media:6.
It will be set after the class is loaded.
> run
run Media
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint Media:6

Breakpoint hit: "thread=main", Media.main(), line=6 bci=34
6                soma += notas[i];

main[1] locals
Method arguments:
args = instance of java.lang.String[0] (id=431)
Local variables:
notas = instance of int[4] (id=432)
soma = 0
i = 1
main[1] print soma
 soma = 0
main[1] print notas[i]
 notas[i] = 7
main[1] next
>
Step completed: "thread=main", Media.main(), line=5 bci=40
5            for (int i = 1; i < notas.length; i++) {

main[1] next
>
Step completed:
Breakpoint hit: "thread=main", Media.main(), line=6 bci=34
6                soma += notas[i];

main[1] print soma
 soma = 7
main[1] cont
>
Breakpoint hit: "thread=main", Media.main(), line=6 bci=34
6                soma += notas[i];

main[1] print i
 i = 3
main[1] print soma
 soma = 17
main[1] clear Media:6
Removed: breakpoint Media:6
main[1] cont
> Média: 5.0

The application exited

Passo a passo do que a Ana fez:

Ela digitouO que aconteceu
stop at Media:6Marcou um ponto de parada na linha 6 da classe Media (a linha que soma). Como o programa ainda não começou, o jdb "adia" (deferring) a marca até a classe ser carregada.
runComeçou o programa. Ele rodou até a linha 6 e congelou: Breakpoint hit … line=6. O jdb mostra a linha que vai rodar (ainda não rodou). O prompt muda para main[1]: o thread main está parado.
localsMostrou todas as variáveis locais. A pista: na primeira passada pelo laço, i = 1. Deveria ser 0!
print soma, print notas[i]Mostrou uma variável ou expressão: a soma ainda é 0, e a nota da vez é 7 (a segunda nota; a primeira, 8, foi pulada).
nextExecutou a linha atual e parou na próxima (step over). Foi para a linha 5, o i++ e o teste do for. Outro next voltou à linha 6, onde está o breakpoint.
contContinuou rodando até o próximo breakpoint (a próxima volta do laço).
clear Media:6Tirou o breakpoint.
contO programa rodou até o fim: Média: 5.0 e The application exited.

Repare: a Ana descobriu o mesmo defeito do println, mas sem mudar uma linha do código. Outros comandos úteis do jdb:

ComandoO que faz
stop in Classe.metodoPara na entrada de um método
stepExecuta a linha e, se ela chamar um método, entra nele (step into)
step upRoda até o fim do método atual e volta para quem chamou (step out)
whereMostra a pilha de chamadas, como um stack trace
dump objetoMostra todos os atributos de um objeto
clearLista os breakpoints
helpLista todos os comandos
exitSai do jdb (e encerra o programa)

4. O depurador da IDE

No dia a dia, quase ninguém usa o jdb: as IDEs (IntelliJ IDEA, VS Code, Eclipse, NetBeans; veja Editores e IDEs) têm um depurador visual que faz o mesmo com cliques. Por baixo, elas usam o mesmo mecanismo do jdb. O que você vai encontrar:

Breakpoint
Clique na margem esquerda, ao lado do número da linha: aparece uma bolinha vermelha. É o stop at.
Botão Debug (o "inseto")
Roda o programa no modo de depuração, em vez do botão Run. É o run do jdb. O programa para na bolinha, e a linha fica destacada.
Painel de variáveis
Mostra sozinho todas as variáveis locais e os atributos dos objetos, que você pode abrir como pastas. É o locals e o dump, sempre atualizado.
Step Over
Executa a linha e para na próxima (next). IntelliJ: F8; VS Code: F10.
Step Into
Entra no método chamado na linha (step). IntelliJ: F7; VS Code: F11.
Step Out
Termina o método atual e volta para quem chamou (step up). IntelliJ: Shift+F8; VS Code: Shift+F11.
Resume / Continue
Continua até o próximo breakpoint (cont). IntelliJ: F9; VS Code: F5.
Watches (expressões vigiadas)
Uma lista de expressões que você escolhe, como soma / notas.length, recalculadas a cada passo. É um print automático.
Breakpoint condicional
Clique com o botão direito na bolinha e escreva uma condição, como i == 3: o programa só para quando ela for verdadeira. Ótimo para laços com milhares de voltas.
Call stack (pilha de chamadas)
A lista de métodos em execução, como o stack trace; clicar numa linha mostra as variáveis daquele método. É o where.

Os nomes e atalhos mudam um pouco de uma IDE para outra, mas as ideias são exatamente estas. Se você entendeu a sessão do jdb, sabe usar qualquer depurador.


5. assert e a opção -ea

Um assert ("afirmar") é uma suposição escrita no código: "neste ponto, isto tem de ser verdade". Se não for, o programa para com um AssertionError, bem perto da origem do problema, em vez de seguir com um valor maluco.

public class Turma {
    static double media(int[] notas) {
        assert notas.length > 0 : "lista de notas vazia";
        int soma = 0;
        for (int nota : notas) {
            soma += nota;
        }
        return (double) soma / notas.length;
    }

    public static void main(String[] args) {
        int[] semNotas = {};
        System.out.println("Média: " + media(semNotas));
    }
}
assert
Palavra-chave do Java.
notas.length > 0
A condição que você supõe verdadeira.
: "lista de notas vazia"
Opcional: a mensagem do erro, se a condição for falsa.

Agora o detalhe importante: os assert vêm desligados. Para ligar, use a opção -ea (enable assertions, "ligar as afirmações") no java:

$ java Turma.java
Média: NaN                          « assert desligado: 0.0 / 0 dá NaN ("não é um número")
$ java -ea Turma.java
Exception in thread "main" java.lang.AssertionError: lista de notas vazia
	at Turma.media(Turma.java:3)
	at Turma.main(Turma.java:13)

Sem o -ea, o programa segue e mostra NaN, um valor sem sentido que pode ir parar num relatório. Com o -ea, ele para na linha 3 e diz por quê. Funciona igual com classes compiladas: java -ea Turma.

Não use assert para validar dados do usuário

Como os assert ficam desligados quando o programa roda normalmente, eles servem para pegar erros de programação enquanto você desenvolve e testa, não para conferir o que vem de fora. Para recusar um valor inválido (um depósito negativo, um campo vazio), use if e throw new IllegalArgumentException(…) (veja throw e throws), que funcionam sempre. E nunca ponha no assert algo que precisa acontecer, como assert lista.remove(x): com o assert desligado, a remoção também some.


6. Logs: um diário do programa

Os prints de depuração somem quando você os apaga. Mas, num programa que roda todo dia (um sistema de loja, um servidor), você quer um diário permanente: o que aconteceu, quando, e com que gravidade. Isso é um log ("diário de bordo"). A diferença para o println: cada mensagem tem um nível de importância, data e hora, e você liga ou desliga níveis sem mexer no código.

java.util.logging

O Java traz um sistema de logs pronto, no pacote java.util.logging:

import java.util.logging.Level;
import java.util.logging.Logger;

public class Loja {
    private static final Logger LOG = Logger.getLogger(Loja.class.getName());

    public static void main(String[] args) {
        LOG.info("Caixa aberto");
        LOG.warning("Estoque baixo: café");
        LOG.fine("Detalhe que não aparece");
        LOG.log(Level.SEVERE, "Falha ao gravar a venda", new IllegalStateException("disco cheio"));
        System.out.println("Fim do expediente");
    }
}
$ javac Loja.java
$ java Loja
out. 16, 2026 6:50:21 PM Loja main
INFORMAÇÕES: Caixa aberto
out. 16, 2026 6:50:21 PM Loja main
ADVERTÊNCIA: Estoque baixo: café
out. 16, 2026 6:50:21 PM Loja main
GRAVE: Falha ao gravar a venda
java.lang.IllegalStateException: disco cheio
	at Loja.main(Loja.java:11)

Fim do expediente
Logger.getLogger(Loja.class.getName())
Pega um logger ("registrador") com o nome da classe. É costume ter um por classe, guardado num atributo private static final.
LOG.info / LOG.warning
Registram uma mensagem com o nível INFO (informação) ou WARNING (aviso). O idioma do computador traduz o nível: INFORMAÇÕES, ADVERTÊNCIA.
LOG.fine
Nível FINE (detalhe). Não apareceu: por padrão, só INFO para cima é mostrado. Os detalhes ficam no código, prontos para ligar quando precisar investigar, sem poluir a saída no dia a dia.
LOG.log(Level.SEVERE, "…", exceção)
Nível SEVERE (GRAVE), com a exceção e o stack trace junto.
out. 16, 2026 6:50:21 PM Loja main
Cada mensagem ganha data, hora, classe e método de onde veio.

Os logs vão para a saída de erros, não para a saída normal. Escondendo a saída de erros, só sobra o println:

$ java Loja.java 2>/dev/null
Fim do expediente

Os níveis, do mais grave ao mais detalhado: SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST.

System.Logger Java 9+

O System.Logger é uma "fachada" mais simples, que já vem no System sem import. Por padrão, ele usa o java.util.logging por baixo (por isso a saída tem o mesmo formato), mas, num projeto grande, pode ser ligado a outra biblioteca de logs sem mudar o seu código. E aceita "buracos" {0}, {1} para os valores:

public class Loja2 {
    private static final System.Logger LOG = System.getLogger("loja");

    public static void main(String[] args) {
        LOG.log(System.Logger.Level.INFO, "Caixa aberto");
        LOG.log(System.Logger.Level.WARNING, "Estoque baixo: {0} ({1} unidades)", "café", 2);
        LOG.log(System.Logger.Level.DEBUG, "Detalhe que não aparece");
    }
}
$ java Loja2.java
out. 16, 2026 6:50:14 PM Loja2 main
INFORMAÇÕES: Caixa aberto
out. 16, 2026 6:50:14 PM Loja2 main
ADVERTÊNCIA: Estoque baixo: café (2 unidades)

Em projetos profissionais, é comum usar bibliotecas de log de fora do JDK (SLF4J com Logback, Log4j 2), adicionadas pelo Maven; a ideia de níveis é a mesma.


Qual ferramenta usar?

SituaçãoFerramenta
O programa parou com uma exceçãoLeia o stack trace: a primeira linha at do seu código
Resultado errado num programa pequenoprintln estratégico, ou o depurador
Resultado errado num programa grande, ou num laço longoDepurador com breakpoint (condicional)
"Isto nunca deveria acontecer"assert, rodando com -ea durante o desenvolvimento
Saber o que aconteceu ontem, num programa que roda sempreLogs
Garantir que o defeito não voltaUm teste que reproduz o defeito

Quando dá errado

O assert "não funciona"

O que aconteceu: a condição é falsa, mas o programa não para. Os assert estão desligados. Como resolver: rode com java -ea …. Nas IDEs, a opção fica nas configurações de execução (VM options: -ea).

O jdb não mostra as variáveis locais

O que aconteceu: a classe foi compilada sem informação de depuração, e os nomes das variáveis locais não estão no .class. O mesmo efeito aparece até nas mensagens de exceção. Sem -g:

$ javac Pedido.java && java Pedido
Validando pedido...
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
	at Pedido.validar(Pedido.java:18)
	at Pedido.main(Pedido.java:7)
$ javac -g Pedido.java && java Pedido
Validando pedido...
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "nome" is null
	at Pedido.validar(Pedido.java:18)
	at Pedido.main(Pedido.java:7)

<local1> quer dizer "a variável local número 1", sem nome. Como resolver: compile com javac -g. (O Maven e o Gradle já compilam com -g, como mostra o [debug release 25] na saída do Maven.)

O breakpoint nunca é atingido

O que aconteceu: a linha marcada não é executada (está num if que não entrou, num método que ninguém chama) ou é uma linha sem código (em branco, comentário, só uma }). Como resolver: ponha o breakpoint numa linha com instrução, mais cedo no caminho, e avance com next/step.

"Arrumei, mas o erro continua igual"

O que aconteceu: você mudou o .java e rodou o .class antigo (esqueceu o javac), ou corrigiu o sintoma num lugar e a causa está em outro. Como resolver: recompile (ou use java Arquivo.java, que compila na hora). Volte ao método: ache o primeiro ponto em que um valor diverge do esperado.


Referência

ItemO que faz
System.err.println("DEBUG x=" + x)Print de depuração na saída de erros
javac -g Arquivo.javaCompila com informação de depuração
jdb ClasseAbre o depurador para a classe
stop at Classe:linha / stop in Classe.metodoBreakpoint numa linha / num método
run / contComeça / continua até o próximo breakpoint
next / step / step upStep over / step into / step out
print expr / locals / whereMostra uma expressão / as variáveis locais / a pilha
assert condição : "mensagem";Suposição verificada só com -ea
java -ea ClasseRoda com os assert ligados
Logger.getLogger(nome)Logger do java.util.logging
System.getLogger(nome)Logger da fachada System.Logger

Exercícios

1. Ligar os asserts

Complete o comando para rodar a classe Turma com os assert ligados:

java Turma

2. Breakpoint no jdb

Complete para parar na linha 12 da classe Boletim:

3. Onde está o erro?

Dado o stack trace abaixo, em que linha de Pedido.java a exceção aconteceu?

$ java Pedido
Validando pedido...
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "nome" is null
	at Pedido.validar(Pedido.java:18)
	at Pedido.main(Pedido.java:7)
Linha

4. No depurador, qual comando executa a linha atual e, se ela chamar um método, entra nele?

5. Por que o LOG.fine("…") não apareceu na saída?

6. Para recusar um depósito negativo digitado pelo usuário, o certo é:

Missão final: a Ana anotou as temperaturas de uma semana de inverno na serra, todas abaixo de zero. O programa diz que a maior foi 0, uma temperatura que nem aparece na lista! Investigue (com prints, se quiser) e corrija para mostrar Maior: -2. Dica: qual é o valor de maior antes da primeira comparação?

public class Main {
    public static void main(String[] args) {
        int[] temperaturas = {-5, -2, -8, -3, -6};
        int maior = 0;
        for (int t : temperaturas) {
            if (t > maior) {
                maior = t;
            }
        }
        System.out.println("Maior: " + maior);
    }
}
public class Main {
    public static void main(String[] args) {
        int[] temperaturas = {-5, -2, -8, -3, -6};
        int maior = temperaturas[0];
        for (int t : temperaturas) {
            if (t > maior) {
                maior = t;
            }
        }
        System.out.println("Maior: " + maior);
    }
}

Resumo

  • Depurar é método: reproduzir, saber o resultado certo, observar até achar o primeiro valor que diverge, corrigir a causa.
  • println estratégico: nome e valor, marca DEBUG, apagar depois (ou System.err).
  • Stack trace: tipo e mensagem da exceção; a primeira linha at do seu código é onde aconteceu; as de baixo, quem chamou.
  • Depurador: breakpoint, executar até ele, olhar variáveis, avançar (next = step over, step = step into), continuar. O jdb faz isso no terminal; as IDEs, com cliques e watches.
  • assert condição : "msg" só vale com java -ea; serve para erros de programação, não para validar dados.
  • Logs (java.util.logging, System.Logger) registram o que acontece com nível, data e hora; FINE/DEBUG ficam escondidos por padrão.