せっかくJava8の時代(いつの話ですか!)になったのに、未だにnew File("xxxx")
なんてコードをよく見かけます。
何故!!!!!!!!!!
ということで、Javaによる最新(いつの話ですか!)ファイルI/Oのご紹介です。
java.nio.file
パッケージには、ファイルやディレクトリを操作する便利なクラスやインターフェイスが用意されています。
その中でも、ベースとなるのが多分Path
インターフェイスでしょう。
下記のようにして作成します。
下記のような記述でも作成できます。
上記の、path1
とpath2
は同じファイルを表現するので、equals
メソッドで比較するとtrue
が返ります。
但し、下記のような記述では注意が必要です。
Windowsの環境では、問題なくファイルパスと認識されるのですが、私の環境(Linuxのデスクトップ)では単一のファイル(名前)と認識されてしまいます。
逆に、Windows環境で"/home/foo/currentDir/java.txt"
を引数に作成した場合は、正しいファイルパスとして認識されます。
このクラスには、ファイルパスを操作する便利な機能が様々実装されています。基本的には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
ありますよ。
論理パスではなく、実際のファイルやディレクトリを操作するためには、java.nio.file.Files
クラスを使います。
このクラスには、ファイルやディレクトリを操作/参照する便利な機能が様々実装されています。こちらも基本的にはJavaDocを参照していただくとして、よく使うと思われるいくつかを紹介します。
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