Arquivos e Fluxos de Entrada/Saída em Java

Arquivos

Caminhos de Arquivo

Separadores
  • Windows: \, por exemplo C:\Usuarios\Padrao
  • Unix: /, por exemplo /home/padraoEm Java, o \ é um caractere de escape, por exemplo \n representa uma nova linha. Portanto, os caminhos de arquivo precisam usar duas barras invertidas \\ para representar um único separador. A JVM adapta automaticamente ambos os símbolos, permitindo usar / ou \\ como separadores de caminho em qualquer sistema operacional.

Embora Java suporte esses dois separadores, em certos cenários, como:

  • Ao interagir com programas não-Java que podem não ter a mesma capacidade de adaptação, por exemplo, programas em C.
  • Ao manipular caminhos como strings, como ao cortar ou concatená-los, podem surgir dificuldades:

Portanto, Java fornece a constante File.separator, que representa o separador de caminho do sistema operacional atual. Por exemplo: "C:"+ File.separator + "Usuarios" + File.separator + "Padrao"

Resumo:

Em Java, pode-se usar \\, / e File.separator para representar separadores de caminho.

Caminhos Absolutos e Relativos
  • Caminho absoluto: D:\\codigo\\JavaSE\\FundamentosJava\\src\\DemonstracaoArquivo.javaCaminho completo do arquivo começando do diretório raiz
  • Caminho relativo: É um problema muito complexo, com tantas nuances que muitas vezes é difícil acertar na primeira tentativa
  1. O caminho relativo é relativo ao diretório de trabalho atual, e toda a dificuldade concentra-se em como determinar esse caminho
  2. Sabemos que Python não tem uma estrutura de projeto tão rígida como Java, então ao executar um programa Python, o diretório de trabalho atual geralmente é o diretório onde o programa está. Em Java, se usarmos uma IDE como IntelliJ IDEA, o diretório de trabalho atual é o diretório do projeto. Se executarmos o programa Java diretamente no terminal no diretório do arquivo de código-fonte, o diretório de trabalho atual será o diretório do prompt de comando. Abaixo estão exemplos detalhados

A estrutura do projeto é a seguinte:

public class DemonstracaoArquivo {
 public static void main(String[] args) {
   // Caminho relativo: ./Comentario.java
     File arquivo = new File("./Comentario.java");
     if (arquivo.exists()){
         System.out.println("O arquivo existe");
     }
     System.out.println(arquivo.length());
     System.out.println(System.getProperty("user.dir"));
 }
}


Executando o código acima no IntelliJ IDEA:

0
D:\codigo\JavaSE


Isso significa que no IntelliJ IDEA, o diretório "D:\codigo\JavaSE\FundamentosJava\src" onde DemonstracaoArquivo está localizado não é o diretório de trabalho atual, mas sim "D:\codigo\JavaSE".

Executando no terminal no diretório onde DemonstracaoArquivo está localizado "D:\codigo\JavaSE\FundamentosJava\src", digitando o comando de compilação javac DemonstracaoArquivo.java e o comando de execução java DemonstracaoArquivo, a saída é:

 PS D:\codigo\JavaSE\FundamentosJava\src> javac DemonstracaoArquivo.java
 PS D:\codigo\JavaSE\FundamentosJava\src> java DemonstracaoArquivo
 O arquivo existe
 968
 D:\codigo\JavaSE\FundamentosJava\src


Pode-se observar que o arquivo de bytecode compilado manuamlente "DemonstracaoArquivo.class" também aparece no diretório de trabalho (D:\codigo\JavaSE\FundamentosJava\src), enquanto o IntelliJ IDEA, para uma estrutura de projeto mais clara, coloca todos os arquivos de bytecode no diretório out, que também é criado sob o diretório de trabalho atual (D:\codigo\JavaSE).

Resumo:

No desenvolvimento diário, geralmente usamos o IntelliJ IDEA como ferramenta de desenvolvimento, e o diretório de trablaho para caminhos relativos é o diretório raiz de todo o projeto (pode-se observar a posição da pasta out).

Criação de Objetos File

File arquivo = new File(String caminho); onde caminho é o caminho do arquivo, e esse arquivo pode não existir. Se não existir, o método arquivo.exists() retornará false. Este objeto File pode representar tanto um arquivo quanto um diretório.

Métodos Comuns do File

Nome do Método Descrição
public boolean exists() Verifica se o arquivo existe
public long length() Obtém o tamanho do arquivo (retorna o número de bytes)
public boolean isDirectory() Verifica se é um diretório
public boolean isFile() Verifica se é um arquivo
public String getName() Obtém o nome do arquivo (incluindo a extensão, ou seja, o tipo de arquivo)
public String getPath() Obtém o caminho do arquivo (o caminho passado para o construtor)
public String getAbsolutePath() Obtém o caminho absoluto do arquivo
public boolean createNewFile() Cria um novo arquivo (precisa tratar exceções, garantindo que o caminho exista)
public boolean mkdir() Cria um diretório (pode criar apenas uma pasta de nível único)
public boolean mkdirs() Cria um diretório (pode criar múltiplas pastas)
public boolean delete() Exclui um arquivo ou diretório vazio (não pode excluir diretórios não vazios, e os arquivos excluídos não vão para a lixeira, sendo difíceis de recuperar)

Percorrimento de Arquivos

public File[] listFiles(), retorna um array File, permitindo acessar informações específicas dos arquivos. No entanto, note que este método só pode percorrer arquivos e diretórios de um nível.

Fluxos de Entrada/Saída

Conjuntos de Caracteres

  • Conjunto de caracteres ASCII: apenas inglês, números e símbolos, ocupando um byte
  • Conjunto de caracteres GBK: caracteres chineses ocupam dois bytes, inglês e números ocupam um byte. O primeiro bit do primeiro byte de caracteres chineses é 1, enquanto o de inglês e números é 0
  • UTF-8: caracteres chineses ocupam 3 bytes, inglês e números ocupam um byte 4 faixas de comprimento:
  • Um byte: 0xxxxxxx (código ASCII)
  • Dois bytes: 110xxxxx 10xxxxxx
  • Três bytes: 1110xxxx 10xxxxxx 10xxxxxx
  • Quatro bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

No conjunto de caracteres UTF-8, os bytes contínuos começam com 10. Embora pareça desperdiçar 2 bits de espaço de armazenamento, na verdade utiliza esse pequeno custo para obter uma capacidade de sincronização enorme.

Por exemplo, durante a transmissão de informações, se alguns bytes forem perdidos, os bytes contínuos que não começam com 10 dificultam determinar se um byte é o primeiro byte de um caractere ou um byte contínuo durante o processo de decodificação, causando erros de decodificação em todo o texto subsequente. Se os bytes contínuos começam com 10, é muito mais fácil detectar sequências inválidas durante o processo de decodificação.

Métodos de Codificação e Decodificação de String

Método Descrição
bytes[] getBytes() Codifica a string em um array de bytes (usando o conjunto de caracteres padrão da plataforma)
bytes[] getBytes(String charset) Codifica a string em um array de bytes, especificando o conjunto de caracteres
String(byte[] bytes) Decodifica um array de bytes em uma string (usando o conjunto de caracterse padrão da plataforma)
String(byte[] bytes, String charset) Decodifica um array de bytes em uma string, especificando o conjunto de caracteres
String(byte[] bytes, int offset, int length) Decodifica um array de bytes em uma string, especificando o intervalo de decodificação como [offset, offset+length). Veja mais em Fluxos de E/S abaixo
public static void main(String[] args) {
        String dados = "Céu";
        byte[] bytes = dados.getBytes();
        System.out.println(Arrays.toString(bytes));

        String str = new String(bytes);
        System.out.println(str);
    }


Resultado da execução:

[-26, -104, -118, -27, -92, -87]
Céu


Classificação dos Fluxos de E/S

Classificação pela direção do fluxo de dados:

  • Fluxo de entrada
  • Fluxo de saída

Classificação pela unidade mínima de dados do fluxo:

  • Fluxo de bytes: adequado para operar todos os tipos de arquivos, como vídeos, imagens, áudio, etc.
  • Fluxo de caracteres: adequado para operar arquivos de texto, como arquivos de texto, XML, etc.

Combinando os dois critérios, obtemos os 4 tipos de fluxos comuns:

Tipo de Fluxo Classe Abstrata Classe de Implementação
Fluxo de entrada de bytes InputStream FileInputStream
Fluxo de saída de bytes OutputStream FileOutputStream
Fluxo de entrada de caracteres Reader FileReader
Fluxo de saída de caracteres Writer FileWriter

Fluxo de Entrada de Bytes

Métodos Construtores

FileInputStream(String nomeArquivo) (comum), nome é o caminho do arquivo de destino

FileInputStream(File arquivo), arquivo é o objeto de arquivo de destino, que na maioria das vezes é desnecessário, sendo menos comum que o primeiro.

public FileInputStream(String nome) throws FileNotFoundException {
        this(nome != null ? new File(nome) : null);
    }


Pode-se ver no código-fonte de FileInputStream(String nomeArquivo) que um objeto File é criado com base no caminho do arquivo.

Métodos Comuns
  • int read(): Lê um byte de cada vez, expande o byte lido para um inteiro sem sinal (0~255) e retorna.

Se chegar ao final do arquivo, retorna -1. Este método tem baixo desempenho, pois requer chamadas frequentes ao sistema.``` public static void main(String[] args) throws IOException { // O conteúdo do arquivo xy.txt é "Céu66" InputStream is = new FileInputStream("FundamentosJava/src/xy.txt"); int dado; while ((dado = is.read()) != -1) { System.out.println((byte) dado); } // O fluxo deve ser fechado após o uso is.close(); }


Resultado da execução:```
-26
-104
-118
-27
-92
-87
54
54
-1


  • int read(byte[] b): Lê múltiplos bytes de uma vez, retornando o número real de bytes lidos.``` public static void main(String[] args) throws IOException { InputStream is = new FileInputStream("FundamentosJava/src/xy.txt"); // Lê três bytes de uma vez byte[] bytes = new byte[3]; // len é o número real de bytes lidos int len = is.read(bytes); String data = new String(bytes); System.out.println(len); System.out.println(data); }

Resultado da execução:```
3
Cé


Quando realizamos a n-ésima leitura, os dados em byte[] serão sobrescritos sequencialmente:``` public static void main(String[] args) throws IOException { InputStream is = new FileInputStream("FundamentosJava/src/xy.txt"); // Lê três bytes de uma vez byte[] bytes = new byte[3]; // len é o número real de bytes lidos int len = is.read(bytes); String data = new String(bytes); System.out.println("Número de bytes lidos na primeira vez: " + len); System.out.println(data); // Múltiplas leituras int len2 = is.read(bytes); String data2 = new String(bytes); System.out.println("Número de bytes lidos na segunda vez: " + len2); System.out.println(data2); int len3 = is.read(bytes); String data3 = new String(bytes); System.out.println("Número de bytes lidos na terceira vez: " + len3); System.out.println(data3); }


Resultado da execução:```
Número de bytes lidos na primeira vez: 3
Cé
Número de bytes lidos na segunda vez: 3
u
Número de bytes lidos na terceira vez: 2
66�


Curiosamente, na terceira leitura ocorre um erro de codificação. Isso ocorre porque no UTF-8 "u" ocupa 3 bytes, enquanto "66" ocupa 2 bytes. Durante o processo de sobrescrita, os dois primeiros bytes de "u" são sobrescritos por "66", mas o último byte ainda é decodificado, resultando em um erro de codificação. Java fornece convenientemente um método sobrecarregado para o construtor de String String(byte[] bytes, int offset, int length), que permite especificar o intervalo de bytes a ser decodificado [offset, offset+length). Modificando o código:``` String data3 = new String(bytes, 0, len3); System.out.println("Número de bytes lidos na terceira vez: " + len3); System.out.println(data3);


Resultado da execução:```
Número de bytes lidos na terceira vez: 2
66


Este método ainda tem uma deficiência: se os três bytes de um caractere chinês forem truncados, por exemplo, se o conteúdo do texto for "66Céu", então os 3 bytes correspondentes a "Cé" não aparecerão simultaneamente em byte[3], causando erros de codificação. A solução também é simples e direta: definir o comprimento do byte[] como o comprimento do texto:``` File f = new File("FundamentosJava/src/xy.txt"); InputStream is = new FileInputStream(f); // O tamanho do disco é muito maior que o tamanho da memória, por exemplo, um arquivo de jogo 3A pode ter dezenas de GB, enquanto a memória geralmente tem apenas alguns GB ou mais // Portanto, o método length() do objeto File retorna um tipo long, enquanto o comprimento do array byte é int (máximo 21_4748_3647 bytes, ou seja, 2^31/2^30=2GB) // No desenvolvimento diário, desde que não processe arquivos maiores que 2GB, podemos fazer um cast de tipo direto long tamanho = f.length(); byte[] bytes = new byte[(int)tamanho]; int len = is.read(bytes); String data = new String(bytes, 0, len); System.out.println("Número de bytes lidos na primeira vez: " + len); System.out.println(data);


Java ainda fornece convenientemente um método semelhante `readAllBytes()` (clássico doge branco), que tem algumas verificações a mais em comparação com nossa conversão de tipo forçado, e lançará exceções como estouro de memória.```
byte[] bytes = is.readAllBytes();
String data = new String(bytes);


Usando o método oficial readAllBytes(), embora possa lançar exceções, ainda não consegue lidar com arquivos com conteúdo excessivamente grande. Portanto, fluxos de bytes são mais adequados para transferência de dados, enquanto a leitura e escrita de conteúdo de texto é melhor deixada para os fluxos de caracteres que serão discutidos a seguir.

Fluxo de Saída de Bytes

Método Função
FileOutputStream(String nomeArquivo) Método construtor, cria um fluxo de saída baseado no caminho do arquivo; se o arquivo não existir, criará um novo arquivo
FileOutputStream(String nomeArquivo, boolean append) Método construtor, por padrão é escrita sobrescrita; se append for true, muda para escrita anexada
void write(int b) Escreve um byte
void write(byte[] b) Escreve um array de bytes
Cópia de Arquivo
String caminhoArquivo1 = "C:\\Usuarios\\28364\\Desktop\\cantora.webp";
String caminhoArquivo2 = "FundamentosJava\\src\\cantora_copia.webp";
// Cria fluxos de entrada e saída de bytes
InputStream is = new FileInputStream(caminhoArquivo1);
OutputStream os = new FileOutputStream(caminhoArquivo2);
// Lê o conteúdo do arquivo, definindo um buffer de memória de 1KB, com melhor desempenho em comparação com o método readAllBytes()
byte[] bytes = new byte[1024];
int len = 0;
while((len = is.read(bytes)) != -1){
    os.write(bytes, 0, len);
}
// Libera recursos
os.close();
is.close();
System.out.println("Cópia do arquivo concluída");


Gerenciamento de Recursos

  • try-catch-finally, embora muito verboso, é uma maneira profissional de liberar recursos``` InputStream is = null; OutputStream os = null; try { String caminhoArquivo1 = "C:\Usuarios\28364\Desktop\cantora.webp"; String caminhoArquivo2 = "FundamentosJava\src\cantora_copia.webp"; // Cria fluxos de entrada e saída de bytes is = new FileInputStream(caminhoArquivo1); os = new FileOutputStream(caminhoArquivo2); // Lê o conteúdo do arquivo byte[] bytes = new byte[1024]; int len = 0; while((len = is.read(bytes)) != -1){ os.write(bytes, 0, len); } System.out.println("Cópia do arquivo concluída"); } catch (IOException e) { e.printStackTrace(); } finally { // Libera recursos try{ if (os != null) os.close(); } catch (IOException e) { e.printStackTrace(); } try{ if (is != null) is.close(); } catch (IOException e) { e.printStackTrace(); } }
- `try-with-resources (novo no JDK7)`, resolve o problema de verbosidade em `try-catch-finally`. Libera automaticamente os recursos em `try()`, e em `try()` só podem ser definidos objetos de recurso; se definirmos algo como `int a=0;`, ocorrerá um erro.
Em Java, pode-se verificar se um objeto é um recurso verificando se ele implementa a interface `AutoCloseable`.
O método de liberação de recursos mencionado anteriormente implementa essa interface```
try (InputStream is = new FileInputStream(caminhoArquivo1);
     OutputStream os = new FileOutputStream(caminhoArquivo2)) {
    // Lê o conteúdo do arquivo
    byte[] bytes = new byte[1024];
    int len = 0;
    while((len = is.read(bytes)) != -1){
        os.write(bytes, 0, len);
    }
    System.out.println("Cópia do arquivo concluída");
} catch (IOException e) {
    e.printStackTrace();
}


Fluxos de Entrada e Saída de Caracteres

Método Função
FileReader(String nomeArquivo) Método construtor, cria um fluxo de entrada de caracteres baseado no caminho do arquivo
FileWriter(String nomeArquivo) Método construtor, cria um fluxo de saída de caracteres baseado no caminho do arquivo
int read() Lê o código int de um caractere
int read(char[] b) Lê um array de caracteres
void write(int c) Escreve um caractere
void write(char[] b) Escreve um array de caracteres

Após escrever dados com um fluxo de saída de caracteres, é necessário atualizar (flush()) ou fechar o fluxo (close()), caso contrário, os dados não serão gravados no arquivo.

Outros Fluxos

Fluxos Bufferizados

Possuem um buffer de 8k, encapsulam fluxos de E/S originais para melhorar o desempenho

Fluxos Bufferizados de Entrada e Saída de Bytes
  • BufferInputStream -> BufferedInputStream(InputStream is)
  • BufferOutputStream -> BufferedOutputStream(OutputStream os)
Fluxos Bufferizados de Entrada e Saída de Caracteres
  • BufferedReader -> BufferedReader(Reader reader), novo método public String readLine(), usado para ler uma linha de caracteres. Após ler todo o arquivo, retorna null
  • BufferedWriter -> BufferedWriter(Writer writer), novo método public void newLine(), usado para quebrar linha
Fluxos de Conversão
  • Fluxo de entrada de caracteres InputStreamReader(InputStream is, String charset)
  • Fluxo de saída de caracteres OutputStreamWriter(OutputStream os, String charset)
Fluxos de Impressão
  • Fluxo de impressão de bytes PrintStream
  • Fluxo de impressão de caracteres PrintWriter
Fluxos de Dados
  • Fluxo de entrada de dados DataInputStream(InputStream is)
  • Fluxo de saída de dados DataOutputStream(OutputStream os)
Fluxos de Serialização
  • Fluxo de entrada de serialização ObjectInputStream(InputStream is)
  • Fluxo de saída de serialização ObjectOutputStream(OutputStream os)

Tags: java arquivos Fluxos de E/S FileInputStream FileOutputStream

Publicado em 6-15 19:10 por Thomas