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.

Você vai aprender
  • 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 throws e a diferença entre throw e throws.
  • 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á em java.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çãoQuando lançarExemplo
IllegalArgumentExceptionUm parâmetro tem valor inaceitávelIdade negativa, nota 11, quantidade 0
IllegalStateExceptionO objeto não está num estado que permita a operaçãoSacar de uma conta encerrada, vender com o caixa fechado
NullPointerExceptionRecebeu null onde não pode (use Objects.requireNonNull)Nome do cliente null
UnsupportedOperationExceptionA operação não existe para este objetoAlterar uma lista que é só de leitura
IndexOutOfBoundsExceptionUma posição fora dos limitesPedir 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:

  1. validarQuantidade é interrompido: quantidade ok não aparece. Não há catch ali, então a exceção sai do método e "cai" em quem o chamou.
  2. Ela chega na linha 12 de calcularTotal. Também não há catch: calcularTotal terminou não aparece, e nada é devolvido. A exceção cai mais um nível.
  3. Chega na linha 19 de fazerPedido. Sem catch: Total: não aparece.
  4. Chega na linha 25 do main. Sem catch, e o main é o fundo da pilha: a exceção sai do programa, o Java mostra o stack trace e encerra. Fim nã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.readString lança essa exceção quando não consegue ler o arquivo, e o lerArquivo nã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. O javac aceita, mas não exige, e quem chama não é obrigado a tratar.

throw × throws

throwthrows
O que éUm comando: lança agoraUma declaração: pode lançar
Onde ficaDentro do corpo do métodoNa assinatura, depois dos parênteses
Seguido deUm objeto: throw new X("…")Nomes de classes: throws X, Y
QuantasUma exceção por vezUma lista
Obrigatório?Você usa quando quer lançarObrigatório para exceções checked que o método não trata
Exemplothrow 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

"String cannot be converted to Throwable"
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");.

"cannot find symbol … method IllegalArgumentException(String)"
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");.

"unreachable statement" depois do throw
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.

"unreported exception …; must be caught or declared to be thrown"

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

SintaxeO 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 IOExceptionDeixa a exceção sair do programa
PropagaçãoSem 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:

IllegalArgumentException("valor negativo");

2. Declarar

Complete a assinatura para declarar que o método pode lançar IOException:

static String ler() { … }

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, IllegalStateException ou Objects.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é um catch ou até sair do main. 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.
  • throws na assinatura declara que o método pode lançar; é obrigatório para exceções checked como IOException (próxima aula).