Help us understand the problem. What is going on with this article?

javaのNIOおさらい

More than 1 year has passed since last update.

javaのIOに続いて、NIOについておさらい!!

NIOとは

簡単に言えば、既存のIOを改善したものです。

既存IOのよくない点

  1. https://docs.oracle.com/javase/jp/8/docs/api/java/io/InputStream.html#read--
    「入力データが読み込めるようになるか、ストリームの終わりが検出されるか、または例外が発生するまで、このメソッドはブロックされます。 」

  2. 効率がよくない
    処理単位がbyte,charなので、効率がよくない。
    hige level stream(PrintStreamとか)を使っても基底の処理単位は変わりません。

NIOのコアAPI

  • Channel
    • データソースとアプリを繋げるパイプのようなもの。
  • Buffer
    • ChannleがIOするための容器のようなもの。
    • 本質は配列である。

Buffer

Bufferは抽象クラスであり、boolean以外の基本型に対応するサブクラスが存在する.

Buffer内の重要要素

要素名 説明
capacity Bufferのサイズを表す
limit BufferのIO出来ない最初のINDEX
position BufferのIOできる最初のINDEX
mark Buffer内のmarkして、resetした際にpositionが戻す位置

上記4つの要素は下記を満足する
0 <= mark <= position <= limit <= capacity

Bufferの常用サブクラス

  • ByteBuffer
    • MappedByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FolatBuffer
  • DoubleBuffer
  1. ByteBufferは、set/get
  2. ByteBufferは、put/get

Bufferの常用メソッド

メソッド 説明 CharBufferの実装
Buffer flip() 出力を準備する limit = position
positon = 0
mark = -1
int capacity() Bufferのcapacity返却 return capacity
int limit() Bufferのlimit返却 return limit
int position() Bufferのposition返却 return position
int remaining() positionとlimit間の要素数 return limit - position
boolean hasRemaining() positionとlimit間に要素あるか return position < limit
Buffer position(int newPostion) positionを設定する
Buffer mark() 現在のpositionでmarkする mark = position
return this
Buffer reset() positionをmarkへ戻す int m = mark
if (m < 0)
throw new InvalidMarkException()
position = m
return this
Buffer rewind() postionをBufferの先頭に戻し、markを取り消す position = 0
mark = -1
return this
Buffer clear() Buffer初期状態へ戻す position = 0
limit = capacity
mark = -1
return this

CharBufferサンプル

※ CharBufferのtoString()は、return toString(position(), limit());

出力用メソッド
private static void showProperty(CharBuffer buffer) {
    System.out.println("capacity : " + buffer.capacity());
    System.out.println("limit : " + buffer.limit());
    System.out.println("position : " + buffer.position());
    System.out.println("toString() : " + buffer.toString());
}
生成する
CharBuffer buffer = CharBuffer.allocate(8);
System.out.println("-----生成後-----");
showProperty(buffer);
-----生成後-----
capacity : 8
limit : 8
position : 0
toString() :         

Buffer-allocate.png
1. bufferの最後は8
2. IO開始位置(position)は0
3. toString()は、position ~ limitなので、null8個

put()
buffer.put('a');
buffer.put('b');
buffer.put('c');
System.out.println("-----put後-----");
showProperty(buffer);
-----put後-----
capacity : 8
limit : 8
position : 3
toString() :      

Buffer-put.png
1. put()三回したので、posiontは+3
2. toString()は、position ~ limitなので、null5個

flip()
buffer.flip();
System.out.println("-----flip後-----");
showProperty(buffer);
-----flip後-----
capacity : 8
limit : 3
position : 0
toString() : abc

Buffer-flip.png
1. IOできない最初のINDEX(limit)は3
2. IO出来る最初のINDEX(position)開始位置は0
3. toString()は、position ~ limitなので、abc

get()
buffer.get();
buffer.get();
System.out.println("-----get-----");
showProperty(buffer);
-----get後-----
capacity : 8
limit : 3
position : 2
toString() : c

Buffer-get.png
1. get()2回なので、positionは+2、cを指す
2. 3. toString()は、position ~ limitなので、c

clear()
buffer.clear();
System.out.println("-----clear後-----");
showProperty(buffer);
System.out.println("get(2) : " + buffer.get(2));
-----clear後-----
capacity : 8
limit : 8
position : 0
toString() : abc     
get(2) : c

Buffer-clear.png
1. 初期位置に戻すので、position=0, limit,capacity=8
2. ここで表示はしてないが、mark=-1
3. clear()は位置を戻すだけで、bufferをクリアするものではないので
clear()後でもget(2)でcが取得できる

おまけ

ByterBufferには、allocateDirect()でインスタンス取得するメソッドが用意されている。
IO効率が高いが、生成コストが高いので、使い捨てには向いてないそうです。

Channel

直接接点(HDD、ネット等)に繋げるパイプであり、全てのIOはBufferを通して行う。

IOメソッド 説明
Channel#map 開始、終了位置の範囲をBufferに入れる
読取り位置(Channel.position)は記録しない
Channel#read ChannelからBufferに入れる
読取り位置は記録する
Channel#write ChannelにBufferから入れる
書込み位置を記録する

channel-img.png

Channelの一部実装

  • FileChannel
  • Pipe.SinkChannel, Pipe.SourceChannle
  • ServerSocketChannel
  • SelectableChannel
  • DatagramChannel

FileChannel

map(), write()

channel-in.txt(UTF-8)
ab12
あい

ab12 = 4byte
ああい = 9byte
\r\n = 2byte

map(),write()
try (
    FileChannel channelIn = new FileInputStream(f).getChannel();
    FileChannel channelOut = new FileOutputStream("./NIO/channel-out.txt").getChannel()) {

    // map()
    MappedByteBuffer mappedByteBuffer = channelIn.map(
        FileChannel.MapMode.READ_ONLY, 0, f.length()
    );
    System.out.println("----channel#map----");
    System.out.println("buffer capacity : " + mappedByteBuffer.capacity());
    System.out.println("buffer limit : " + mappedByteBuffer.limit());
    System.out.println("buffer position : " + mappedByteBuffer.position());
    System.out.println("channel position : " + channelIn.position());

    // write
    channelOut.write(mappedByteBuffer);
    System.out.println("----channel#write----");
    System.out.println("buffer capacity : " + mappedByteBuffer.capacity());
    System.out.println("buffer limit : " + mappedByteBuffer.limit());
    System.out.println("buffer position : " + mappedByteBuffer.position());
    System.out.println("channel position : " + channelOut.position());
} catch (IOException ex) {
    System.out.println(ex.getMessage());
}
----channel#map----
buffer capacity : 15
buffer limit : 15
buffer position : 0
channel position : 0
----channel#write----
buffer capacity : 15
buffer limit : 15
buffer position : 15
channel position : 15
  • map処理
    • getChannel() -> FileChannelインスタンス生成
    • 0, f.length() -> 0~15byte取得
    • buffer position : 0 -> flip()済み
    • channel position -? ファイル内読取位置を保持しない
  • write
    • out.write -> position ~ limitを書出す
    • buffer position : 15 -> 書出した分positionが移動された
    • channel position : 15 -> 次回はファイルindex = 15(16byte目)から書込みする

read()

File f = new File("./NIO/channel-in.txt");
try (
    FileChannel inChannel = new FileInputStream(f).getChannel()) {

    ByteBuffer byteBuffer = ByteBuffer.allocate(8);
    int hasRead = 0;

    System.out.println("----channel#read1回目----");
    hasRead = inChannel.read(byteBuffer);
    System.out.println("buffer capacity : " + byteBuffer.capacity());
    System.out.println("buffer limit : " + byteBuffer.limit());
    System.out.println("buffer position : " + byteBuffer.position());
    System.out.println("channel position : " + inChannel.position());
    System.out.println("hasRead : " + hasRead);

    byteBuffer.clear();

    System.out.println("----channel#read2回目----");
    hasRead = inChannel.read(byteBuffer);
    System.out.println("buffer capacity : " + byteBuffer.capacity());
    System.out.println("buffer limit : " + byteBuffer.limit());
    System.out.println("buffer position : " + byteBuffer.position());
    System.out.println("channel position : " + inChannel.position());
    System.out.println("hasRead : " + hasRead);
} catch (IOException ex) {
    System.out.println(ex.getMessage());
}
----channel#read1回目----
buffer capacity : 8
buffer limit : 8
buffer position : 8
channel position : 8
hasRead : 8
----channel#read2回目----
buffer capacity : 8
buffer limit : 8
buffer position : 7
channel position : 15
hasRead : 7
  • read1回目
    • buffer position : 8 -> 8byte読取った
    • channel position : 8 -> 次はfile index = 8(9byte目)から読取る
  • read2回目
    • buffer position : 7 -> 7byte読取った
    • channel position : 15 -> 次はfile index = 15(9byte目)から読取る
  • flip()しないと、get()使えないね

Charset

javaはデフォルトでUnicodeを使うが、それ以外の文字コードを読取る際に文字化けが発生する可能性がある。
Charsetは、byteとcharの変換をするために提供されている。

IOメソッド 説明
CharBuffer Charset#decode(ByteBuffer buf) ByteBuffer to CharBuffer
ByteBuffer Charset#encode(CharBuffer buf) CharBuffer to ByteBuffer
ByteBuffer Charset#encode(String str) String to ByteBuffer

javaがサポートするcharset取得

Charset.availableCharsets()
// Charset一覧
Charset.availableCharsets().entrySet().forEach(System.out::println);

// defalut
System.out.println("default charset : " + Charset.defaultCharset()); // UTF-8
一部
・・・略
Shift_JIS=Shift_JIS
UTF-16=UTF-16
UTF-32=UTF-32
UTF-8=UTF-8
・・・略

ByteBuffer to CharBuffer

SJISファイル読込む
File f = new File("./NIO/file-sjis-in.txt");
try (
    FileChannel inChannel = new FileInputStream(f).getChannel()) {

    // FileChannel to ByteBuffer
    ByteBuffer byteBuffer = ByteBuffer.allocate(6);
    inChannel.read(byteBuffer);
    byteBuffer.flip();

    // Shift_JIS
    Charset sjis = Charset.forName("Shift_JIS");

    // decode buff with SJIS
    CharBuffer charBuffer = sjis.decode(byteBuffer);
    System.out.println("str : " + charBuffer.toString());
} catch (IOException ex) {
    System.out.println(ex.getMessage());
}

String to ByteBuffer

String(unicode)をSJISで書出す
try (
    FileChannel outChannel = new FileOutputStream("./NIO/file-sjis-out.txt").getChannel()) {

    // unicode
    String str = "123あいう" + System.lineSeparator() + "SHIFT-JIS";
    // Shift_JIS
    Charset sjis = Charset.forName("Shift_JIS");
    // encode buff with SJIS
    ByteBuffer byteBuffer = sjis.encode(str);
    // write to file
    outChannel.write(byteBuffer);
} catch (IOException ex) {
    System.out.println(ex.getMessage());
}

か、

Stringにもあるね!!
new String(byte[] xx, "Shift_JIS");

一つ不明点

utf-8(1byte ~ 6byte)のような、文字によってバイト数が異なる場合、
Bufferサイズはどうすればいいのか???

UTF-8のファイル
abあい

ab = 2byte
あい = 3byte + 3byte

ByteBuffer byteBuffer = ByteBuffer.allocate(6)にした場合、
abあ + いの1バイト目がbufferに入ってしまい、decodeしたらabあ�になる。

この場合、どうすればいいの???

  1. 文字セットの公倍数を使う。
    「1,2,3,4,5,6」なので、60バイトの倍数をBufferサイズとして使う。

  2. 文字化けを判別し、Buffer先頭に入れてpositionを調整御、readする。
    ⇒ map()は、ブロック読み込み&file positionは維持しないね!
    はやり文字化けしないサイズに指定しないといけないのか??

下記で一応解決。

制御 説明
onMalformedInput 不正入力エラー
onUnmappableCharacter マップできない文字エラー
種別 説明
CodingErrorAction.IGNORE エラー文字無視
CodingErrorAction.REPLACE エラー文字置き換え
CodingErrorAction.REPORT エラー報告

CodingErrorAction.IGNORE

IGNOREサンプル
File f = new File("./a/file-utf8-in.txt");
try (
    FileChannel inChannel = new FileInputStream(f).getChannel()) {
    ByteBuffer byteBuffer = ByteBuffer.allocate(6);
    while (inChannel.read(byteBuffer) > -1) {
        // IO準備
        byteBuffer.flip();

        Charset utf8 = Charset.forName("UTF-8");
        CharsetDecoder decoder = utf8.newDecoder();

        CharBuffer charBuffer2 = decoder
            .onMalformedInput(CodingErrorAction.IGNORE)
            .decode(byteBuffer);
        System.out.print(charBuffer2.toString());
        byteBuffer.clear();
    }
} catch (IOException ex) {
    System.out.println(ex.getMessage());
}
出力
abあ

は無視された

CodingErrorAction.IGNORE

IGNORE
File f = new File("./a/file-utf8-in.txt");
try (
    FileChannel inChannel = new FileInputStream(f).getChannel()) {
    ByteBuffer byteBuffer = ByteBuffer.allocate(6);
    while (inChannel.read(byteBuffer) > -1) {
        // IO準備
        byteBuffer.flip();
        Charset utf8 = Charset.forName("UTF-8");
        CharsetDecoder decoder = utf8.newDecoder();
        CharBuffer charBuffer2 = decoder
            .onMalformedInput(CodingErrorAction.REPLACE).replaceWith("お")
            .decode(byteBuffer);
        System.out.print(charBuffer2.toString());
        byteBuffer.clear();
    }
} catch (IOException ex) {
    System.out.println(ex.getMessage());
}
出力
abあおおお

の1バイト目⇒お
の2バイト目⇒お
の3バイト目⇒お

CodingErrorAction.REPORT

IGNORE,REPALCEは元データを破壊する!!

REPORT
File f = new File("./a/file-utf8-in.txt");
try (
    FileChannel inChannel = new FileInputStream(f).getChannel()) {
    ByteBuffer byteBuffer = ByteBuffer.allocate(6);
    while (inChannel.read(byteBuffer) > -1) {
        byteBuffer.flip();
        Charset utf8 = Charset.forName("UTF-8");
        CharsetDecoder decoder = utf8.newDecoder();
        try {
            CharBuffer charBuffer = decoder
                .onMalformedInput(CodingErrorAction.REPORT)
                .decode(byteBuffer);

            // 出力
            System.out.print(charBuffer.toString());
            byteBuffer.clear();
        } catch (MalformedInputException ex) {
            // エラー発生位置と終了位置
            int errorIndexStart = byteBuffer.position();
            int errorIndexEnd = byteBuffer.limit();

            // 正常位置まで出力
            byteBuffer.flip();
            CharBuffer charBuffer = decoder
                .decode(byteBuffer);
            System.out.print(charBuffer.toString());

            // エラー位置開始~終了をバッファ先頭へ移動
            byteBuffer.clear();
            for (int i = errorIndexStart; i < errorIndexEnd; i++) {
                byteBuffer.put(byteBuffer.get(i));
            }
        }
    }
} catch (IOException ex) {
    System.out.println(ex.getMessage());
}
abあい
  1. エラー発生時、まず正常位置まで出力する。
  2. エラー発生位置から最後までbufferの先頭へ移動させる
  3. 次の読み取りは、0からではなく、エラー発生長さの後ろから
  4. utf-8は、1バイト~6バイトなので、最小でもallocate(6)は必要だね?

本当にこれが最善なのか??

FileLock

ファイルに対するロックを表す

FileChannelのロックメソッド 説明
FileChannel#lock() lock(0L, Long.MAX_VALUE, false)
FileChannel#lock(long position, long size, boolean shared) ロック範囲、共有ロック、排他ロック指定可能
FileChannel#tryLock() tryLock(0L, Long.MAX_VALUE, false)
FileChannel#tryLock(long position, long size, boolean shared) ロック範囲、共有ロック、排他ロック指定可能

lock()とtryLock()の違い

lock()はロック取得までスレッドブロックする。
tryLockはロック取得できないとnullを返す。

lock()

lockサンプル
try (FileChannel fileChannel = new FileOutputStream("file-lock-out.txt").getChannel();) {
    FileLock lock = fileChannel.lock();
    System.out.println("lock取得できた");
    System.out.println("60秒間lock維持");
    Thread.sleep(60 * 1000);
    lock.release();
} catch (IOException | InterruptedException ex) {
    System.out.println("1" + ex.getMessage());
}

上記を異なるjvmで2回実行した際

jvm1の出力
lock取得できた
60秒間lock維持

jvm2の出力は待機するので、出力なし。

tryLock()

tryLock()サンプル
try (FileChannel fileChannel = new FileOutputStream("file-lock-out.txt").getChannel();) {
    FileLock lock = fileChannel.tryLock();
    if (lock != null) {
        System.out.println("tryLock取得できた");
        Thread.sleep(60 * 1000);
        lock.release();
    } else {
        System.out.println("lock is null");
    }
} catch (IOException | InterruptedException ex) {
    System.out.println(ex.getMessage());
}
jvm1出力
tryLock取得できた
jvm2出力
lock is null

ほか

  1. lockはjvmが所持しているので、同じjvmで同じfileのlockはできない。
  2. プラットフォームによっては、FileChannelをクローズする際に、ロックをrelaseする。 なので、ロックされたファイルに対して複数FileChannelを開くのはよくない。
  3. プラットフォームによっては、ファイルロックは推奨であり、強制でないない。 ⇒ ロックなしで読み書きできるってこと。

次はNIO.2

https://qiita.com/liguofeng29/items/3d0ba350a1547630727b

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした