Java throw e throws
Até agora, quem lançava as exceções era o Java: o parseInt, a divisão por zero, o array. Nesta aula, você lança. Com throw, o seu método recusa um valor inválido e avisa quem o chamou. Com throws, ele declara, na própria assinatura, que pode lançar certo tipo de exceção. E você vai ver a exceção "subindo" de método em método até alguém tratá-la.
- Lançar uma exceção com
throw new IllegalArgumentException("…"). - Validar os parâmetros de um método logo no começo.
- Como uma exceção se propaga pela pilha de chamadas, passo a passo.
- Declarar exceções com
throwse a diferença entrethrowethrows. - Os erros de compilação mais comuns ao usar
throw.
A ideia antes do código
A Ana trabalha no balcão de uma loja. Um cliente pede para trocar um produto comprado há dois anos. A regra não permite, e ela não tem autoridade para abrir exceção. O que ela faz? Não finge que trocou, nem fica parada: ela recusa e avisa ("não posso fazer isso, o prazo acabou") e chama a gerente. Se a gerente também não resolver, chama o dono.
No Java, um método que recebe um pedido impossível faz o mesmo: lança uma exceção (throw, "lançar, arremessar") com uma mensagem explicando o motivo. A exceção vai para quem chamou o método; se ele não tratar, vai para quem chamou ele, e assim por diante, até alguém tratar (um catch) ou até chegar ao topo e o programa encerrar.
Lançar uma exceção com throw
A Ana tem um método que cadastra clientes. Uma idade negativa não faz sentido, e o método não deve aceitar:
public class Main {
static void cadastrar(String nome, int idade) {
if (idade < 0) {
throw new IllegalArgumentException("idade não pode ser negativa: " + idade);
}
System.out.println("Cadastrado: " + nome + ", " + idade + " anos");
}
public static void main(String[] args) {
cadastrar("Ana", 29);
cadastrar("Bruno", -3);
cadastrar("Carla", 22);
}
}
- throw
- "Lance". Interrompe o método na hora e envia a exceção para quem o chamou. Nenhuma linha depois dele, no método, roda.
- new
- Cria um objeto novo, como em qualquer classe (veja Construtores). Exceções são objetos: primeiro você cria, depois lança.
- IllegalArgumentException
- "Exceção de argumento ilegal": a exceção que o Java usa quando um método recebe um argumento (parâmetro) com valor inaceitável. Já existe na biblioteca; não precisa de
import(está emjava.lang). - ("idade não pode ser negativa: " + idade)
- A mensagem, passada ao construtor. É ela que aparece depois dos dois-pontos no stack trace e em
getMessage(). Inclua o valor recebido: ajuda muito a achar o problema.
$ java Main.java Cadastrado: Ana, 29 anos Exception in thread "main" java.lang.IllegalArgumentException: idade não pode ser negativa: -3 at Main.cadastrar(Main.java:4) at Main.main(Main.java:11)
O stack trace tem o mesmo formato das exceções do Java, agora com a sua mensagem. A primeira linha at aponta a linha 4, a do throw; a segunda, a linha 11, onde o main chamou cadastrar("Bruno", -3). A Carla nunca foi cadastrada, porque ninguém tratou a exceção.
Tratando o que você lançou
Uma exceção lançada por você é capturada exatamente como as do Java, com try e catch:
public static void main(String[] args) {
String[] nomes = {"Ana", "Bruno", "Carla"};
int[] idades = {29, -3, 22};
for (int i = 0; i < nomes.length; i++) {
try {
cadastrar(nomes[i], idades[i]);
} catch (IllegalArgumentException e) {
System.out.println("Não cadastrei " + nomes[i] + ": " + e.getMessage());
}
}
}
$ java Main.java Cadastrado: Ana, 29 anos Não cadastrei Bruno: idade não pode ser negativa: -3 Cadastrado: Carla, 22 anos
Repare na divisão de tarefas: o método cadastrar detecta o problema e explica; o main decide o que fazer (aqui, avisar e seguir para o próximo). Quem detecta nem sempre é quem sabe decidir.
Missão: o método aceita qualquer nota, até 11. Faça-o lançar IllegalArgumentException com uma mensagem quando a nota for menor que 0 ou maior que 10. A nota 9.5 deve ser registrada e a 11 deve parar o programa com a sua exceção.
public class Main {
static void registrarNota(double nota) {
System.out.println("Nota registrada: " + nota);
}
public static void main(String[] args) {
registrarNota(9.5);
registrarNota(11);
registrarNota(7);
}
}
public class Main {
static void registrarNota(double nota) {
if (nota < 0 || nota > 10) {
throw new IllegalArgumentException("nota fora de 0 a 10: " + nota);
}
System.out.println("Nota registrada: " + nota);
}
public static void main(String[] args) {
registrarNota(9.5);
registrarNota(11);
registrarNota(7);
}
}
Validar parâmetros: a "guarda" no começo do método
O padrão que você acabou de usar tem nome: cláusula de guarda. Logo na entrada do método, antes de fazer qualquer coisa, você confere os parâmetros e lança uma exceção se algo estiver errado. Assim o método nunca trabalha com dados inválidos, e o problema é apontado no lugar certo, não três métodos depois.
As exceções da biblioteca que mais servem para isso:
| Exceção | Quando lançar | Exemplo |
|---|---|---|
IllegalArgumentException | Um parâmetro tem valor inaceitável | Idade negativa, nota 11, quantidade 0 |
IllegalStateException | O objeto não está num estado que permita a operação | Sacar de uma conta encerrada, vender com o caixa fechado |
NullPointerException | Recebeu null onde não pode (use Objects.requireNonNull) | Nome do cliente null |
UnsupportedOperationException | A operação não existe para este objeto | Alterar uma lista que é só de leitura |
IndexOutOfBoundsException | Uma posição fora dos limites | Pedir o item 10 de uma lista de 3 |
Para o null, há um atalho pronto, o Objects.requireNonNull ("exige não nulo"), da classe java.util.Objects:
import java.util.Objects;
public class Main {
static String saudacao(String nome) {
Objects.requireNonNull(nome, "nome não pode ser null");
return "Olá, " + nome + "!";
}
public static void main(String[] args) {
System.out.println(saudacao("Ana"));
System.out.println(saudacao(null));
}
}
$ java Main.java Olá, Ana! Exception in thread "main" java.lang.NullPointerException: nome não pode ser null at java.base/java.util.Objects.requireNonNull(Objects.java:246) at Main.saudacao(Main.java:5) at Main.main(Main.java:11)
Se o valor for null, ele lança NullPointerException com a sua mensagem; se não for, não faz nada. É o mesmo que escrever if (nome == null) throw new NullPointerException("…");, numa linha só.
Propagação: a exceção subindo pela pilha
Agora vamos ver de perto o caminho de uma exceção. O pedido da loja passa por três métodos: o main chama fazerPedido, que chama calcularTotal, que chama validarQuantidade. Cada método mostra uma mensagem ao começar e ao terminar (com espaços na frente, para ver a "profundidade"):
public class Main {
static void validarQuantidade(int qtd) {
System.out.println(" validarQuantidade(" + qtd + ")");
if (qtd <= 0) {
throw new IllegalArgumentException("quantidade inválida: " + qtd);
}
System.out.println(" quantidade ok");
}
static double calcularTotal(int qtd, double preco) {
System.out.println(" calcularTotal começou");
validarQuantidade(qtd);
System.out.println(" calcularTotal terminou");
return qtd * preco;
}
static void fazerPedido(String produto, int qtd, double preco) {
System.out.println("fazerPedido: " + produto);
double total = calcularTotal(qtd, preco);
System.out.println("Total: " + total);
}
public static void main(String[] args) {
fazerPedido("pão", 3, 0.5);
fazerPedido("bolo", 0, 25.0);
System.out.println("Fim");
}
}
$ java Main.java fazerPedido: pão calcularTotal começou validarQuantidade(3) quantidade ok calcularTotal terminou Total: 1.5 fazerPedido: bolo calcularTotal começou validarQuantidade(0) Exception in thread "main" java.lang.IllegalArgumentException: quantidade inválida: 0 at Main.validarQuantidade(Main.java:5) at Main.calcularTotal(Main.java:12) at Main.fazerPedido(Main.java:19) at Main.main(Main.java:25)
O pedido de pão percorre tudo: desce até validarQuantidade e volta, terminando cada método. O de bolo desce do mesmo jeito, mas, na linha 5, o throw lança a exceção. A partir daí, acompanhe:
validarQuantidadeé interrompido:quantidade oknão aparece. Não hácatchali, então a exceção sai do método e "cai" em quem o chamou.- Ela chega na linha 12 de
calcularTotal. Também não hácatch:calcularTotal terminounão aparece, e nada é devolvido. A exceção cai mais um nível. - Chega na linha 19 de
fazerPedido. Semcatch:Total:não aparece. - Chega na linha 25 do
main. Semcatch, e omainé o fundo da pilha: a exceção sai do programa, o Java mostra o stack trace e encerra.Fimnão aparece.
O stack trace é exatamente esse caminho, de cima para baixo: linha 5, 12, 19, 25. Isso se chama propagação: a exceção sobe pela pilha de chamadas, abandonando cada método, até achar um catch que combine.
Tratando no lugar certo
Quem deve tratar? Quem sabe o que fazer. O validarQuantidade não sabe se deve avisar na tela, pedir de novo ou cancelar o pedido; o main, que conversa com o usuário, sabe. Basta um try/catch lá em cima:
public static void main(String[] args) {
try {
fazerPedido("pão", 3, 0.5);
fazerPedido("bolo", 0, 25.0);
} catch (IllegalArgumentException e) {
System.out.println("Pedido recusado: " + e.getMessage());
}
System.out.println("Fim");
}
$ java Main.java fazerPedido: pão calcularTotal começou validarQuantidade(3) quantidade ok calcularTotal terminou Total: 1.5 fazerPedido: bolo calcularTotal começou validarQuantidade(0) Pedido recusado: quantidade inválida: 0 Fim
A exceção atravessou calcularTotal e fazerPedido sem que eles precisassem de nenhuma linha a mais, e foi parar no catch do main. Os métodos do meio continuam simples: só cuidam do caso normal.
Declarar com throws
throws (com s, "lança", na terceira pessoa) não lança nada: é um aviso na assinatura do método dizendo "este método pode lançar tal exceção; quem me chamar, fique atento". Ele fica entre os parênteses dos parâmetros e a chave de abertura:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
static String lerArquivo(String arquivo) throws IOException {
return Files.readString(Path.of(arquivo));
}
public static void main(String[] args) {
try {
System.out.print(lerArquivo("Documentos/notas.txt"));
System.out.print(lerArquivo("Documentos/notas2.txt"));
} catch (IOException e) {
System.out.println("Não consegui ler: " + e);
}
}
}
- static String lerArquivo(String arquivo)
- A assinatura de sempre: devolve uma
String, recebe o nome do arquivo. - throws IOException
- A declaração: "posso lançar
IOException" (exceção de entrada e saída, input/output).Files.readStringlança essa exceção quando não consegue ler o arquivo, e olerArquivonão a trata: deixa passar para quem chamou. - catch (IOException e)
- Quem chamou trata.
$ java Main.java Comprar café Ligar para o João Estudar Bash Fazer backup das fotos Não consegui ler: java.nio.file.NoSuchFileException: Documentos/notas2.txt
O primeiro arquivo existe (é a lista de lembretes da Ana) e foi mostrado; o segundo não, e a NoSuchFileException ("não existe tal arquivo", uma filha de IOException) foi parar no catch. Ler e escrever arquivos tem aula própria: Ler e Escrever Arquivos.
Por que o throws aqui é obrigatório
Tire o throws IOException do lerArquivo e veja o javac recusar:
$ java Main.java
Main.java:7: error: unreported exception IOException; must be caught or declared to be thrown
return Files.readString(Path.of(arquivo));
^
Main.java:14: error: exception IOException is never thrown in body of corresponding try statement
} catch (IOException e) {
^
2 errors
error: compilation failed
O primeiro erro diz: "exceção IOException não informada; precisa ser capturada ou declarada para ser lançada". O segundo é consequência do primeiro: agora, para o javac, nada dentro do try do main lança IOException, então aquele catch seria inútil. A IOException é uma exceção checked ("verificada"): o compilador obriga você a tratar ou declarar. Já a IllegalArgumentException que você lançou antes é unchecked, e o javac não exige nada. Essa diferença é o assunto inteiro da próxima aula, Checked × Unchecked.
Até o main pode ter throws
Se ninguém tratar, a declaração sobe até o main: public static void main(String[] args) throws IOException. É comum em programas pequenos e exemplos. Se o arquivo não existir, a exceção sai do main e o programa encerra com o stack trace, como qualquer exceção não tratada:
$ java Main.java Exception in thread "main" java.nio.file.NoSuchFileException: Documentos/notas2.txt at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92) at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106) at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111) at java.base/sun.nio.fs.UnixFileSystemProvider.newFileChannel(UnixFileSystemProvider.java:213) at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:244) at java.base/java.nio.file.Files.newByteChannel(Files.java:357) at java.base/java.nio.file.Files.newByteChannel(Files.java:399) at java.base/java.nio.file.Files.readAllBytes(Files.java:2973) at java.base/java.nio.file.Files.readString(Files.java:3043) at java.base/java.nio.file.Files.readString(Files.java:3006) at Main.lerArquivo(Main.java:7) at Main.main(Main.java:11)
Dez linhas da biblioteca (o Java chegou até o sistema de arquivos do Linux, UnixFileSystemProvider) e, no fim, as duas suas: lerArquivo, linha 7, chamado pelo main, linha 11.
throws com várias exceções e com unchecked
- Várias, separadas por vírgula:
void importar() throws IOException, InterruptedException. - Exceções unchecked também podem aparecer no
throws, como documentação:void cadastrar(int idade) throws IllegalArgumentException. Ojavacaceita, mas não exige, e quem chama não é obrigado a tratar.
throw × throws
throw | throws | |
|---|---|---|
| O que é | Um comando: lança agora | Uma declaração: pode lançar |
| Onde fica | Dentro do corpo do método | Na assinatura, depois dos parênteses |
| Seguido de | Um objeto: throw new X("…") | Nomes de classes: throws X, Y |
| Quantas | Uma exceção por vez | Uma lista |
| Obrigatório? | Você usa quando quer lançar | Obrigatório para exceções checked que o método não trata |
| Exemplo | throw new IllegalArgumentException("idade negativa"); | static String ler() throws IOException |
Um truque para lembrar: throw é o verbo no imperativo, "lance!"; throws é o verbo descrevendo o método, "ele lança".
Quando dá errado
public class Main {
public static void main(String[] args) {
throw "deu ruim";
}
}
$ java Main.java
Main.java:3: error: incompatible types: String cannot be converted to Throwable
throw "deu ruim";
^
1 error
error: compilation failed
O que aconteceu: o throw só aceita objetos da família Throwable. Um texto não é uma exceção.
Como resolver: coloque o texto dentro de uma exceção: throw new IllegalStateException("deu ruim");.
public class Main {
public static void main(String[] args) {
throw IllegalArgumentException("deu ruim");
}
}
$ java Main.java
Main.java:3: error: cannot find symbol
throw IllegalArgumentException("deu ruim");
^
symbol: method IllegalArgumentException(String)
location: class Main
1 error
error: compilation failed
O que aconteceu: faltou o new. Sem ele, o Java acha que IllegalArgumentException(…) é a chamada de um método com esse nome, e não encontra nenhum.
Como resolver: throw new IllegalArgumentException("deu ruim");.
public class Main {
static int dobro(int x) {
if (x < 0) {
throw new IllegalArgumentException("negativo: " + x);
System.out.println("nunca roda");
}
return x * 2;
}
public static void main(String[] args) {
System.out.println(dobro(4));
}
}
$ java Main.java
Main.java:5: error: unreachable statement
System.out.println("nunca roda");
^
1 error
error: compilation failed
O que aconteceu: "comando inalcançável". Depois de um throw, o bloco termina, como depois de um return. O javac não aceita código que nunca vai rodar.
Como resolver: coloque o que precisa rodar antes do throw, ou apague a linha.
O que aconteceu: você chamou um método que lança uma exceção checked (como Files.readString, que lança IOException) sem tratar nem declarar.
Como resolver: envolva a chamada num try/catch ou acrescente throws ao seu método. Veja todos os detalhes em Checked × Unchecked.
Referência
| Sintaxe | O que faz |
|---|---|
throw new Tipo("mensagem"); | Cria e lança a exceção; o método para ali |
if (x < 0) throw new IllegalArgumentException("…"); | Cláusula de guarda: valida o parâmetro |
Objects.requireNonNull(x, "msg") | Lança NullPointerException("msg") se x for null |
tipo metodo(…) throws A, B { … } | Declara que o método pode lançar A e B |
main(String[] args) throws IOException | Deixa a exceção sair do programa |
| Propagação | Sem catch, a exceção sai do método e vai para quem chamou, até o main |
Exercícios
1. Lançar
Complete para lançar a exceção:
2. Declarar
Complete a assinatura para declarar que o método pode lançar IOException:
3. A exceção certa
Um método sacar(double valor) recebeu -50. Qual exceção da biblioteca combina? (só o nome da classe)
4. O main chama a(), que chama b(), que lança uma exceção. Só o main tem try/catch. O que acontece com as linhas de a() que vinham depois da chamada a b()?
5. Qual frase está certa?
6. Onde é melhor validar a idade recebida por cadastrar(String nome, int idade)?
Missão final: a exceção lançada em validarQuantidade sobe até o main e derruba o programa. Sem mexer nos outros métodos, trate-a no main: mostre Pedido recusado: seguido da mensagem, e faça o Fim aparecer.
public class Main {
static void validarQuantidade(int qtd) {
System.out.println(" validarQuantidade(" + qtd + ")");
if (qtd <= 0) {
throw new IllegalArgumentException("quantidade inválida: " + qtd);
}
System.out.println(" quantidade ok");
}
static double calcularTotal(int qtd, double preco) {
System.out.println(" calcularTotal começou");
validarQuantidade(qtd);
System.out.println(" calcularTotal terminou");
return qtd * preco;
}
static void fazerPedido(String produto, int qtd, double preco) {
System.out.println("fazerPedido: " + produto);
double total = calcularTotal(qtd, preco);
System.out.println("Total: " + total);
}
public static void main(String[] args) {
fazerPedido("pão", 3, 0.5);
fazerPedido("bolo", 0, 25.0);
System.out.println("Fim");
}
}
public class Main {
static void validarQuantidade(int qtd) {
System.out.println(" validarQuantidade(" + qtd + ")");
if (qtd <= 0) {
throw new IllegalArgumentException("quantidade inválida: " + qtd);
}
System.out.println(" quantidade ok");
}
static double calcularTotal(int qtd, double preco) {
System.out.println(" calcularTotal começou");
validarQuantidade(qtd);
System.out.println(" calcularTotal terminou");
return qtd * preco;
}
static void fazerPedido(String produto, int qtd, double preco) {
System.out.println("fazerPedido: " + produto);
double total = calcularTotal(qtd, preco);
System.out.println("Total: " + total);
}
public static void main(String[] args) {
try {
fazerPedido("pão", 3, 0.5);
fazerPedido("bolo", 0, 25.0);
} catch (IllegalArgumentException e) {
System.out.println("Pedido recusado: " + e.getMessage());
}
System.out.println("Fim");
}
}
Resumo
throw new Tipo("mensagem")cria e lança uma exceção; o método para naquela linha.- Valide os parâmetros no começo do método (cláusula de guarda) com
IllegalArgumentException,IllegalStateExceptionouObjects.requireNonNull. Coloque o valor recebido na mensagem. - Sem
catch, a exceção se propaga: abandona o método e cai em quem chamou, nível por nível, até umcatchou até sair domain. O stack trace mostra esse caminho. - Trate a exceção onde se sabe o que fazer com ela; os métodos do meio não precisam de código extra.
throwsna assinatura declara que o método pode lançar; é obrigatório para exceções checked comoIOException(próxima aula).