Java Essencial: Entrada e saída básica (console e arquivos) com java.io e java.nio

Capítulo 14

Tempo estimado de leitura: 9 minutos

+ Exercício

Entrada e saída (I/O) em Java: visão geral

Entrada e saída (I/O) é o conjunto de técnicas para ler dados (entrada) e produzir dados (saída). No dia a dia, isso normalmente significa: (1) ler do console (teclado) e escrever no console; (2) ler e escrever arquivos. Em Java, você encontrará duas “famílias” principais:

  • java.io: APIs clássicas baseadas em streams e readers/writers (ex.: BufferedReader, FileReader, FileWriter).
  • java.nio (NIO.2): APIs modernas com Path, Files, melhor suporte a charset e operações utilitárias (ex.: Files.readString, Files.writeString, Files.lines).

Uma boa prática é preferir java.nio.file para operações comuns com arquivos texto, e usar java.io quando você precisa de controle mais fino sobre streams ou compatibilidade com APIs antigas.

Leitura do console com Scanner

Scanner é prático para ler tokens (palavras, números) do console. Ele usa delimitadores (por padrão, espaços e quebras de linha). Isso é ótimo para entradas “separadas por espaços”, mas exige cuidado quando você mistura leitura de números com leitura de linha inteira.

Exemplo: lendo texto e números

import java.util.Locale;import java.util.Scanner;public class ConsoleComScanner {    public static void main(String[] args) {        // Locale influencia parsing de números (ponto vs vírgula)        Locale.setDefault(Locale.US);        try (Scanner sc = new Scanner(System.in)) {            System.out.print("Nome: ");            String nome = sc.nextLine();            System.out.print("Idade: ");            int idade = sc.nextInt();            System.out.print("Altura (ex.: 1.75): ");            double altura = sc.nextDouble();            System.out.println("Resumo: " + nome + ", " + idade + " anos, " + altura + "m");        }    }}

Cuidados com locale (ponto e vírgula em decimais)

O parsing de double no Scanner depende do Locale. Em muitos ambientes pt-BR, o usuário pode digitar 1,75, mas o Scanner pode esperar 1.75. Você tem opções:

  • Definir um locale conhecido (ex.: Locale.US) e orientar o usuário a usar ponto.
  • Usar sc.useLocale(new Locale("pt", "BR")) e aceitar vírgula.
  • Ler como texto (nextLine()) e normalizar (replace(',', '.')) antes de converter.

Cuidados com quebras de linha ao misturar nextInt/nextDouble com nextLine

Quando você usa nextInt() ou nextDouble(), o Scanner não consome a quebra de linha final. Se você chamar nextLine() logo depois, pode receber uma string vazia. Uma solução é consumir a linha pendente:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

System.out.print("Idade: ");int idade = sc.nextInt();sc.nextLine(); // consome a quebra de linha pendenteSystem.out.print("Cidade: ");String cidade = sc.nextLine();

Leitura do console com BufferedReader (controle por linha)

BufferedReader é uma alternativa excelente quando você quer ler linha por linha, sem a lógica de tokens do Scanner. Ele é rápido e previsível para entradas textuais.

import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.nio.charset.StandardCharsets;public class ConsoleComBufferedReader {    public static void main(String[] args) throws IOException {        // Especificar charset ajuda em ambientes com acentuação        try (BufferedReader br = new BufferedReader(                new InputStreamReader(System.in, StandardCharsets.UTF_8))) {            System.out.print("Digite uma frase: ");            String frase = br.readLine();            System.out.println("Você digitou: " + frase);        }    }}

Como readLine() retorna a linha sem o terminador (\n ou \r\n), você não precisa lidar com “sobras” de quebra de linha como no Scanner.

Arquivos texto: Path, charset e boas práticas

Ao trabalhar com arquivos texto, três pontos são fundamentais:

  • Path: representa o caminho do arquivo de forma independente de plataforma (Windows/Linux/macOS).
  • Charset: define como bytes viram texto e vice-versa. Prefira UTF-8 para evitar problemas com acentos.
  • try-with-resources: garante fechamento de recursos (streams/readers/writers) mesmo em caso de erro.

Criando Paths

import java.nio.file.Path;public class PathsExemplo {    public static void main(String[] args) {        Path relativo = Path.of("dados", "entrada.txt");        Path absoluto = Path.of("/tmp", "saida.txt"); // exemplo em Unix        System.out.println(relativo);        System.out.println(absoluto);    }}

Use Path.of(...) (Java 11+) para montar caminhos com segurança, evitando concatenar strings com "/" ou "\\".

Leitura e escrita simples com NIO: Files.readString e Files.writeString

Para arquivos pequenos/médios, Files.readString e Files.writeString são diretos e legíveis.

Escrever um arquivo texto em UTF-8

import java.io.IOException;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Path;public class EscreverArquivo {    public static void main(String[] args) throws IOException {        Path out = Path.of("saida", "mensagem.txt");        Files.createDirectories(out.getParent());        String conteudo = "Olá, arquivo!\nLinha 2 com acentuação: ação, café.";        Files.writeString(out, conteudo, StandardCharsets.UTF_8);    }}

Ler um arquivo texto em UTF-8

import java.io.IOException;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Path;public class LerArquivo {    public static void main(String[] args) throws IOException {        Path in = Path.of("saida", "mensagem.txt");        String texto = Files.readString(in, StandardCharsets.UTF_8);        System.out.println(texto);    }}

Observação: se você não informar o charset, alguns métodos usam o charset padrão do sistema, o que pode gerar inconsistência entre máquinas. Para material didático e projetos reais, explicitar UTF-8 evita surpresas.

Processamento por linhas com Files.lines (streaming)

Quando o arquivo pode ser grande, ler tudo de uma vez pode ser desnecessário. Files.lines fornece um Stream<String> com as linhas do arquivo, permitindo processar em fluxo.

import java.io.IOException;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Path;public class ProcessarLinhas {    public static void main(String[] args) throws IOException {        Path in = Path.of("dados", "log.txt");        try (var lines = Files.lines(in, StandardCharsets.UTF_8)) {            long erros = lines.filter(l -> l.contains("ERROR")).count();            System.out.println("Total de linhas com ERROR: " + erros);        }    }}

O Stream precisa ser fechado; por isso, use try-with-resources.

Operações comuns com NIO (Files)

OperaçãoMétodoObservação
Criar diretóriosFiles.createDirectories(path)Cria a árvore toda se necessário
Verificar existênciaFiles.exists(path)Útil antes de ler
CopiarFiles.copy(origem, destino)Há opções de overwrite com StandardCopyOption
Mover/renomearFiles.move(origem, destino)Também aceita opções
ApagarFiles.delete(path)Lança exceção se não existir
Apagar se existirFiles.deleteIfExists(path)Mais seguro em scripts

Exercício completo: ler CSV, processar e gerar relatório

Objetivo: ler um arquivo CSV simples de vendas, calcular totais e gerar um relatório em outro arquivo.

Formato do CSV de entrada

Crie o arquivo dados/vendas.csv em UTF-8 com o conteúdo abaixo (primeira linha é cabeçalho):

produto,quantidade,preco_unitarioCaderno,10,12.50Caneta,50,2.00Mochila,3,120.00Caderno,5,12.50

Regras de processamento

  • Ignorar a linha de cabeçalho.
  • Para cada linha: subtotal = quantidade * preco_unitario.
  • Somar o faturamento total.
  • Agrupar por produto: somar quantidade total e faturamento por produto.
  • Gerar um relatório em saida/relatorio.txt (UTF-8) com um resumo geral e um detalhamento por produto.

Passo a passo prático

1) Definir paths de entrada e saída

Use Path.of e garanta que o diretório de saída exista.

2) Ler linhas com Files.lines

Processe em streaming para não depender do tamanho do arquivo.

3) Fazer parsing do CSV

Como o CSV é simples (sem aspas e sem vírgulas dentro de campos), podemos usar split(","). Em CSVs reais, prefira uma biblioteca própria de CSV.

4) Acumular resultados em mapas

Use Map para agrupar por produto e acumular quantidade e faturamento.

5) Montar o texto do relatório e escrever com Files.writeString

Gere uma string final e grave em UTF-8.

Código completo do exercício

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.util.Comparator;import java.util.HashMap;import java.util.Locale;import java.util.Map;public class RelatorioVendasCsv {    static class Acumulado {        long quantidade;        BigDecimal faturamento = BigDecimal.ZERO;        void adicionar(long qtd, BigDecimal subtotal) {            this.quantidade += qtd;            this.faturamento = this.faturamento.add(subtotal);        }    }    public static void main(String[] args) throws IOException {        // Locale aqui é apenas para formatação final; parsing será controlado manualmente        Locale.setDefault(Locale.US);        Path entrada = Path.of("dados", "vendas.csv");        Path saida = Path.of("saida", "relatorio.txt");        if (!Files.exists(entrada)) {            throw new IllegalStateException("Arquivo de entrada não encontrado: " + entrada.toAbsolutePath());        }        Files.createDirectories(saida.getParent());        Map<String, Acumulado> porProduto = new HashMap<>();        BigDecimal faturamentoTotal = BigDecimal.ZERO;        long linhasProcessadas = 0;        try (var lines = Files.lines(entrada, StandardCharsets.UTF_8)) {            var it = lines.iterator();            if (it.hasNext()) it.next(); // pula cabeçalho            while (it.hasNext()) {                String linha = it.next().trim();                if (linha.isEmpty()) continue;                String[] partes = linha.split(",");                if (partes.length != 3) {                    throw new IllegalArgumentException("Linha inválida (esperado 3 colunas): " + linha);                }                String produto = partes[0].trim();                long quantidade = Long.parseLong(partes[1].trim());                // Garante ponto como separador decimal no arquivo                String precoStr = partes[2].trim().replace(',', '.');                BigDecimal precoUnit = new BigDecimal(precoStr);                BigDecimal subtotal = precoUnit.multiply(BigDecimal.valueOf(quantidade));                faturamentoTotal = faturamentoTotal.add(subtotal);                porProduto.computeIfAbsent(produto, k -> new Acumulado())                         .adicionar(quantidade, subtotal);                linhasProcessadas++;            }        }        StringBuilder rel = new StringBuilder();        rel.append("RELATÓRIO DE VENDAS\n");        rel.append("Arquivo: ").append(entrada.toAbsolutePath()).append("\n");        rel.append("Linhas processadas: ").append(linhasProcessadas).append("\n");        rel.append("Faturamento total: R$ ")           .append(faturamentoTotal.setScale(2, RoundingMode.HALF_UP))           .append("\n\n");        rel.append("DETALHAMENTO POR PRODUTO\n");        rel.append("Produto | Quantidade | Faturamento\n");        rel.append("----------------------------------\n");        porProduto.entrySet().stream()                 .sorted(Comparator.comparing(e -> e.getKey().toLowerCase()))                 .forEach(e -> {                     String produto = e.getKey();                     Acumulado a = e.getValue();                     rel.append(produto).append(" | ")                        .append(a.quantidade).append(" | R$ ")                        .append(a.faturamento.setScale(2, RoundingMode.HALF_UP))                        .append("\n");                 });        Files.writeString(saida, rel.toString(), StandardCharsets.UTF_8);        System.out.println("Relatório gerado em: " + saida.toAbsolutePath());    }}

Verificações e variações úteis

  • Se o CSV vier com ; em vez de ,, ajuste o split para split(";").
  • Se houver linhas com espaços extras, trim() já ajuda.
  • Para evitar problemas de precisão com dinheiro, use BigDecimal (como no exemplo) em vez de double.
  • Se você quiser gerar também um CSV de saída, basta montar linhas com separador e escrever com Files.writeString ou Files.write.

Agora responda o exercício sobre o conteúdo:

Ao processar um arquivo de texto potencialmente grande em Java, qual abordagem é mais adequada para ler e tratar as linhas em streaming, garantindo o fechamento correto do recurso?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Files.lines fornece um Stream<String> que permite processar as linhas em fluxo, sem ler tudo de uma vez. Como o stream precisa ser fechado, o try-with-resources garante o fechamento adequado.

Próximo capitúlo

Java Essencial: Tratamento de exceções e criação de erros significativos

Arrow Right Icon
Capa do Ebook gratuito Java Essencial: Fundamentos da Linguagem e do Ecossistema (JDK, IDE, Maven)
78%

Java Essencial: Fundamentos da Linguagem e do Ecossistema (JDK, IDE, Maven)

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.