まだFileでI/Oですか?

せっかくJava8の時代(いつの話ですか!)になったのに、未だにnew File("xxxx")なんてコードをよく見かけます。

何故!!!!!!!!!!

ということで、Javaによる最新(いつの話ですか!)ファイルI/Oのご紹介です。

先ずは、Fileに代わるPath

java.nio.fileパッケージには、ファイルやディレクトリを操作する便利なクラスやインターフェイスが用意されています。

その中でも、ベースとなるのが多分Pathインターフェイスでしょう。

Pathを作る

下記のようにして作成します。

下記のような記述でも作成できます。

上記の、path1path2は同じファイルを表現するので、equalsメソッドで比較するとtrueが返ります。

但し、下記のような記述では注意が必要です。

Windowsの環境では、問題なくファイルパスと認識されるのですが、私の環境(Linuxのデスクトップ)では単一のファイル(名前)と認識されてしまいます。

逆に、Windows環境で"/home/foo/currentDir/java.txt"を引数に作成した場合は、正しいファイルパスとして認識されます。

Pathを操作する

このクラスには、ファイルパスを操作する便利な機能が様々実装されています。基本的にはJavaDocを参照していただくとして、よく使うと思われるいくつかを紹介します。

相対パスを絶対パスに変換する

上記コードは、私の環境では下記のように出力します(カレントディレクトリが/home/foo/currentDirだとすると)。

java.txt
/home/foo/currentDir/java.txt

Windows環境では下記のように出力します(カレントディレクトリがC:\home\foo\currentDirだとすると)。

java.txt
C:\home\foo\currentDir\java.txt

親ディレクトリを取得する

上記コードは、下記のように出力します(カレントディレクトリが/home/foo/currentDirだとすると)。

/home/foo/currentDir

パスを解決する

上記コードは、下記のように出力します(カレントディレクトリが/home/foo/currentDirだとすると)。

/home/foo/currentDir/c.txt

親のパスに対してパスを解決する

上記コードは、下記のように出力します(カレントディレクトリが/home/foo/currentDirだとすると)。

/home/foo/currentDir/c.txt

(3)と同じことを、わざわざ親ディレクトリを取得しないで実現できます。

ファイル名だけ取得する

上記コードは、下記のように出力します。

java.txt
java.txt

どうしてもFileが使いたい

ありますよ。

実際のファイルやディレクトリを操作する

論理パスではなく、実際のファイルやディレクトリを操作するためには、java.nio.file.Filesクラスを使います。

このクラスには、ファイルやディレクトリを操作/参照する便利な機能が様々実装されています。こちらも基本的にはJavaDocを参照していただくとして、よく使うと思われるいくつかを紹介します。

ファイルの情報を取得する

Pathに該当するファイルが本当に存在するか確認する

pathに該当するファイルやディレクトリが存在すれば、trueが返ります。

最終更新日を取得する

FileTimeクラスの文字列表現は、UTCのISO 8601形式となります。

2020-01-30T08:10:51.365231Z

ローカル日付に変換して、フォーマットを変更する

少々本題からは外れますが、UTCでは使い難いのでFileTimeをローカル日付に変換する方法です。

(2)のソースの続きです。

2020/01/30 17:10:51

DateTimeFormatter#ofPatternが返すインスタンスはスレッドセーフなので、SimpleDateFormatの様に使う度に作成する必要はありません。

日付は和暦以外認めません

だいぶ本題から外れますが、和暦で表示する方法です。

LocalDateTimeを取得する箇所までは、(3)と同じです。

和暦で表示する場合は、java.time.chrono.JapaneseDateを使います。

3行目で、LocalDateTimeからJapaneseDateに変換しています。

但し、JapaneseDateは日付しか保持しないので、時分秒までフォーマットしようとすると、例外がスローされます。

このため、時刻はLocalDateTimeから取得します。

令和2年1月30日 17時10分51秒

令和に対応していないバージョンのJDKを使った場合は、当然「令和」とは表示されません。

ファイルを読み込む

バイナリとして一気に読み込む

ファイルの読み込みは、このクラスのおかげでとても簡単になりました。ワンステップです。

読み込んだサイズ:57バイト

テキストとして一気に読み込む

こちらも、ワンステップでリストに格納してくれるので、とても簡単です。

このファイルは、日本語です
SJISでエンコードされています

もちろん文字コードも指定できます。

文字コードを指定しない場合(Files#readAllLinesの第ニ引数がないバージョン)は、ファイルの内容がUTF-8としてデコードされます。

環境依存ではなくなったということですね。

マルチバイト文字のエンコード/デコード

またまた本題からはそれますが、Charsetのおかげで、地味にマルチバイト文字のエンコード/デコードが楽になりました。

String#getBytes(String)は例外をスローするのでtry〜catchで囲わざるを得ず、テストでカバレッジを100%にしたいがために、不自然なコーディングを余儀なくされたものでした。

しかし、Charset#forNameはランタイム例外しかスローしないので、無駄なtry〜catchも必要なくなりました。

まあ、String#getBytes(Charset)はJDK1.6からのサポートなので、今更何をって感じですかね。

テキストとして一行ずつ読み込む

おっと、一気にJava8っぽくなってきました。

関数型インターフェイスにアロー演算子、ラムダ式の登場です。

関数型プログラミングってやつですね。CPUのマルチコア化が進んで注目されているパラダイムです。オブジェクト指向と比べて...。おっと、このあたりは沼なので、さらっとこの程度で(^_^;

上記コードは関数型インターフェイスを強調するためにConsumerを登場させていますが、下記のようにも記述できます(こちらのほうが一般的でしょうか)。

Files#linesが返すStreamは、Files#readAllLinesと違い(関数型プログラミングなので)遅延評価されます。

上限が不明な巨大ファイルを操作するような場合、Files#readAllLinesは危険で使えませんが、Files#linesを使えば一定サイズのバッファリングで処理される(筈)ので、巨大なメモリを用意しなくても大丈夫です。

注意が必要なのは、エラーが発生するタイミングです。

JavaDocにも記述されていますが、Files#linesがスローするIOExceptionは、ファイルオープン時のエラーです。

リード中のエラーは、Streamの評価中にUncheckedIOExceptionとしてスローされます。

上記コード上では、4行目に当たります。そのため、7行目でキャッチしている例外をExceptionとしています。

また、全ての処理が終了した時点でStreamのクローズも必要です(特にサーバの処理では、ガベージコレクションに任せると痛い目にあいます)。

ファイルに書き込む

バイナリとして一気に書き込む

ファイルの書き込みも、このクラスのおかげでとても簡単になりました。ワンステップです。

現在のサイズ:7バイト

テキストとして一気に書き込む

こちらも、ワンステップでリストから書き込んでくれるので、とても簡単です。

現在のサイズ:56バイト

書き込む時に、環境標準の改行コードが各行の終端に付加されます。

Files#writeは、第4引数以降(Charsetを指定する場合。Charsetを指定しない場合は第3引数以降)に、ファイルをオープンする際のオプションを指定できます。

オプションを指定しない場合、指定のファイルが存在しなければ作成し、存在した場合はオープン時に、サイズ0となります。

テキストとして随時書き込む

先程はListを渡しましたが、Files#writeの第2引数はjava.lang.Iterableなので、事象が発生した時点で随時書き込むということも可能です。

hasNextの中で、事象の発生か終了を待って値を返すようにすれば、随時書き込みの完成です。

ディレクトリを渡り歩く

以下のようなディレクトリ構成を前提として説明します。

${user.home}/
  +tmp/
    +test/
      +sub_dir_01/
      | +sub_dir_text.txt
      +sub_dir_02/
      | +sub_dir_text.txt
      | +sub_dir_html.htm
      | +sub_dir_html.html
      +text_file_01.txt
      +text_file_02.txt
      +html_file_01.html

ディレクトリのファイルを取得する

${user.home}/tmp/test/text_file_01.txt
${user.home}/tmp/test/text_file_02.txt
${user.home}/tmp/test/html_file_01.html
${user.home}/tmp/test/sub_dir_01
${user.home}/tmp/test/sub_dir_02

指定のパス(この場合は${user.home}/tmp/test)の直下にあるファイルとディレクトリを一覧します。

全てのファイルとディレクトリを取得する

${user.home}/tmp/test
${user.home}/tmp/test/text_file_01.txt
${user.home}/tmp/test/text_file_02.txt
${user.home}/tmp/test/html_file_01.html
${user.home}/tmp/test/sub_dir_01
${user.home}/tmp/test/sub_dir_01/sub_dir_text.txt
${user.home}/tmp/test/sub_dir_02
${user.home}/tmp/test/sub_dir_02/sub_dir_text.txt
${user.home}/tmp/test/sub_dir_02/sub_dir_html.html
${user.home}/tmp/test/sub_dir_02/sub_dir_html.htm

指定のパスを含んだ配下にある全てのファイルとディレクトリを一覧します。

特定の拡張子のファイルを取得する

${user.home}/tmp/test/text_file_01.txt
${user.home}/tmp/test/text_file_02.txt

複数の拡張子を指定する場合は、下記のように指定します。

${user.home}/tmp/test/sub_dir_02/sub_dir_html.html
${user.home}/tmp/test/sub_dir_02/sub_dir_html.htm