Laboratório Java 08: Streams de E/S e Manipulação de Arquivos
Visão Geral
Este laboratório aborda conceitos fundamentais de entrada e saída em Java, incluindo manipulação de arquivos binários, arquivos de texto, streams bufferizados e serialização de objetos. Os objetivos incluem o domínio das classes de stream do pacote java.io e o entendimento de diferenças entre formatos binários e texto.
Seção 1: Streams de Bytes e Arquivos Binários
1. Leitura e Escrita de Arquivos Binários
Para manipulação de arquivos binários, utilizam-se as classes DataOutputStream e DataInputStream em conjunto com FileOutputStream e FileInputStream.
Escrita de Objetos em Arquivo Binário
O método a seguir demonstra como escrever objetos Student em um arquivo binário:
public static void gravarDadosEmBinario(String nomeArquivo) throws IOException {
List<Student> listaAlunos = new ArrayList<>();
listaAlunos.add(new Student(1, "Ana", 20, 85.5));
listaAlunos.add(new Student(2, "Bruno", 22, 90.0));
try (FileOutputStream fos = new FileOutputStream(nomeArquivo);
DataOutputStream dos = new DataOutputStream(fos)) {
for (Student aluno : listaAlunos) {
dos.writeInt(aluno.getId());
dos.writeUTF(aluno.getNome());
dos.writeInt(aluno.getIdade());
dos.writeDouble(aluno.getNota());
}
} catch (IOException e) {
throw new IOException("Erro ao gravar arquivo: " + e.getMessage(), e);
}
}
Leitura de Objetos de Arquivo Binário
O método para ler os dados:
public static List<Student> lerDadosDeBinario(String nomeArquivo) throws IOException {
List<Student> listaAlunos = new ArrayList<>();
try (FileInputStream fis = new FileInputStream(nomeArquivo);
DataInputStream dis = new DataInputStream(fis)) {
while (fis.available() > 0) {
int id = dis.readInt();
String nome = dis.readUTF();
int idade = dis.readInt();
double nota = dis.readDouble();
listaAlunos.add(new Student(id, nome, idade, nota));
}
} catch (IOException e) {
throw new IOException("Erro ao ler arquivo: " + e.getMessage(), e);
}
return listaAlunos;
}
Diferenças entre Arquivos Binários e de Texto
Arquivos binários armazenam dados no formato nativo do computador, enquanto arquivos de texto utilizam codificação de caracteres. Para analisar a estrutura interna de um arquivo binário, pode-se utilizar editores hexadecimais como wxMEdit, HxD ou Notepad++ com plugin Hex Editor.
Seção 2: Streams de Caracteres e Arquivos de Texto
2. Utilização de PrintWriter e BufferedReader
Leitura de Arquivo de Texto Formatado
Para leitura de arquivos de texto com suporte a diferentes codificações, utiliza-se BufferedReader combinado com InputStreamReader:
public static List<Student> lerArquivoTexto(String nomeArquivo, Charset charset)
throws IOException {
List<Student> alunos = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(nomeArquivo), charset))) {
String linha;
while ((linha = reader.readLine()) != null) {
if (linha.trim().isEmpty()) continue;
String[] partes = linha.split("\\s+");
try {
int id = Integer.parseInt(partes[0]);
String nome = partes[1];
int idade = Integer.parseInt(partes[2]);
double nota = Double.parseDouble(partes[3]);
alunos.add(new Student(id, nome, idade, nota));
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
System.err.println("Linha ignorada por erro de formato: " + linha);
}
}
}
return alunos;
}
Escrita de Arquivo de Texto
Utilização de PrintWriter para gravação:
public static void gravarArquivoTexto(List<Student> alunos, String nomeArquivo,
Charset charset) throws FileNotFoundException {
try (PrintWriter writer = new PrintWriter(
new OutputStreamWriter(new FileOutputStream(nomeArquivo), charset))) {
for (Student aluno : alunos) {
writer.println(aluno.getId() + "\t" + aluno.getNome() + "\t" +
aluno.getIdade() + "\t" + aluno.getNota());
}
}
}
Tratamento de Codificação de Caracteres
Problemas com caracteres chineses podem ocorrer quando FileReader utiliza a codificação padrão do sistema. Para resolver:
// Solução 1: Especificar codificação explicitamente
Charset utf8 = StandardCharsets.UTF_8;
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(arquivo), utf8));
// Solução 2: Converter arquivo para GBK e usar Charset.forName("GBK")
// Available em Java 11+: FileReader suporta Charset no construtor
Seção 3: Streams Bufferizados
3. Comparação de Performence: BufferedReader vs Scanner
O seguinte código testa a performance de diferentes métodos de leitura:
private static final int QUANTIDADE_REGISTROS = 10_000_000;
private static final long SEMENTE = 100;
private static final String NOME_ARQUIVO = "dados.txt";
@BeforeAll
static void gerarArquivo() throws FileNotFoundException {
Random random = new Random(SEMENTE);
try (PrintWriter pw = new PrintWriter(NOME_ARQUIVO)) {
for (int i = 0; i < QUANTIDADE_REGISTROS; i++) {
pw.println(random.nextInt(11));
}
}
}
@Test
void testarPerformanceScanner() {
List<Integer> numeros = new ArrayList<>();
try (Scanner sc = new Scanner(new File(NOME_ARQUIVO))) {
long inicio = System.nanoTime();
while (sc.hasNextInt()) {
numeros.add(sc.nextInt());
}
long duracao = System.nanoTime() - inicio;
System.out.printf("Scanner: %d ms%n", duracao / 1_000_000);
} catch (FileNotFoundException e) {
fail("Arquivo não encontrado");
}
}
@Test
void testarPerformanceBufferedReader() {
List<Integer> numeros = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(NOME_ARQUIVO))) {
long inicio = System.nanoTime();
String linha;
while ((linha = reader.readLine()) != null) {
numeros.add(Integer.parseInt(linha));
}
long duracao = System.nanoTime() - inicio;
System.out.printf("BufferedReader: %d ms%n", duracao / 1_000_000);
} catch (IOException e) {
fail("Erro ao ler arquivo");
}
}
Cálculo de Média
@Test
void calcularMedia() throws IOException {
List<Integer> valores = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(NOME_ARQUIVO))) {
String linha;
while ((linha = reader.readLine()) != null) {
valores.add(Integer.parseInt(linha));
}
}
double media = valores.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
System.out.format("Quantidade: %d | Média: %.5f%n", valores.size(), media);
}
Entendendo o Buffer de PrintWriter
O código a seguir demonstra o problema do buffer:
String arquivo = "d:/teste_buffer.dat";
int quantidade = 16383; // Menor que o tamanho padrão do buffer (8192 * 2)
PrintWriter writer = null;
try {
writer = new PrintWriter(arquivo);
for (int i = 0; i < quantidade; i++) {
writer.print(1); // Não adiciona nova linha
}
// Sem chamada a flush() ou close() - dados ficam no buffer
} finally {
// Importante: close() flusha o buffer automaticamente
if (writer != null) {
writer.close();
}
}
O problema ocorre porque o buffer padrão do PrintWriter é 8192 bytes. Quando quantidade é menor que 16384 (2x o buffer), os dados permanecem em memória até o buffer ser cheio ou o stream ser fechado.
Seção 4: Streams de Objetos
4. Serialização de Objetos
ObjectOutputStream e ObjectInputStream permitem serializar objetos inteiros:
public static void serializarAlunos(List<Student> alunos, String arquivo)
throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(arquivo))) {
oos.writeObject(alunos);
}
}
public static List<Student> deserializarAlunos(String arquivo)
throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(arquivo))) {
Object obj = ois.readObject();
if (obj instanceof List<?>) {
return (List<Student>) obj;
}
throw new ClassNotFoundException("Tipo inválido no arquivo");
}
}
Observação: A classe Student deve implementar a interface Serializable.
Seção 5: RandomAccessFile
6. Acesso Aleatório a Arquivos
RandomAccessFile permite leitura e escrita em posições específicas do arquivo:
public static void manipularComRandomAccess(String arquivo)
throws IOException {
// Escrita inicial
try (RandomAccessFile raf = new RandomAccessFile(arquivo, "rw")) {
raf.writeUTF("Primeiro registro");
raf.writeInt(100);
raf.writeUTF("Segundo registro");
raf.writeInt(200);
}
// Leitura a partir de posição específica
try (RandomAccessFile raf = new RandomAccessFile(arquivo, "r")) {
// Posicionar no segundo registro
raf.seek(raf.readUTF().length() + Integer.BYTES + 2); // +2 para flags UTF
String segundo = raf.readUTF();
int valor = raf.readInt();
System.out.println(segundo + ": " + valor);
}
}
Vantagens do RandomAccessFile:
- Acesso direto a qualquer posição do arquivo
- Ideal para estruturas de dados simples
- Suporta ambos os modos de leitura e escrita
Desvantagens:
- Não integra-se facilmente com outros streams
- Requer manipulação manual de ponteiros
Seção 6: Operações de Arquivos
7. Busca Recursiva de Arquivos
Implementação utilizando fila (BFS):
public static void buscarArquivo(Path diretorio, String nomeAlvo) throws IOException {
Queue<Path> fila = new LinkedList<>();
fila.add(diretorio);
boolean encontrado = false;
while (!fila.isEmpty()) {
Path atual = fila.poll();
if (Files.isRegularFile(atual) && atual.getFileName().toString().equals(nomeAlvo)) {
System.out.println("Encontrado: " + atual.toAbsolutePath());
encontrado = true;
}
if (Files.isDirectory(atual)) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(atual)) {
for (Path entry : stream) {
if (Files.isDirectory(entry) ||
entry.getFileName().toString().contains(nomeAlvo)) {
fila.add(entry);
}
}
}
}
}
if (!encontrado) {
System.out.println("Nenhum arquivo encontrado com o nome: " + nomeAlvo);
}
}
Utilizando Files.walkFileTree (Abordagem Alternativa)
public static void buscarComWalkFileTree(Path inicio, String nomeAlvo)
throws IOException {
Files.walkFileTree(inicio, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path arquivo, BasicFileAttributes attrs)
throws IOException {
if (arquivo.getFileName().toString().equals(nomeAlvo)) {
System.out.println("Encontrado: " + arquivo.toAbsolutePath());
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
return FileVisitResult.CONTINUE;
}
});
}
Interface Gráfica para Seleção de Diretório:
JFileChooser seletor = new JFileChooser();
seletor.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
int resultado = seletor.showOpenDialog(null);
if (resultado == JFileChooser.APPROVE_OPTION) {
Path diretorio = seletor.getSelectedFile().toPath();
buscarArquivo(diretorio, "arquivo_busca.txt");
}