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ブロックの外で呼び出そうとすると、コンパイラはエラーを生成し、それを修正するための 2 つのオプションを提案します。メソッドを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のノンブロッキング モードを使用すると、チャネルからの読み取りデータをリクエストし、現在利用可能なデータのみを受信できます (利用可能なデータがまだない場合は何も受信しません)。ただし、SelectableChannelとその実装は、 connect()メソッドを使用してノンブロッキング モードに設定できます。

セレクタ

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

バッファの基本的なプロパティ:

基本属性
容量 バッファ サイズ。配列の長さです。
位置 データを操作するための開始位置。
限界 動作限界。読み取り操作の場合、制限は読み取り可能なデータの量ですが、書き込み操作の場合、制限は書き込みに使用できる容量またはクォータです。
マーク replace()メソッドが呼び出されたときに位置パラメータがリセットされる値のインデックス。

ここで、 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 は、ファイル システムへのインターフェイスを提供します。FileSystem は、さまざまなオブジェクトを作成するためのファクトリーのように機能します (パスマッチャーファイル)。これは、ファイル システム内のファイルやその他のオブジェクトにアクセスするのに役立ちます。


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

性能テスト

このテストでは、2 つのファイルを取り上げます。1 つ目は小さなテキスト ファイルで、2 つ目は大きなビデオです。

ファイルを作成し、いくつかの単語と文字を追加します。

% タッチテキスト.txt

このファイルはメモリ内で合計 42 バイトを占有します。

次に、ファイルをあるフォルダーから別のフォルダーにコピーするコードを作成しましょう。IONIONIO.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 はファイルを 1 つずつ移動して処理しますが、オペレーティング システムはデータを 1 つの大きな塊にまとめて送信します。NIO はIOのようなストリーム指向ではなくバッファ指向であるため、パフォーマンスが低下しました。

実行時間 (ミリ秒): 12

Java NIO.2のテストも実行してみましょう。NIO.2では、 Java NIOと比較してファイル管理が改善されています。これが、更新されたライブラリがこのように異なる結果を生成する理由です。

実行時間 (ミリ秒): 3

次に、大きなファイルである 521 MB のビデオをテストしてみましょう。タスクはまったく同じです。ファイルを別のフォルダーにコピーします。見て!

Java IOの結果:

実行時間 (ミリ秒): 1866

Java NIOの結果は次のとおりです。

実行時間 (ミリ秒): 205

Java NIO は、最初のテストでファイルを 9 倍高速に処理しました。テストを繰り返した結果、ほぼ同じ結果が得られました。

また、 Java NIO.2でテストを試してみます。

実行時間 (ミリ秒): 360

なぜこの結果になったのでしょうか? 単純に、目的が異なるため、両者のパフォーマンスを比較することはあまり意味がありません。NIOはより抽象的な低レベル I/O ですが、NIO.2はファイル管理を指向しています。

まとめ

Java NIO は、ブロック内で使用することにより、ファイルを操作する際の効率が大幅に向上したと言っても過言ではありません。もう 1 つの利点は、NIOライブラリが 2 つの部分に分かれていることです。1 つはファイルの操作用、もう 1 つはネットワークの操作用です。

ファイルを操作するためのJava NIO.2の新しい API は、多くの便利な機能を提供します。

  • Pathを使用したファイルシステムのアドレス指定がはるかに便利です。

  • カスタム ファイル システム プロバイダーを使用した ZIP ファイルの処理が大幅に改善されました。

  • 特殊なファイル属性へのアクセス、

  • 単一のステートメントでファイル全体を読み取る、単一のステートメントでファイルをコピーするなど、便利なメソッドが多数あります。

それはすべてファイルとファイル システムに関するものであり、すべて非常に高レベルです。

現在、 Java NIO がファイル操作のおよそ 80 ~ 90% を占めているのが現実ですが、 Java IOのシェアは依然として大きいです。

💡 PS これらのテストは MacBook Pro 14" 16/512 で実行されました。テスト結果はオペレーティング システムとワークステーションの仕様によって異なる場合があります。