Java throw e throws
Lance exceções manualmente com throw e declare exceções em assinaturas de métodos com throws no Java.
Até agora estávamos capturando exceções lançadas por outro código. Agora você verá como lançar as suas próprias. Duas palavras-chave fazem a maior parte do trabalho — e é fácil confundi-las porque diferem em apenas uma letra.
throw— um comando que lança uma exceção em tempo de execução. Uma palavra, em código que roda.throws— uma declaração na assinatura de um método que diz "este método pode lançar estes tipos de exceção." É verificado pelo compilador, nunca é executado.
throw acontece. throws avisa. Mantenha esse par em mente.
Lançando uma exceção
throw recebe uma expressão do tipo Throwable (ou qualquer subtipo) e a lança. O método atual sai imediatamente, a pilha começa a ser desempilhada e a exceção inicia sua busca por um catch correspondente.
if (amount < 0) {
throw new IllegalArgumentException("amount must be non-negative, got " + amount);
}Três detalhes:
- Você só pode lançar um
Throwable. O compilador garante isso —throw "oops";não compila. - Você sempre lança uma instância, não uma classe.
throw new X(...), nuncathrow X. - A instância pode ser uma que você criou inline (comum), ou um objeto preexistente (raro — exceções carregam rastreamentos de pilha a partir da sua construção, então reutilizar uma congela o rastreamento errado).
Quando lançar
Lance uma exceção quando o método atual não consegue cumprir seu contrato. Alguns casos claros:
- Argumentos inválidos —
IllegalArgumentExceptionpara "você me chamou errado." - Estado incorreto —
IllegalStateExceptionpara "você me chamou no momento errado" (por exemplo,next()em um iterador vazio). - Dados ausentes — exceções específicas do domínio como
UserNotFoundException. - Operações externas com falha — erros de IO, erros de rede. Geralmente esses vêm da chamada que você acabou de fazer, então você não os constrói; você os deixa propagar, ou os envolve em uma exceção de nível superior.
O caso em que não se deve lançar: como atalho de fluxo de controle para resultados normais. "Lançar para controle de fluxo" é lento e confuso. Se "não encontrado" é um resultado comum, retorne um Optional<T>, não um NotFoundException.
Escolhendo um tipo
As exceções integradas de java.lang cobrem a maioria dos casos sem cerimônia:
IllegalArgumentException— argumento ruimIllegalStateException— estado incorretoNullPointerException— um argumento obrigatório era null (useObjects.requireNonNull)UnsupportedOperationException— operação não implementada (por exemplo,addem uma lista imutável)ArithmeticException— falha matemática
Quando a falha é específica do seu domínio — "usuário não encontrado," "cupom inválido," "configuração fora de sincronia" — escreva uma classe personalizada para ela. Dois capítulos adiante faremos exatamente isso.
A cláusula throws
Se o seu método pode lançar uma exceção verificada que ele mesmo não captura, você deve declará-la:
public Config loadConfig(Path p) throws IOException, ParseException {
String text = Files.readString(p);
return parser.parse(text);
}A cláusula faz parte do contrato do método. Ela diz a cada chamador: "se você me chamar, você precisa ou capturar essas exceções ou declará-las você mesmo." O compilador garante isso — é isso que as torna verificadas.
Algumas regras:
- Você declara apenas exceções verificadas.
RuntimeExceptions e suas subclasses são não verificadas — declará-las é permitido mas não obrigatório, e geralmente não é feito. - Você pode declarar mais tipos do que realmente lança — útil quando você está mantendo a opção aberta para implementações futuras, embora seja um pequeno ruído.
- Um método que sobrescreve outro pode declarar as mesmas ou menos exceções verificadas do que o pai (e apenas subtipos das declaradas). Não pode adicionar novas. Isso é a substituição de Liskov aplicada a exceções.
throw e throws juntos
Um método real geralmente faz os dois:
public User loadUser(String id) throws IOException {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("id must be non-blank");
}
String json = httpClient.get("/users/" + id); // may throw IOException
return parser.toUser(json);
}- O
throws IOExceptiondeclara a exceção verificada que pode vir dehttpClient.get. - O
throw new IllegalArgumentException(...)lança uma não verificada para entrada inválida. Ela não precisa aparecer na cláusulathrows.
Encapsulando uma exceção
Quando uma exceção de baixo nível não é significativa na sua camada, encapsule-a em uma que seja. Passe a original como causa para que o rastreamento permaneça intacto:
try {
return Files.readString(configPath);
} catch (IOException e) {
throw new ConfigLoadException("could not load config from " + configPath, e);
}O padrão do construtor de segundo argumento — (message, cause) — é padrão em Exception, IOException e todos os integrados. Quando você escreve sua própria classe de exceção, forneça ambos os construtores.
Um exemplo prático
Um pequeno auxiliar no estilo bancário que valida a entrada com IllegalArgumentException, sinaliza uma conta vazia com IllegalStateException, e deixa uma exceção verificada subir para o chamador via throws. O driver mostra como cada uma fica quando lançada.
Os três casos em tempo de execução — argumento, estado e saque bem-sucedido — passam por um único catch. O quarto, archive(), só compila porque main pode capturar Exception e porque archive() declarou throws ArchiveException. Tente remover a cláusula throws e o programa falha ao compilar.
O que vem a seguir
O compilador trata algumas exceções de forma estrita (você deve tratá-las) e outras de forma permissiva (você não precisa). Essa divisão é o próximo capítulo. Continue em Exceções verificadas vs. não verificadas no Java.