読者です 読者をやめる 読者になる 読者になる

可変長固定長

IT関係の話題が中心

Java NIO.2でファイル入出力など、ファイル操作の使い方を一通りまとめてみた

java

NIO.2とはJava SE7で導入された新しいファイルシステムにアクセスするためのAPIです。新しいといってももう大分経ってますね。

 

NIO.2では、従来のjava.ioパッケージよりも機能拡充されており、ファイルの作成やコピー、削除といった基本操作に加えてファイル属性の取得など、より簡単に利用できるようになっています。

 

 

また、FilesクラスのwalkFileTreeメソッドによるファイルツリーに対する再帰検索やWatchServiceインターフェースによるディレクトリ監視といった便利なライブラリも提供されています。非常に便利です。

 

ファイルI/Oをプロジェクトで新規に実装する場合や、既存の資産を作り変える場合において、オープンソース等の外部ライブラリを利用する方針でなければ、JDK標準のNIO.2を選択肢にしないわけにはいかないでしょう。

 

 

 

  

目次

 

 

NIO.2の主要なパッケージ

まずはNIO.2の主要なパッケージをおさえておきましょう。

  • java.nio.fileパッケージ

ファイルにアクセスするためのインターフェースとクラスが提供されてます。

  • java.nio.file.attributeパッケージ

ファイル属性にアクセスするためのインターフェースとクラスが提供されてます。

 

 

Pathクラスを利用したファイルやディレクトリパス操作

NIO.2で最初にすべきことはjava.nio.file.Pathインターフェースのオブジェクトを取得するところでしょう。Pathオブジェクトはディレクトリパスを抽象化したもので、ファイルシステムに関連した様々なメソッドが提供されています。

 

PathオブジェクトはPathsクラスのgetメソッドで取得することができます。getメソッドの引数にはパス文字列を指定します。この時、指定したパス文字列のディレクトリやファイルは実際に存在する必要はありません。ファイルチェックや新規作成をするケースを考えると当然です。

 

Path p = Paths.get("C:¥¥foo¥¥bar¥¥a.txt");

 

Pathインターフェースは従来のjava.io.Fileのようなものといえば理解しやすいかもしれません。NIO.2が導入されたJDK1.7以降では、java.io.Fileから相互変換するメソッドが提供されています。

 

File f = new File("C:¥¥foo¥¥bar¥¥a.txt");
Path p = f.toPath();

 

Pathインターフェースにはファイルシステムのパスにアクセスするためのメソッドが提供されています。良く使いそうなメソッドと使い方を紹介します。

 


Path getName(index)メソッド

引数indexに指定した要素番号のPathを取得する事ができます。要素番号の0はルートディレクトリでは無いことに注意してください。

 

例えば、C:¥¥foo¥¥bar¥¥a.txtのようなパス情報をもつPathオブジェクトの場合、要素番号0を指定するとルートのC:を除いた最初のパスであるfooを取得することになります。

 

Path p = Paths.get("C:\foo\bar\a.txt");
Path p1 = p.getName(0);
Path p2 = p.getName(1);
Path p3 = p.getName(2);
System.out.println(p1);//foo
System.out.println(p2);//bar
System.out.println(p3);//a.txt

 

Pathオブジェクトが保持していない要素番号を指定するとどうなるでしょうか。もちろん実行エラーとなります。存在しない配列の要素数を超えて指定するようなものです。

 

Path p4 = p.getName(3);
System.out.println(p4);
>Exception in thread "main" java.lang.IllegalArgumentException

 


int getNameCount()メソッド

パス情報の要素数を取得するメソッドです。該当のPathオブジェクトのパス情報がどれだけの数のディレクトリを保持しているかわかります。ただし、getNameメソッド同様にルートディレクトリは要素数に含まないことに注意してください。また、ファイル名は要素数に含みます。

 

Path p = Paths.get("C:\foo\bar\a.txt");
System.out.println(p.getNameCount());//3

 

上記のPathオブジェクトが保持しているfooとbarとa.txtをカウントして3が表示されています。

 


Path subpath(beginIndex, endIndex)メソッド

subpathメソッドもgetNameメソッドと同様にパス情報を取得することができます。こちらの場合は引数で要素番号の範囲を指定することができます。ただし、同様にルートディレクトリは含まないことと、ファイル名も含まないことに注意してください。また、endIndexに指定された要素番号からマイナス1された範囲を取得します。

 

Path p = Paths.get("C:\foo\bar\a.txt");
Path sub1 = p.subpath(0, 1);
Path sub2 = p.subpath(0, 2);
System.out.println(sub1);//foo
System.out.println(sub2);//fooar

 

Pathオブジェクトが保持しているパス情報の範囲を超えてしてするとどうなるでしょうか。getNameメソッド同様、保持していない要素にアクセスすると実行エラーとなります。

 

Path sub3 = p.subpath(0, 9);
System.out.println(sub3);
>java.lang.IllegalArgumentException

 


boolean startsWith(other)メソッド

Pathオブジェクトが保持しているパス情報が引数で指定した文字列から始まっているかをチェックすることができます。

 

Path p = Paths.get("C:\foo\bar\a.txt");
if (p.startsWith("C:\")) {
 System.out.println("true");
} else {
 System.out.println("false");
}

 

上記のコードではtrueが出力されます。startsWithメソッドではルートディレクトリを含めて判定されます。startsWithメソッドはオーバーロードされているので別のPathオブジェクトを指定することもできます。

 

p.startsWith(Paths.get("C:\")

 


Path relativize(other)メソッド

引数で指定した別のPathオブジェクトへの相対パスを取得します。取得する相対パスはrelativizeメソッドを実行したPathオブジェクトからみての相対パスとなる点に注意が必要です。引数で渡した別のPathオブジェクトからみる勘違いをするコードミスをしてしまいがちです。

 

Path p = Paths.get("C:\foo\bar\a.txt");
Path r = p.relativize(Paths.get("C:\foo\other\test\b.txt"));
System.out.println(r);//....other est.txt

 


Path normalize()メソッド

Pathオブジェクトが保持しているパス情報のなかで、.\や..\とかかれていた場合に、それらを省略したPathオブジェクトを返します。

 

Path n = Paths.get("C:\foo\..\bar\..\.\.\a.txt");
System.out.println(n.normalize());//C:\a.txt

 

 

ファイルやディレクトリの情報取得、コピー、移動、削除

java.nio.file.Filesクラスは、ファイルやディレクトリを操作するライブラリが提供されている重要なクラスで、例えばファイルの新規作成やコピー、移動や削除といったおなじみの操作やファイル属性の取得、ファイルツリーの再帰検索といった操作も簡単に実装できます。

 

Filesクラスのメソッドはすべてstaticで定義されています。NIO.2はこのFilesクラスのメソッドでシステム開発シーンで実現したいことはほぼ網羅されていると思ってもよいくらい充実しています。

 


ファイルコピー Path Files.copy(source, target, options)メソッド

ファイルコピーはcopyメソッドを利用します。sourceにはコピー元のPathオブジェクト、targetにはコピー先のPathオブジェクトを指定します。optionsにはコピーオプションを任意で設定できます。

 

Path c1 = Paths.get("C:\foo\a.txt");
Path c2 = Paths.get("C:\foo\b.txt");
Files.copy(c1, c2);

 

コピー先のファイルが存在する場合はjava.nio.file.FileAlreadyExistsExceptionが発生します。もしコピー先ファイルが存在する場合に上書きしたい場合はコピーオプションのStandardCopyOption.REPLACE_EXISTINGを指定しましょう。

 

Files.copy(c1, c2, StandardCopyOption.REPLACE_EXISTING);


読み取り専用等のファイル属性をコピーしたい場合は、コピーオプションのStandardCopyOption.COPY_ATTRIBUTESを指定します。

 

Files.copy(c1, c2, StandardCopyOption.COPY_ATTRIBUTES);

 

コピー元ファイルがシンボリックリンクであった場合はコピーしたくない場合、コピーオプションのLinkOption.NOFOLLOW_LINKSを設定します。

 

Files.copy(c1, c2, LinkOption.NOFOLLOW_LINKS);

 

コピーオプションは複数指定することが可能です。

Files.copy(c1, c2, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);

 

ちなみにですが、StandardCopyOption.REPLACE_EXISTINGのように定数を利用する場合、クラス名が冗長になってしまいますよね。この場合、スタティックインポートを利用することでコードをスッキリと書くことができるのでお勧めします。

 

import static java.nio.file.StandardCopyOption.*;

Files.copy(c1, c2, REPLACE_EXISTING, COPY_ATTRIBUTES);

 


ファイル移動 Path Files.move(source, target, options)メソッド

ファイル移動はmoveメソッドを利用します。sourceには移動元のPathオブジェクト、targetには移動先のPathオブジェクトを指定します。optionsには移動オプションを任意で設定できます。

 

Path m1 = Paths.get("C:\foo\a.txt");
Path m2 = Paths.get("C:\foo\b.txt");
Files.move(m1, m2);

 

移動なのでもちろんですが、上記のコードを実行するとa.txtは無くなりb.txtが存在します。moveメソッドにもオプションを設定することができます。移動先のファイルを上書きしたい場合はStandardCopyOption.REPLACE_EXISTINGを指定します。

 

Files.move(m1, m2, StandardCopyOption.REPLACE_EXISTING);//上書き

 

アトミックにファイルを移動したい場合は移動オプションとしてStandardCopyOption.ATOMIC_MOVEを指定します。アトミックオプションを指定した場合、もしファイルがロックされていた時は移動が取り消されます。

 

Files.move(m1, m2, StandardCopyOption.ATOMIC_MOVE);

 


ファイル削除 void Files.delete(path)メソッド

ファイル削除を行いたい場合はdeleteメソッドを利用します。このメソッドでは、引数に渡したPathオブジェクトが保持するファイルを削除します。ここでは例として、ファイルの存在チェックにexistsメソッドを利用しています。

 

Path d1 = Paths.get("C:\foo\bar\a.txt");
if(Files.exists(d1, LinkOption.NOFOLLOW_LINKS)) {
 Files.delete(d1);
}

 


同じファイルか判定 Files.isSameFile(path, path2)メソッド

Pathオブジェクトが同じパス情報を保持しているかはisSameFileメソッドを利用します。第一引数のpathオブジェクトが第二引数のpath2オブジェクトと同じパス情報を保持している場合、trueが返ってきます。

 

仮に、path2オブジェクトに.や..で冗長になっていた場合、normalizeしてパスの冗長部分を削除しなくても同じパス情報と判断されています。

 

Path h1 = Paths.get("C:\foo\bar\a.txt");
Path h2 = Paths.get("C:\foo\bar\b.txt");
Path h3 = Paths.get("C:\foo\.\bar\a.txt");
System.out.println(Files.isSameFile(h1, h2));//false
System.out.println(Files.isSameFile(h1, h3));//true
System.out.println(Files.isSameFile(h1, h3));//true

 

リンクの作成を行いたい場合はcreateLinkメソッドを利用します。リンク作成というのはあまり利用シーンが無いかもしれませんね。また、isRegularFileメソッドは引数で指定したPathオブジェクトがファイルであるかどうかを判定しています。

 

createLinkメソッドで作成されたPathオブジェクトもファイルとしてみなされるのでtrueが返っています。

 

Path l1 = Paths.get("C:\foo\bar\a.txt");//作成するリンクファイル
Path l2 = Paths.get("C:\foo\bar\b.txt");//リンク元
Path l3 = Files.createLink(l1, l2);
System.out.println(Files.isRegularFile(l3));//true

 

 

属性取得 Files.readAttributes(path, type, options)メソッド

readAttributesメソッドではファイルの属性情報を一括で取得することができます。ファイルの属性情報とはファイルに付随するメタデータのことです。例えばファイルの作成時間やファイルサイズ、変更時間なども取得することができます。なかなか利用シーンは多いと思います。

 

Path p1 = Paths.get("C:\foo\bar\a.txt");
BasicFileAttributes attr = Files.readAttributes(p1, BasicFileAttributes.class);
System.out.println(attr.creationTime());//作成時間
System.out.println(attr.size());//ファイルサイズ(バイト)
System.out.println(attr.lastModifiedTime());//最終変更時間
System.out.println(attr.lastAccessTime());//最終アクセス時間

 

readAttributesの第二引数ではBasicFileAttributes.classを指定しています。readAttributesメソッドでファイル属性を取得する場合、ファイル属性をもつインターフェースを指定する必要があります。

 

  • BasicFileAttributes すべてのファイルシステムで実装されている基本情報を持つインターフェース
  • DosFileAttributes DOSサポート
  • PosixFileAttributes POSIXサポート

 

基本的なファイル属性を取得したい場合はBasicFileAttributesを指定しておけば良いでしょう。Windows系の情報が必要ならDosFileAttributes、UNIX系の情報が必要ならPosixFileAttributesの指定を検討してください。

 


ファイルの作成と属性指定

ファイルの作成と同時にファイル属性の指定をしてみます。ここではDOSベースシステムにおいて隠しファイル指定と読み取り指定を設定しています。

 

Path p2 = Paths.get("C:\foo\bar\test.txt");
Files.createFile(p2);
Files.setAttribute(p2, "dos:hidden", true);//隠しファイル
Files.setAttribute(p2, "dos:readonly", true);//読み取り専用

 


ファイルシステムへのアクセス

FileSystemクラスを利用すると、実行している環境におけるファイルシステム内のファイルやディレクトリにアクセスすることができます。例えばルートディレクトリの一覧を取得したり、サポートしているファイル属性を出力することができます。

 

FileSystem fs = FileSystems.getDefault();
Iterable<Path> fsp = fs.getRootDirectories();
for (Path path : fsp) {
 System.out.println(path);//ルートディレクト
}
for (String support : fs.supportedFileAttributeViews()) {
 System.out.println(support);//サポートしているファイル属性
}

 


FileVisitorによるファイルツリーに対する再帰的な探索

ここからはNIO.2がパワフルになってくるところです。FilesクラスのwalkFileTreeメソッドとFileVisitorインターフェースを利用することにより、ファイルツリーに対する再帰的な探索を実現することができます。

 

Files.walkFileTree(start, visitor)メソッド

第一引数には検索ルートを示すPathオブジェクトを渡します。第二引数に渡すFileVisitorインターフェースの実装クラスは、定義されているすべてのメソッドをオーバーライドして実装する必要があります。

 

FileVisitorインターフェースに定義されている抽象メソッドは4メソッドあります。

  • preVisitDirectory ディレクトリ内にアクセスされる前に呼び出すメソッド
  • visitFile ファイルにアクセスする際に呼び出すメソッド
  • visitFileFailed ファイルにアクセスできなかった際に呼び出すメソッド
  • postVisitDirectory ディレクトリ内のすべてのエントリにアクセス後に呼び出すメソッド

 

これらのメソッドの内容からわかるように、実装したFileVisitorインターフェースでは、ディレクトリアクセス前やファイルアクセス時、ファイルアクセスができなかった場合等の挙動を指定してファイルの再帰検索を実装することができます。

 

また、FileVisitorインターフェースの4つのメソッドの戻り値は、すべてFileVisitResult型となります。FileVisitResultは列挙型として定義されており、それぞれの値がもつ意味は以下の通りです。

 

  • FileVisitResult.CONTINUE ファイル探索を続行
  • FileVisitResult.TERMINATE ファイル探索を終了
  • FileVisitResult.SKIP_SUBTREE 対象のディレクトリとサブディレクトリはスキップ
  • FileVisitResult.SKIP_SIBLINGS 対象のディレクトリはアクセスされず、postVisitDirectoryは呼び出されない

 

FileVisitorインターフェースには4つのメソッドがあり、すべて実装する必要があります。でもこれは少し面倒です。visitFileメソッドやvisitFileFailedメソッドはよく利用するかもしれませんが、postVisitDirectoryメソッドは利用するシーンが少ないかもしれません。

 

そこで、FileVisitorインターフェースを実装したSimpleFileVisitorクラスを継承したクラスを定義することをおすすめします。SimpleFileVisitorクラスを継承したクラスであれば、実装が必要なメソッドのみオーバーライドすれば良いだけです。簡単に再帰的なファイル検索が実装できる非常に便利なライブラリです。

 

以下のコードはC:oo配下のディレクトリを再帰的に検索して、C:ooara.txtを探すサンプルです。

 

Path start = Paths.get("C:\foo\");
Files.walkFileTree(start, new MyFileVisitor());

class MyFileVisitor extends SimpleFileVisitor<Path> {
 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
  if (Files.isSameFile(file, Paths.get("C:\foo\bar\a.txt"))) {
   System.out.println("Same File.");
   return FileVisitResult.TERMINATE;
  }
  return FileVisitResult.CONTINUE;
 }
}

 

 

探索パターンを指定したファイル探索

PathMatcherを使用することで、探索するファイルやディレクトリのパターンを指定することができます。例えばテキストファイルなら.txtの拡張子をもつファイルといった指定が可能です。PathMatcherオブジェクトは、FileSystemsクラスのgetPathMatcherメソッドで取得することができます。

 

このとき、正規表現regex)とより簡単な指定をする場合はglob構文(glob)で指定します。glob構文のパターンとしては?(特定の1文字)や*(0文字以上の文字)といった指定ができます。

 

以下のコードでは、"glob:*.txt"という指定でPathMatcherオブジェクトを生成しています。つまり、.txtにマッチングするディレクトリやファイルを探しています。"glob:{*.txt,*.jar,*.java}"のように複数指定することも可能です。

 

Path start = Paths.get("C:\foo\");
Files.walkFileTree(start, new MyFileVisitor2());

class MyFileVisitor2 extends SimpleFileVisitor<Path> {
 private PathMatcher matcher = null;
 public MyFileVisitor2() {
  matcher = FileSystems.getDefault().getPathMatcher("glob:*.txt");
 }
 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
  if (matcher.matches(file.getFileName())) {
   System.out.println("Matche:*.txt");
   return FileVisitResult.TERMINATE;
  }
  return FileVisitResult.CONTINUE;
 }
}

 

 

WatchServiceを利用したディレクトリの監視

WatchServiceインターフェースを利用することで、ディレクトリを監視して、ファイルやディレクトリの作成イベントや変更イベント、削除イベントなどの通知を受け取ることができます。特定のディレクトリにファイルが作成された場合に何か処理を実行するといった使い方に利用できます。

 

WatchServiceインターフェースの使い方は、FileSystemsクラスのnewWatchServiceメソッドでWatchServiceオブジェクトを取得し、監視したいディレクトリ情報を保持するPathオブジェクトのregisterメソッドに登録してあげるだけで利用できます。

 

監視イベントは以下の4つです。StandardWatchEventKindsクラスに定数として定義されています。

 

  • StandardWatchEventKinds.ENTRY_CREATE エントリの作成
  • StandardWatchEventKinds.ENTRY_MODIFY エントリの更新
  • StandardWatchEventKinds.ENTRY_DELETE エントリの削除
  • StandardWatchEventKinds.OVERFLOW イベントが消失又は破棄された

 

以下のサンプルコードはC:ooディレクトリ内を監視します。このコードではENTRY_CREATE、ENTRY_MODIFY、ENTRY_DELETEが登録されています。仮にENTRY_CREATEが登録されていなかった場合、監視対象ディレクトリでファイル作成をしても、ENTRY_CREATEイベントは発生しないため、"ENTRY_CREATE"が出力されることはありません。必要なイベントは必ず登録しておくように注意してください。

 

try {

 Path w1 = Paths.get("C:\foo\");
 WatchService ws = FileSystems.getDefault().newWatchService();
 w1.register(ws, StandardWatchEventKinds.ENTRY_CREATE,StandardWatchEventKinds.ENTRY_MODIFY,StandardWatchEventKinds.ENTRY_DELETE);

 while(true) {
  WatchKey key = ws.take();//発生したイベントを取得
  for (WatchEvent<?> e : key.pollEvents()) {
   switch(e.kind().name()) {
    case "ENTRY_CREATE"://作成イベント
     System.out.println("ENTRY_CREATE");
     break;
    case "ENTRY_DELETE"://削除イベント
     System.out.println("ENTRY_DELETE");
     break;
    case "ENTRY_MODIFY"://変更イベント
     System.out.println("ENTRY_MODIFY");
     break;
    case "OVERFLOW"://イベント消失又は破棄
     System.out.println("OVERFLOW");
     break;
   }
  }
  key.reset();//監視キーのリセット
 }
} catch(Exception e) {
}

 

以上です。従来のjava.ioとは違ってわかりやすくて非常に便利になっているのがわかったと思います。