Por que o Java IO é tão ruim?
A API IO (Input & Output) é uma API Java que facilita o trabalho dos desenvolvedores com streams. Digamos que recebemos alguns dados (por exemplo, nome, nome do meio, sobrenome) e precisamos gravá-los em um arquivo — chegou a hora de usar java.io .
Estrutura da biblioteca java.io
Mas o Java IO tem suas desvantagens, então vamos falar sobre cada um deles:
- Bloqueio de acesso para entrada/saída. O problema é que, quando um desenvolvedor tenta ler ou gravar algo em um arquivo usando Java IO , ele bloqueia o arquivo e bloqueia o acesso a ele até que o trabalho seja concluído.
- Sem suporte para sistemas de arquivos virtuais.
- Não há suporte para links.
- Muitas e muitas exceções verificadas.
Trabalhar com arquivos sempre envolve trabalhar com exceções: por exemplo, tentar criar um novo arquivo que já existe lançará um IOException . Nesse caso, o aplicativo deve continuar em execução e o usuário deve ser notificado porque o arquivo não pôde ser criado.
try {
File.createTempFile("prefix", "");
} catch (IOException e) {
// Handle the IOException
}
/**
* Creates an empty file in the default temporary-file directory
* any exceptions will be ignored. This is typically used in finally blocks.
* @param prefix
* @param suffix
* @throws IOException - If a file could not be created
*/
public static File createTempFile(String prefix, String suffix)
throws IOException {
...
}
Aqui vemos que o método createTempFile lança uma IOException quando o arquivo não pode ser criado. Essa exceção deve ser tratada adequadamente. Se tentarmos chamar esse método fora de um bloco try-catch , o compilador gerará um erro e sugerirá duas opções para corrigi-lo: agrupar o método em um bloco try-catch ou fazer com que o método que chama File.createTempFile lance uma IOException ( para que possa ser tratado em um nível superior).
Chegando ao Java NIO e como ele se compara ao Java IO
Java NIO , ou Java Non-Blocking I/O (ou às vezes Java New I/O) é projetado para operações de E/S de alto desempenho.
Vamos comparar os métodos Java IO e aqueles que os substituem.
Primeiro, vamos falar sobre como trabalhar com Java IO :
Classe InputStream
try(FileInputStream fin = new FileInputStream("C:/codegym/file.txt")){
System.out.printf("File size: %d bytes \n", fin.available());
int i=-1;
while((i=fin.read())!=-1) {
System.out.print((char)i);
}
} catch(IOException ex) {
System.out.println(ex.getMessage());
}
A classe FileInputStream é para ler dados de um arquivo. Ele herda a classe InputStream e, portanto, implementa todos os seus métodos. Se o arquivo não puder ser aberto, uma FileNotFoundException será lançada.
Classe OutputStream
String text = "Hello world!"; // String to write
try(FileOutputStream fos = new FileOutputStream("C:/codegym/file.txt")){
// Convert our string into bytes
byte[] buffer = text.getBytes();
fos.write(buffer, 0, buffer.length);
System.out.println("The file has been written");
} catch(IOException ex) {
System.out.println(ex.getMessage());
}
A classe FileOutputStream para gravar bytes em um arquivo. Ele deriva da classe OutputStream .
Classes Leitor e Escritor
A classe FileReader nos permite ler dados de caracteres de fluxos, e a classe FileWriter é usada para gravar fluxos de caracteres. O código a seguir mostra como escrever e ler de um arquivo:
String fileName = "c:/codegym/Example.txt";
// Create a FileWriter object
try (FileWriter writer = new FileWriter(fileName)) {
// Write content to file
writer.write("This is a simple example\nin which we\nwrite to a file\nand read from a file\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
// Create a FileReader object
try (FileReader fr = new FileReader(fileName)) {
char[] a = new char[200]; // Number of characters to read
fr.read(a); // Read content into an array
for (char c : a) {
System.out.print(c); // Display characters one by one
}
} catch (IOException e) {
e.printStackTrace();
}
Agora vamos falar sobre Java NIO :
Canal
Ao contrário dos fluxos usados no Java IO , Channel é uma interface bidirecional, ou seja, pode ler e escrever. Um canal Java NIO suporta fluxo de dados assíncrono nos modos de bloqueio e não bloqueio.
RandomAccessFile aFile = new RandomAccessFile("C:/codegym/file.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(100);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read: " + bytesRead);
buf.flip();
while(buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
Aqui usamos um FileChannel . Usamos um canal de arquivo para ler dados de um arquivo. Um objeto de canal de arquivo só pode ser criado chamando o método getChannel() em um objeto de arquivo — não há como criar diretamente um objeto de canal de arquivo.
Além de FileChannel , temos outras implementações de canal:
-
FileChannel — para trabalhar com arquivos
-
DatagramChannel — um canal para trabalhar em uma conexão UDP
-
SocketChannel — um canal para trabalhar em uma conexão TCP
-
ServerSocketChannel contém um SocketChannel e é semelhante ao funcionamento de um servidor da Web
Observação: o FileChannel não pode ser alternado para o modo sem bloqueio. O modo sem bloqueio do Java NIO permite que você solicite dados de leitura de um canal e receba apenas o que está disponível no momento (ou nada se ainda não houver dados disponíveis) . Dito isso, SelectableChannel e suas implementações podem ser colocadas no modo sem bloqueio usando o método connect() .
Seletor
O Java NIO introduziu a capacidade de criar um encadeamento que sabe qual canal está pronto para gravar e ler dados e pode processar esse canal específico. Essa habilidade é implementada usando a classe Selector .
Conectando canais a um seletor
Selector selector = Selector.open();
channel.configureBlocking(false); // Non-blocking mode
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Então criamos nosso Selector e o conectamos a um SelectableChannel .
Para ser usado com um seletor, um canal deve estar no modo sem bloqueio. Isso significa que você não pode usar o FileChannel com um seletor, pois o FileChannel não pode ser colocado no modo sem bloqueio. Mas os canais de soquete funcionarão bem.
Aqui vamos mencionar que em nosso exemplo SelectionKey é um conjunto de operações que podem ser executadas em um canal. A tecla de seleção permite saber o estado de um canal.
Tipos de chave de seleção
-
SelectionKey.OP_CONNECT significa um canal que está pronto para se conectar ao servidor.
-
SelectionKey.OP_ACCEPT é um canal que está pronto para aceitar conexões de entrada.
-
SelectionKey.OP_READ significa um canal que está pronto para ler dados.
-
SelectionKey.OP_WRITE significa um canal que está pronto para gravar dados.
Amortecedor
Os dados são lidos em um buffer para processamento posterior. Um desenvolvedor pode mover para frente e para trás no buffer, o que nos dá um pouco mais de flexibilidade ao processar dados. Ao mesmo tempo, precisamos verificar se o buffer contém a quantidade de dados necessária para o processamento correto. Além disso, ao ler dados em um buffer, certifique-se de não destruir os dados existentes que ainda não foram processados.
ByteBuffer buf = ByteBuffer.allocate (2048);
int bytesRead = channel.read(buf);
buf.flip(); // Change to read mode
while (buf.hasRemaining()) {
byte data = buf.get(); // There are methods for primitives
}
buf.clear(); // Clear the buffer - now it can be reused
Propriedades básicas de um buffer:
|
|
---|---|
capacidade | O tamanho do buffer, que é o comprimento da matriz. |
posição | A posição inicial para trabalhar com dados. |
limite | O limite operacional. Para operações de leitura, o limite é a quantidade de dados que podem ser lidos, mas para operações de gravação, é a capacidade ou cota disponível para gravação. |
marca | O índice do valor para o qual o parâmetro de posição será redefinido quando o método reset() for chamado. |
Agora vamos falar um pouco sobre o que há de novo no Java NIO.2 .
Caminho
Path representa um caminho no sistema de arquivos. Ele contém o nome de um arquivo e uma lista de diretórios que definem o caminho para ele.
Path relative = Paths.get("Main.java");
System.out.println("File: " + relative);
// Get the file system
System.out.println(relative.getFileSystem());
Paths é uma classe muito simples com um único método estático: get() . Ele foi criado apenas para obter um objeto Path da string ou URI passada.
Path path = Paths.get("c:\\data\\file.txt");
arquivos
Files é uma classe de utilitário que nos permite obter diretamente o tamanho de um arquivo, copiar arquivos e muito mais.
Path path = Paths.get("files/file.txt");
boolean pathExists = Files.exists(path);
Sistema de arquivo
FileSystem fornece uma interface para o sistema de arquivos. FileSystem funciona como uma fábrica para criar vários objetos (Caminho,PathMatcher,arquivos). Isso nos ajuda a acessar arquivos e outros objetos no sistema de arquivos.
try {
FileSystem filesystem = FileSystems.getDefault();
for (Path rootdir : filesystem.getRootDirectories()) {
System.out.println(rootdir.toString());
}
} catch (Exception e) {
e.printStackTrace();
}
Teste de performance
Para este teste, vamos pegar dois arquivos. O primeiro é um pequeno arquivo de texto e o segundo é um vídeo grande.
Vamos criar um arquivo e adicionar algumas palavras e caracteres:
Nosso arquivo ocupa um total de 42 bytes na memória:
Agora vamos escrever o código que copiará nosso arquivo de uma pasta para outra. Vamos testá-lo nos arquivos pequenos e grandes para comparar a velocidade de IO e NIO e NIO.2 .
Código para copiar, escrito usando Java IO :
public static void main(String[] args) {
long currentMills = System.currentTimeMillis();
long startMills = currentMills;
File src = new File("/Users/IdeaProjects/testFolder/text.txt");
File dst = new File("/Users/IdeaProjects/testFolder/text1.txt");
copyFileByIO(src, dst);
currentMills = System.currentTimeMillis();
System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
}
public static void copyFileByIO(File src, File dst) {
try(InputStream inputStream = new FileInputStream(src);
OutputStream outputStream = new FileOutputStream(dst)){
byte[] buffer = new byte[1024];
int length;
// Read data into a byte array and then output to an OutputStream
while((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
}
}
E aqui está o código para Java NIO :
public static void main(String[] args) {
long currentMills = System.currentTimeMillis();
long startMills = currentMills;
File src = new File("/Users/IdeaProjects/testFolder/text.txt");
File dst = new File("/Users/IdeaProjects/testFolder/text2.txt");
// Code for copying using NIO
copyFileByChannel(src, dst);
currentMills = System.currentTimeMillis();
System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
}
public static void copyFileByChannel(File src, File dst) {
// 1. Get a FileChannel for the source file and the target file
try(FileChannel srcFileChannel = new FileInputStream(src).getChannel();
FileChannel dstFileChannel = new FileOutputStream(dst).getChannel()){
// 2. Size of the current FileChannel
long count = srcFileChannel.size();
while(count > 0) {
/**=============================================================
* 3. Write bytes from the source file's FileChannel to the target FileChannel
* 1. srcFileChannel.position(): the starting position in the source file, cannot be negative
* 2. count: the maximum number of bytes transferred, cannot be negative
* 3. dstFileChannel: the target file
*==============================================================*/
long transferred = srcFileChannel.transferTo(srcFileChannel.position(),
count, dstFileChannel);
// 4. After the transfer is complete, change the position of the original file to the new one
srcFileChannel.position(srcFileChannel.position() + transferred);
// 5. Calculate how many bytes are left to transfer
count -= transferred;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
Código para Java NIO.2 :
public static void main(String[] args) {
long currentMills = System.currentTimeMillis();
long startMills = currentMills;
Path sourceDirectory = Paths.get("/Users/IdeaProjects/testFolder/test.txt");
Path targetDirectory = Paths.get("/Users/IdeaProjects/testFolder/test3.txt");
Files.copy(sourceDirectory, targetDirectory);
currentMills = System.currentTimeMillis();
System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
}
Vamos começar com o arquivo pequeno.
O tempo de execução para Java IO foi de 1 milissegundo em média. Executando o teste várias vezes, obtemos resultados de 0 a 2 milissegundos.
O tempo de execução do Java NIO é muito maior. O tempo médio é de 11 milissegundos. Os resultados variaram de 9 a 16. Isso ocorre porque o Java IO funciona de maneira diferente do nosso sistema operacional. O IO move e processa arquivos um por um, mas o sistema operacional envia os dados em um grande bloco. O NIO teve um desempenho ruim porque é orientado a buffer, não orientado a fluxo como IO .
E também vamos executar nosso teste para Java NIO.2 . O NIO.2 melhorou o gerenciamento de arquivos em comparação com o Java NIO . É por isso que a biblioteca atualizada produz resultados tão diferentes:
Agora vamos tentar testar nosso arquivo grande, um vídeo de 521 MB. A tarefa será exatamente a mesma: copiar o arquivo para outra pasta. Olhar!
Resultados para Java IO :
E aqui está o resultado para Java NIO :
O Java NIO manipulou o arquivo 9 vezes mais rápido no primeiro teste. Testes repetidos mostraram aproximadamente os mesmos resultados.
E também tentaremos nosso teste em Java NIO.2 :
Por que esse resultado? Simplesmente porque não faz muito sentido compararmos o desempenho entre eles, já que servem a propósitos diferentes. O NIO é uma E/S de baixo nível mais abstrata, enquanto o NIO.2 é orientado para o gerenciamento de arquivos.
Resumo
Podemos dizer com segurança que o Java NIO é significativamente mais eficiente ao trabalhar com arquivos graças ao uso dentro de blocos. Outra vantagem é que a biblioteca NIO é dividida em duas partes: uma para trabalhar com arquivos e outra para trabalhar com a rede.
A nova API do Java NIO.2 para trabalhar com arquivos oferece muitos recursos úteis:
-
endereçamento de sistema de arquivos muito mais útil usando Path ,
-
manipulação significativamente aprimorada de arquivos ZIP usando um provedor de sistema de arquivos personalizado,
-
acesso a atributos de arquivo especiais,
-
muitos métodos convenientes, como ler um arquivo inteiro com uma única instrução, copiar um arquivo com uma única instrução, etc.
É tudo sobre arquivos e sistemas de arquivos, e é tudo de alto nível.
A realidade hoje é que o Java NIO é responsável por cerca de 80-90% do trabalho com arquivos, embora a participação do Java IO ainda seja significativa.
💡 PS Estes testes foram executados em um MacBook Pro 14" 16/512. Os resultados do teste podem diferir com base no sistema operacional e nas especificações da estação de trabalho.
GO TO FULL VERSION