javaのNIOおさらい

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

* IO : https://qiita.com/liguofeng29/items/e08dc21b16c0057f601e


# 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が戻す位置|

上記４つの要素は下記を満足する
__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<br>positon = 0<br> mark = -1<br>       |
| 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<br>return this      |
| Buffer reset()                   | positionをmarkへ戻す                        | int m = mark<br>if (m < 0)<br>throw new InvalidMarkException()<br>position = m<br>return this |
| Buffer rewind()                  | postionをBufferの先頭に戻し、markを取り消す | position = 0<br> mark = -1<br>return this           |
| Buffer clear()                   | Buffer初期状態へ戻す                        | position = 0<br>limit = capacity<br>mark = -1<br>return this |


### CharBufferサンプル
※ CharBufferのtoString()は、`return toString(position(), limit());`

```java:出力用メソッド
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());
}
```
```java:生成する
CharBuffer buffer = CharBuffer.allocate(8);
System.out.println("-----生成後-----");
showProperty(buffer);
```
```出力```
-----生成後-----
capacity : 8
limit : 8
position : 0
toString() :         
```
![Buffer-allocate.png](https://qiita-image-store.s3.amazonaws.com/0/104977/2497625d-f231-645a-d64d-19076d0ed910.png)
1. bufferの最後は8
2. IO開始位置(position)は0
3. toString()は、position ~ limitなので、null８個

```java: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](https://qiita-image-store.s3.amazonaws.com/0/104977/0ea54b42-4dbe-a964-4619-372163f7613c.png)
1. put()三回したので、posiontは+3
2. toString()は、position ~ limitなので、null５個

```java:flip()
buffer.flip();
System.out.println("-----flip後-----");
showProperty(buffer);
```
```出力
-----flip後-----
capacity : 8
limit : 3
position : 0
toString() : abc
```
![Buffer-flip.png](https://qiita-image-store.s3.amazonaws.com/0/104977/a382a6ba-76bc-405f-7ae9-adb5d9e4b017.png)
1. IOできない最初のINDEX(limit)は3
2. IO出来る最初のINDEX(position)開始位置は0
3. toString()は、position ~ limitなので、`abc`

```java:get()
buffer.get();
buffer.get();
System.out.println("-----get-----");
showProperty(buffer);
```
```出力
-----get後-----
capacity : 8
limit : 3
position : 2
toString() : c
```
![Buffer-get.png](https://qiita-image-store.s3.amazonaws.com/0/104977/a1deb24d-ad1f-1063-150e-ee85c808510a.png)
1. get()２回なので、positionは+2、`c`を指す
2. 3. toString()は、position ~ limitなので、`c`

```java: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](https://qiita-image-store.s3.amazonaws.com/0/104977/be843ee5-2875-0530-4d03-e4aedd2f7f5f.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に入れる<br>読取り位置(Channel.position)は記録しない|
| Channel#read | ChannelからBufferに入れる<br>読取り位置は記録する |
| Channel#write | ChannelにBufferから入れる<br>書込み位置を記録する|

![channel-img.png](https://qiita-image-store.s3.amazonaws.com/0/104977/521bb332-203c-3b02-cc54-943bab97fd31.png)

### Channelの一部実装
* FileChannel
* Pipe.SinkChannel, Pipe.SourceChannle
* ServerSocketChannel
* SelectableChannel
* DatagramChannel

### FileChannel
#### map(), write()

```java:channel-in.txt(UTF-8)
ab12あ
あい
```
ab12 = 4byte
ああい = 9byte
\r\n = 2byte

```java: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()
```java
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#read１回目----");
    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#read２回目----");
    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#read１回目----
buffer capacity : 8
buffer limit : 8
buffer position : 8
channel position : 8
hasRead : 8
----channel#read２回目----
buffer capacity : 8
buffer limit : 8
buffer position : 7
channel position : 15
hasRead : 7
```
* read１回目
    * `buffer position : 8` -> 8byte読取った
    * `channel position : 8` -> 次はfile index = 8(9byte目)から読取る
* read２回目
    * `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取得
```java:Charset.availableCharsets()
// Charset一覧
Charset.availableCharsets().entrySet().forEach(System.out::println);

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

### ByteBuffer to CharBuffer
```java: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
```java:String(unicode)をSJISで書出す
try (
    FileChannel outChannel = new FileOutputStream("./NIO/file-sjis-out.txt").getChannel()) {

    // unicode
    String str = "123あいう" + System.lineSeparator() + "ＳＨＩＦＴ－ＪＩＳ";
    // 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());
}
```
か、

```java:Stringにもあるね！！
new String(byte[] xx, "Shift_JIS");
```
### 一つ不明点
utf-8(1byte ~ 6byte)のような、文字によってバイト数が異なる場合、
Bufferサイズはどうすればいいのか？？？

```text: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
```java: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());
}
```
```text:出力
abあ
```
`い`は無視された

#### CodingErrorAction.IGNORE
```java: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());
}
```
```text:出力
abあおおお
```
`い`の１バイト目⇒お
`い`の２バイト目⇒お
`い`の３バイト目⇒お

#### CodingErrorAction.REPORT
IGNORE,REPALCEは元データを破壊する！！

```java: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は、１バイト～６バイトなので、最小でも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()
```java: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回実行した際

```text:jvm1の出力
lock取得できた
60秒間lock維持
```
jvm2の出力は待機するので、出力なし。

### tryLock()
```java: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());
}
```
```text:jvm1出力
tryLock取得できた
```
```text:jvm2出力
lock is null
```

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


# 次はNIO.2
https://qiita.com/liguofeng29/items/3d0ba350a1547630727b