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 comPath,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:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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-8para 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ção | Método | Observação |
|---|---|---|
| Criar diretórios | Files.createDirectories(path) | Cria a árvore toda se necessário |
| Verificar existência | Files.exists(path) | Útil antes de ler |
| Copiar | Files.copy(origem, destino) | Há opções de overwrite com StandardCopyOption |
| Mover/renomear | Files.move(origem, destino) | Também aceita opções |
| Apagar | Files.delete(path) | Lança exceção se não existir |
| Apagar se existir | Files.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.50Regras 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 osplitparasplit(";"). - 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 dedouble. - Se você quiser gerar também um CSV de saída, basta montar linhas com separador e escrever com
Files.writeStringouFiles.write.