Arquivos
Caminhos de Arquivo
Separadores
- Windows:
\, por exemploC:\Usuarios\Padrao - Unix:
/, por exemplo/home/padraoEm Java, o\é um caractere de escape, por exemplo\nrepresenta 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
\\,/eFile.separatorpara 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
- O caminho relativo é relativo ao diretório de trabalho atual, e toda a dificuldade concentra-se em como determinar esse caminho
- 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 atualgeralmente é o diretório onde o programa está. Em Java, se usarmos uma IDE como IntelliJ IDEA, odiretó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, odiretório de trabalho atualserá 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étodopublic String readLine(), usado para ler uma linha de caracteres. Após ler todo o arquivo, retornanullBufferedWriter->BufferedWriter(Writer writer), novo métodopublic 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)