Laboratório Java 08: Streams de E/S e Manipulação de Arquivos

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");
}

Publicado em 7-26 03:49