为什么 Java IO 如此糟糕?

IO(输入和输出)API 是一种 Java API,它使开发人员可以轻松地使用流。假设我们收到了一些数据(例如,名字、中间名、姓氏),我们需要将其写入文件——是时候使用java.io了。

java.io 库的结构

但是Java IO也有它的缺点,所以让我们依次谈谈它们:

  1. 阻止对输入/输出的访问。问题在于,当开发人员尝试使用Java IO读取文件或向文件写入内容时,它会锁定文件并阻止对其的访问,直到工作完成。
  2. 不支持虚拟文件系统。
  3. 不支持链接。
  4. 很多很多检查过的异常。

使用文件总是需要处理异常:例如,尝试创建一个已经存在的新文件将抛出IOException。在这种情况下,应用程序应继续运行,并且应通知用户无法创建文件的原因。


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 {
...
}

在这里我们看到createTempFile方法在无法创建文件时抛出IOException 。必须适当地处理此异常。如果我们尝试在try-catch块之外调用此方法,编译器将生成错误并建议两种修复方法:将方法包装在try-catch块中或使调用File.createTempFile的方法抛出IOException (所以它可以在更高级别处理)。

到达 Java NIO 以及它与 Java IO 的比较

Java NIO或 Java Non-Blocking I/O(有时称为 Java New I/O)专为高性能 I/O 操作而设计。

让我们比较Java IO方法和替代它们的方法。

首先,让我们谈谈如何使用Java IO

输入流类


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

FileInputStream类用于从文件中读取数据它继承了InputStream类,因此实现了它的所有方法。如果无法打开文件,则会抛出FileNotFoundException 。

输出流类


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

用于将字节写入文件的FileOutputStream类。它派生自OutputStream类。

读者和作家类

FileReader类让我们从流中读取字符数据,FileWriter用于写入字符流以下代码显示了如何写入和读取文件:


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

现在让我们谈谈Java NIO

渠道

与Java IO中使用的流不同,Channel是双向接口,即既可以读也可以写。Java NIO通道支持阻塞和非阻塞模式下的异步数据流。


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();

这里我们使用了FileChannel。我们使用文件通道从文件中读取数据。文件通道对象只能通过调用文件对象的getChannel()方法来创建——没有办法直接创建文件通道对象。

除了FileChannel,我们还有其他通道实现:

  • FileChannel — 用于处理文件

  • DatagramChannel — 通过 UDP 连接工作的通道

  • SocketChannel — 通过 TCP 连接工作的通道

  • ServerSocketChannel包含一个SocketChannel并且类似于 Web 服务器的工作方式

请注意:FileChannel不能切换到非阻塞模式。Java NIO的非阻塞模式允许您请求从通道读取数据并仅接收当前可用的数据(如果还没有可用数据,则根本不接收任何数据)。也就是说,可以使用connect()方法将SelectableChannel及其实现置于非阻塞模式。

选择器

Java NIO引入了创建线程的能力,该线程知道哪个通道已准备好写入和读取数据并可以处理该特定通道。此功能是使用Selector类实现的。

将通道连接到选择器


Selector selector = Selector.open();
channel.configureBlocking(false); // Non-blocking mode
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

因此,我们创建了Selector并将其连接到SelectableChannel

要与选择器一起使用,通道必须处于非阻塞模式。这意味着您不能将FileChannel与选择器一起使用,因为FileChannel不能置于非阻塞模式。但是套接字通道可以正常工作。

这里提一下,在我们的示例中,SelectionKey是一组可以在通道上执行的操作。选择键让我们知道通道的状态。

选择键的类型

  • SelectionKey.OP_CONNECT表示准备好连接到服务器的通道。

  • SelectionKey.OP_ACCEPT是准备好接受传入连接的通道。

  • SelectionKey.OP_READ表示准备好读取数据的通道。

  • SelectionKey.OP_WRITE表示准备写入数据的通道。

缓冲

数据被读入缓冲区以供进一步处理。开发人员可以在缓冲区上来回移动,这让我们在处理数据时更加灵活。同时,我们需要检查缓冲区是否包含正确处理所需的数据量。此外,在将数据读入缓冲区时,请确保不要破坏尚未处理的现有数据。


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

缓冲区的基本属性:

基本属性
容量 缓冲区大小,即数组的长度。
位置 处理数据的起始位置。
限制 经营限额。对于读取操作,限制是可以读取的数据量,但对于写入操作,它是可用于写入的容量或配额。
标记 调用reset()方法时位置参数将重置为的值的索引。

现在让我们谈谈Java NIO.2中的新内容。

小路

Path表示文件系统中的路径。它包含文件名和定义文件路径的目录列表。


Path relative = Paths.get("Main.java");
System.out.println("File: " + relative);
// Get the file system
System.out.println(relative.getFileSystem());

Paths是一个非常简单的类,只有一个静态方法: get()。创建它只是为了从传递的字符串或 URI 中获取Path对象。


Path path = Paths.get("c:\\data\\file.txt");

文件

Files是一个实用类,可以让我们直接获取文件的大小、复制文件等。


Path path = Paths.get("files/file.txt");
boolean pathExists = Files.exists(path);

文件系统

FileSystem提供文件系统的接口。文件系统就像一个工厂,用于创建各种对象(小路,路径匹配器,文件). 它帮助我们访问文件系统中的文件和其他对象。


try {
      FileSystem filesystem = FileSystems.getDefault();
      for (Path rootdir : filesystem.getRootDirectories()) {
          System.out.println(rootdir.toString());
      }
  } catch (Exception e) {
      e.printStackTrace();
  }

性能测试

对于此测试,让我们使用两个文件。第一个是小文本文件,第二个是大视频。

我们将创建一个文件并添加一些单词和字符:

% 触摸文本.txt

我们的文件一共占用了42个字节的内存:

现在让我们编写代码,将我们的文件从一个文件夹复制到另一个文件夹。让我们在小文件和大文件上进行测试,以便比较IONIO以及NIO.2的速度。

用于复制的代码,使用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();
        }
    }

这是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();
        }
    }

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

让我们从小文件开始。

Java IO的平均执行时间为 1 毫秒。通过多次运行测试,我们得到了 0 到 2 毫秒的结果。

以毫秒为单位的执行时间:1

Java NIO的执行时间要长得多。平均时间为 11 毫秒。结果介于 9 到 16 之间。这是因为Java IO 的工作方式与我们的操作系统不同。IO一个一个地移动和处理文件,但操作系统将数据一大块发送。NIO表现不佳,因为它是面向缓冲区的,而不是像IO那样面向流。

执行时间(以毫秒为单位):12

让我们也为Java NIO.2运行我们的测试。与Java NIO相比, NIO.2改进了文件管理。这就是为什么更新后的库会产生如此不同的结果:

执行时间(以毫秒为单位):3

现在让我们尝试测试我们的大文件,一个 521 MB 的视频。任务将完全相同:将文件复制到另一个文件夹。看!

Java IO的结果:

以毫秒为单位的执行时间:1866

这是Java NIO的结果:

执行时间(以毫秒为单位):205

Java NIO在第一次测试中处理文件的速度提高了 9 倍。重复测试显示大致相同的结果。

我们还将尝试在Java NIO.2上进行测试:

执行时间(以毫秒为单位):360

为什么是这个结果?仅仅是因为我们比较它们之间的性能没有多大意义,因为它们服务于不同的目的。NIO是更抽象的低级 I/O,而NIO.2则面向文件管理。

概括

我们可以有把握地说,由于在块内部使用,Java NIO在处理文件时效率显着提高。另一个优点是NIO库分为两部分:一部分用于处理文件,另一部分用于处理网络。

Java NIO.2用于处理文件的新 API 提供了许多有用的特性:

  • 使用Path寻址更有用的文件系统,

  • 使用自定义文件系统提供程序显着改进了对 ZIP 文件的处理,

  • 访问特殊文件属性,

  • 许多方便的方法,例如用一条语句读取整个文件,用一条语句复制文件等。

这都是关于文件和文件系统的,而且都是相当高级的。

今天的现实是Java NIO大约占文件工作的 80-90%,尽管Java IO的份额仍然很大。

💡 PS 这些测试是在 MacBook Pro 14" 16/512 上运行的。测试结果可能因操作系统和工作站规格而异。