Um arquivo de cobranças contém duas linhas válidas, uma data impossível e um valor escrito como texto. Somar tudo como se nada tivesse acontecido produz um resultado errado. Parar sem explicar a causa também não ajuda quem enviou o arquivo. Precisamos distinguir falha de entrada, falha de leitura e uma operação válida.
Você já deve conhecer métodos, classes e coleções. Usaremos Java 21. Vamos combinar exceções, leitura de arquivo, datas e valores decimais em um exemplo pequeno, sem banco ou Spring. Ao final, você conseguirá rejeitar uma linha informando seu número e impedir que uma falha de acesso seja confundida com um arquivo vazio.
Exceção muda o caminho da execução
Uma exceção é um objeto que sinaliza uma situação em que o caminho normal não continuou. throw lança esse objeto. A execução procura um catch compatível; se não existir naquele método, a falha pode se propagar ao chamador.
O stack trace registra o caminho de chamadas relacionado à falha. Leia o tipo, a mensagem, a causa quando houver e a primeira linha relevante do seu código. Não tente resolver tudo adicionando catch (Exception e) {}: esse bloco vazio pode transformar uma falha em aparente sucesso.
Exceções checked, como IOException, precisam ser capturadas ou declaradas com throws. Exceções derivadas de RuntimeException são unchecked: o compilador não exige essa declaração. Isso não diz se uma falha é importante; diz como a linguagem exige que ela seja declarada ou tratada. Error representa outra categoria de problemas e não deve ser capturado indiscriminadamente como parte da regra de negócio.
Tratar significa decidir o que fazer
Se uma linha tem data inválida, podemos informar o número e continuar, porque escolhemos importação parcial. Se o arquivo inteiro não pode ser lido, não temos como calcular um total confiável: precisamos avisar a falha. Em um sistema financeiro real, talvez a política seja rejeitar o lote inteiro. A política vem do domínio, não do try/catch.
Quando você converte uma falha para outra exceção, preserve sua causa, por exemplo new IllegalStateException("Falha ao importar", causa). A mensagem explica o contexto; a causa mantém o diagnóstico. Capturar e registrar a mesma falha em todas as camadas costuma gerar ruído duplicado.
Um arquivo é bytes; um charset dá significado ao texto
Path representa um caminho. Files oferece operações sobre arquivos. Um charset, como UTF-8, define como bytes são interpretados como caracteres. Declarar o charset ajuda a evitar que nomes acentuados dependam da configuração da máquina.
Um leitor mantém um recurso aberto. Try-with-resources chama close ao sair do bloco, inclusive quando uma exceção ocorre. Isso é diferente de esperar o coletor de lixo. Um recurso pode implementar AutoCloseable para participar desse mecanismo.
Data de vencimento não é instante global
LocalDate representa ano, mês e dia sem horário ou fuso: é adequado para um vencimento definido por data. Instant representa um ponto na linha do tempo, útil para registrar quando algo aconteceu. ZonedDateTime combina data/hora com uma zona, como America/Sao_Paulo.
Uma data não vira um instante sem uma decisão adicional sobre horário e zona. Também não trate “amanhã às nove” como simplesmente somar 24 horas em qualquer contexto: mudanças de fuso e calendário podem importar. Nosso arquivo usa datas ISO, como 2026-10-01, evitando ambiguidade entre dia e mês.
Dinheiro exige uma política de precisão
double usa representação binária e não representa exatamente todos os decimais. Para estes valores monetários, usamos BigDecimal construído de texto. new BigDecimal("0.1") representa o decimal escrito; construir a partir do double pode carregar sua aproximação.
BigDecimal é imutável: total.add(valor) devolve outro objeto. Se você ignorar o retorno, total não muda. compareTo compara o valor numérico; equals também considera escala, então 2.0 e 2.00 podem ser numericamente iguais sem serem iguais por equals.
A importação inteira, passo a passo
Crie cobrancas.txt ao lado do programa:
2026-10-01;10.20
2026-10-02;4.80
2026-02-30;9.00
Salve este código em Importacao.java:
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
public class Importacao {
public static void main(String[] args) {
BigDecimal total = new BigDecimal("0.00");
int validas = 0;
int invalidas = 0;
try (var leitor = Files.newBufferedReader(
Path.of("cobrancas.txt"), StandardCharsets.UTF_8)) {
String linha;
int numero = 0;
while ((linha = leitor.readLine()) != null) {
numero++;
try {
String[] campos = linha.split(";", -1);
if (campos.length != 2) {
throw new IllegalArgumentException("Esperadas duas colunas");
}
LocalDate.parse(campos[0].trim());
BigDecimal valor = new BigDecimal(campos[1].trim())
.setScale(2, RoundingMode.UNNECESSARY);
if (valor.signum() < 0) {
throw new IllegalArgumentException("Valor negativo");
}
total = total.add(valor);
validas++;
} catch (IllegalArgumentException | DateTimeParseException
| ArithmeticException erro) {
invalidas++;
System.out.println("Linha " + numero + " inválida");
}
}
System.out.println("Válidas: " + validas);
System.out.println("Inválidas: " + invalidas);
System.out.println("Total: " + total.toPlainString());
} catch (IOException erro) {
System.err.println("Importação incompleta: " + erro.getMessage());
}
}
}
A saída esperada informa linha 3 inválida, duas válidas, uma inválida e total 15.00. Não é um parser geral de CSV: o formato deste exercício tem duas colunas simples, sem campos escapados ou separadores dentro delas.
O -1 no split preserva uma coluna vazia no final, permitindo distingui-la de uma linha com formato diferente. setScale com UNNECESSARY rejeita valores que exigiriam arredondar para duas casas. signum() informa se o número é negativo, zero ou positivo. A data é validada, mas não armazenada neste relatório.
O catch interno cobre problemas conhecidos de conteúdo. O externo cobre abertura ou leitura. Se a leitura falha no meio, o programa não imprime o resumo como se o lote tivesse acabado normalmente. A mensagem ao usuário é curta; em uma aplicação real, mantenha detalhes técnicos em um canal de diagnóstico apropriado.
Arredondamento não cria uma regra de negócio sozinho
Dividir 10 por 3 exige decidir o destino do centavo restante. Arredondar cada parcela para 3,33 produz 9,99. Uma solução possível é duas parcelas de 3,33 e uma de 3,34; outra política precisa ser definida pelo produto. BigDecimal ajuda a expressar a decisão, mas não a escolhe.
Divisão inteira por zero lança exceção; divisão em ponto flutuante pode produzir infinito ou NaN. Não generalize um comportamento para todos os tipos numéricos.
Confira com uma variação
Acrescente 2026-10-03;2.345 ao arquivo. Ela deve ser rejeitada, pois a política não permite arredondamento implícito. Acrescente depois 2026-10-03;2.30: o total deve virar 17,30 com três linhas válidas. Se desapareceu o centavo, veja se usou double ou ignorou o retorno de add.
Em outra sessão, explique por que um arquivo ausente não equivale a um arquivo vazio e por que LocalDate não contém fuso. Depois altere a política para rejeitar qualquer lote com linha inválida: calcule primeiro, confirme depois. Essa separação prepara a conversa sobre testes e transações.
Referência usada: guia de exceções e recursos da biblioteca padrão.
Pratique com /aprender
Copie o comando e cole no chat para iniciar uma prática guiada:
/aprender Pratique exceções em Java 21 usando uma linha de arquivo com data e valor inválidos. Peça que eu preveja qual falha ocorre e como preservar o número da linha no diagnóstico; depois proponha um pequeno ajuste sem mostrar a resposta antes.
