🍍

5.1 ファイル操作(Java NIO.2、Path、Paths、Files、入出力ストリームなど)~Java Advanced編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、斉藤賢哉と申します。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Advanced編』の一部の範囲をカバーしたものです。『Java Advanced編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの基本的なスキルを習得済みで、さらなるレベルアップを目指している方
  • 将来的なキャリアとして、希少性の高い上級エンジニアやアーキテクトを志向している方
  • フリーランスエンジニアとして付加価値の更なる向上を図っている方
  • 「Oracle認定Javaプログラマ」の資格取得を目指している方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

5.1 ファイル操作

チャプターの概要

このチャプターでは、ファイルの操作(コピー・移動等)と、ファイルへの入出力を行うためのクラスの特徴やAPIについて学びます。

5.1.1 ファイル操作の基本

Java SEおけるファイル操作用クラスライブラリ

Java SEにおけるファイル操作のためのクラスは、基本的にjava.ioパッケージに所属しています。ただし、Java 7で新しくJava NIO.2と呼ばれる新しいAPIが導入されたことにより、ファイル操作で使用するクラス群が刷新されました。
Java NIO.2におけるファイル操作のためのクラスは、java.nio.fileパッケージに所属しています。
現在でも、Java NIO.2以前のクラスによって作成されたアプリケーションを見かけることは、ゼロではありません。ただし今後新たにJavaでファイル操作を行う場合は、Java NIO.2として提供されたクラス群を使う方が望ましいでしょう。本コースでも、Java NIO.2のクラスを前提に説明を進めます。

ファイル操作のためのクラス・インタフェース

Javaでは元来、ファイルを操作するためのクラスとしてjava.io.Fileクラスが使われてきましたが、このクラスは機能性の観点で十分とは言えません。
昨今ではjava.io.Fileクラスの代わりに、Java NIO.2として提供された、以下のクラス・インタフェースを使うケースが一般的です。

  • java.nio.file.Path … ファイルシステム内のファイルおよびディレクトリを表すインタフェース
  • java.nio.file.Paths … Pathオブジェクトを生成するためのユーティリティクラス
  • java.nio.file.Files … ファイルやディレクトリを操作するためのユーティリティクラス

PathsクラスのAPIとその具体例

ここではjava.nio.file.PathsクラスのAPIと、その使い方を説明します。
これらのAPIは、いずれもPathオブジェクトを生成するためのユーティリティです。

API(メソッド) 説明
static Path get(String, String...) 1つのパス文字列または、連結すると1つのパス文字列になる文字列から、Pathオブジェクトを生成する。
static Path get(URI) 指定されたURI(java.net.URI)から、Pathオブジェクトを生成する。

具体的な使用方法を見ていきましょう。
例えば以下のように、get()メソッドにファイルのパスを指定して、Pathオブジェクトを生成することができます。

snippet_1 (pro.kensait.java.advanced.lsn_5_1_1.Main_Path)
Path path = Paths.get("hoge/fuga/foo.txt");

パスには、絶対パスも相対パスも指定することができます。相対パスの場合は、当該のJavaプログラムを実行したカレントディレクトリが起点になります。

また以下のように、ファイルのパスを構成する要素、すなわちディレクトリ名とファイル名を、文字列として列挙することも可能です。

snippet_2 (pro.kensait.java.advanced.lsn_5_1_1.Main_Path)
Path path = Paths.get("hoge", "fuga", "foo.txt");

また、以下のようにjava.net.URIのオブジェクトを指定することも可能です。ただしここで指定可能なURIのスキームは"file:"に限定されます。

snippet
Path path = Paths.get(new URI("file:/C:/learn_java_advenced/05_file_io/hoge/fuga/foo.txt"));

PathインタフェースのAPIとその具体例

次にjava.nio.file.Pathインタフェースです。
このインタフェースは、ファイルとディレクトリを、より抽象度の高い概念である「パス」として表すためのものです。
以下にその主要なAPIを示しますが、これらはいずれも「パス」に関するものに限定されます。ファイルやディレクトリとしての属性を取得したり、コピーや移動といった操作については、後述するFilesクラスの役割です。

API(メソッド) 説明
Path getFileName() このパスが示すファイルまたはディレクトリの名前(親パスを取り除いた部分)を、Pathオブジェクトで返す。
Path getParent() このパスの親パスを、Pathオブジェクトで返す。親を持たない場合はnullを返す。
Path toAbsolutePath() このパスの絶対パスを、Pathオブジェクトで返す。
Path normalize() このパスから冗長な名前要素を削除したPathオブジェクトを返す。
String toString() このパスの文字列表現を返す。
URI toUri() このパスを表すURIを返す。
File toFile() このパスを表すjava.io.Fileオブジェクトを返す。

それでは、具体的な使用方法をコードで見ていきましょう。

snippet_3 (pro.kensait.java.advanced.lsn_5_1_1.Main_Path)
Path path = Paths.get("./hoge/fuga/foo.txt"); // 相対パスでファイルを指定
Path fileName = path.getFileName(); // 親パスを除いたパス
System.out.println("fileName => " + fileName);
Path parent = path.getParent(); // 親パス
System.out.println("parent => " + parent);
Path absolutePath1 = path.toAbsolutePath(); // 絶対パス
System.out.println("absolutePath => " + absolutePath1);
Path absolutePath2 = path.toAbsolutePath().normalize(); // 冗長さを排除したパス
System.out.println("normalized absolutePath => " + absolutePath2);

このコードを実行すると、コンソールには以下のように表示されます。

fileName => foo.txt
parent => .\hoge\fuga
absolutePath => C:\learn_java_advenced\05_file_io\.\hoge\fuga\foo.txt
normalized absolutePath => C:\learn_java_advenced\05_file_io\hoge\fuga\foo.txt

FilesクラスのAPI

ここではjava.nio.file.Filesクラスの主要なAPIについて、その全体像を説明します。Filesクラスでは、ファイルまたはディレクトリを「パス」という概念で同一に扱います。このクラスのAPIは、すべてスタティックメソッドです。大きく、以下の3つに分類されます。

(1)指定されたパスの属性を取得するユーティリティ
(2)指定されたパスに対する何らかの操作を行うユーティリティ
(3)指定されたファイルへの読み込みや書き込みを行うためのオブジェクトを生成するファクトリメソッド

順番に見ていきましょう。

(1)指定されたパスの属性を取得するユーティリティ

API(メソッド) 説明
static boolean isDirectory(Path) 指定されたパスがディレクトリかどうかを判定し、その結果を返す。
static boolean isReadable(Path) 指定されたパスが読み込み可能かどうかを判定し、その結果を返す。
static boolean isWritable(Path) 指定されたパスが書き込み可能かどうかを判定し、その結果を返す。
static boolean isExecutable(Path) 指定されたパスが実行可能かどうかを判定し、その結果を返す。

(2)指定されたパスに対する何らかの操作を行うユーティリティ
※いずれのAPIも入出力エラーによってjava.io.IOException(チェック例外)が発生するため例外ハンドリングが必要

API(メソッド) 説明
static long size(Path) 指定されたファイルのサイズをバイトで返す。
static FileTime getLastModifiedTime(Path) 指定されたパスの最終変更時間を、java.nio.file.attribute.FileTime型で返す。
static UserPrincipal getOwner(Path) 指定されたパスの所有者を、java.nio.file.attribute.UserPrincipal型で返す。
static Path copy(Path, Path) 指定されたパスからパスへコピーする。
static Path move(Path, Path) 指定されたパスからパスへ移動、またはリネームする。
static void delete(Path) 指定されたパスを削除する。
static boolean deleteIfExists(Path) 指定されたパスが存在した場合に限り削除し、その結果をboolean型で返す。

(3)指定されたファイルへの読み込みや書き込みを行うためのオブジェクトを生成するファクトリメソッド
※いずれのAPIも入出力エラーによってjava.io.IOException(チェック例外)が発生するため例外ハンドリングが必要

API(メソッド) 説明
static BufferedReader newBufferedReader(Path, Charset) 指定されたテキストファイルを、指定された文字コードで読み込むためのBufferedReaderを生成して返す。
static BufferedWriter newBufferedWriter(Path, Charset) 指定されたテキストファイルに、指定された文字コードで書き込むためのBufferedWriterを生成して返す。
static InputStream newInputStream(Path) 指定されたファイルを読み込むためのInputStreamを生成して返す。
static OutputStream newOutputStream(Path) 指定されたファイルに書き込むためのOutputStreamを生成して返す。

このレッスンでは、まず(1)と(2)について説明します。(3)については、レッスン5.1.2~5.1.3で取り上げます。

Filesクラスの使用方法(1):パスの属性取得

ここではFilesクラスのAPIによって、パスの属性を取得するための方法を説明します。
以下のコードを見てください。

snippet (pro.kensait.java.advanced.lsn_5_1_1.Main_Files_1)
Path path = Paths.get("hoge/fuga/foo.txt");
boolean isDirectory = Files.isDirectory(path);
System.out.println("isDirectory => " + isDirectory);
long length = Files.size(path);
System.out.println("length => " + length);
FileTime lastModified = Files.getLastModifiedTime(path);
System.out.println("lastModified => " + lastModified);
UserPrincipal owner = Files.getOwner(path);
System.out.println("owner => " + owner);

このコードを実行すると、コンソールには以下のように表示されます。

isDirectory => false
length => 136
lastModified => 2022-04-17T18:32:01.0327712Z
owner => MyPC\kenya (User)

Filesクラスの使用方法(2):パスの操作

FilesクラスのAPIによって、パスを操作するための方法を説明します。なおこれらのAPIは、いずれもjava.io.IOExceptionの例外ハンドリングが必要ですが、ここでは便宜上省略しています。
まずはファイルのコピーです。

snippet_1 (pro.kensait.java.advanced.lsn_5_1_1.Main_Files_2)
Path srcFile = Paths.get("hoge/fuga/foo.txt");
Path destFile = Paths.get("hoge/fuga/foo2.txt");
Files.copy(srcFile, destFile);

このようにcopy()メソッドを呼び出すと、指定されたファイル"hoge/fuga/foo.txt"が、ファイル"hoge/fuga/foo2.txt"にコピーされます。

続いてファイルの移動です。

snippet_2 (pro.kensait.java.advanced.lsn_5_1_1.Main_Files_2)
Path destFile = Paths.get("hoge/piyo/foo.txt");
Files.move(srcFile, destFile);

このようにmove()メソッドを呼び出すと、指定されたファイル"hoge/fuga/foo.txt"が、ファイル"hoge/piyo/foo.txt"に移動します。

続いてディレクトリのリネームです。

snippet_3 (pro.kensait.java.advanced.lsn_5_1_1.Main_Files_2)
Path srcDir = Paths.get("hoge/piyo");
Path destDir = Paths.get("hoge/piyo2");
Files.move(srcDir, destDir);

このようにmove()メソッドを呼び出すと、指定されたディレクトリ"hoge/piyo"が、ディレクトリ"hoge/piyo2"にリネームされます。

最後にファイルの削除です。

snippet_4 (pro.kensait.java.advanced.lsn_5_1_1.Main_Files_2)
Path destFile = Paths.get("hoge/fuga/foo2.txt");
Files.delete(destFile);

このようにdelete()メソッドを呼び出すと、指定されたファイル"hoge/fuga/foo2.txt"が削除されます。

5.1.2 入出力クラスの全体像とデータの分類

入出力クラスの全体像

一般的に、データは大きくテキストデータとバイナリデータに分類されます。両者の違いは後述するとして、ここではテキストデータとバイナリデータ、それぞれを読み込んだり書き込んだりするために必要なクラスの全体像を整理します。
いずれのクラスもjava.ioパッケージに所属しています。

【表5-1-1】入出力クラス全体像

データの種類 処理 抽象クラス よく使われる具象クラス
テキストデータ 読み込み Reader BufferedReader(テキストファイル用)
InputStreamReader(入力ストリーム用)
書き込み Writer BufferedWriter(テキストファイル用)
OutputStreamWriter(出力ストリーム用)
バイナリデータ 読み込み InputStream BufferedInputStream(バイナリファイル用)
ByteArrayInputStream(バイト配列用)
ObjectInputStream(オブジェクト用)
書き込み OutputStream BufferedOutputStream(バイナリファイル用)
ByteArrayOutputStream(バイト配列用)
ObjectOutputStream(オブジェクト用)

テキストデータと文字コード

データはテキストデータとバイナリデータに分類されますが、そもそもテキストデータとは、何でしょうか。
テキストデータもバイナリデータも、コンピュータの目線から見ると、何らかのバイト表現(複数個のバイトからなる列)であることに変わりはありません。テキストデータとは「あいう」といった文字が、特定の文字コードによってバイト変換されたデータを指し、一方でバイナリデータとはテキストデータ以外のデータのことです。
つまりテキストファイルとは、テキストデータが特定の文字コードによってバイト変換され、それがファイルシステム上に保存されたもの、ということになります。従ってテキストファイルを読み込むためには、対象のテキストファイルがどのような文字コードでバイト変換されているのかを、指定する必要があります。同じようにテキストファイルを書き込むためには、どのような文字コードでテキストをバイト表現に変換するのか、指定しなければなりません。
Javaの入出力クラス全体像の中では、テキストデータを読み込むための汎用的なインタフェースを持つ抽象クラスが、java.io.Readerです。一方テキストデータを書き込むための汎用的なインタフェースを持つ抽象クラスが、java.io.Writerです。
ReaderとWriterには、入出力のソースに応じて、いくつかの具象クラスが用意されています。例えばBufferedReaderクラスは、テキストファイルを効率的に読み込むためのReaderであり、BufferedWriterクラスは、テキストファイルに効率的に書き込むためのWriterです。

文字コードを表すクラス

Javaには、文字コードを抽象化したクラスとして、java.nio.charset.Charsetクラスがあります。このクラスは、テキストファイルの読み込みや書き込みにおいて、文字コードを指定するために使用します。チャプター6.1で取り上げるネットワークプログラミングでも、送受信されるデータは、このクラスのAPIによって文字列との相互変換を行います。
また、java.nio.charset.StandardCharsetsクラスは、様々なCharsetオブジェクトを定数として保持するクラスです。このクラスには、UTF-8、UTF-16などの文字コードが定数として定義されていますが、昨今ではUTF-8を使うケースが一般的でしょう。
例えばUTF-8のCharsetであれば、以下のようにして取得します。

snippet
Charset charset = StandardCharsets.UTF_8;

これは、以下のコードと同義です。

snippet
Charset charset = Charset.forName("UTF-8");

入出力ストリーム

データ(バイナリデータ)を、何らかのリソースからパイプライン的に読み込むための機能を、入力ストリームと呼びます。一方データ(バイナリデータ)を、何らかのリソースに対してパイプライン的に書き込むための機能を、出力ストリームと呼びます。両者を合わせて、入出力ストリームと言うこともあります。
Javaの入出力クラス全体像の中では、入力ストリームを表す汎用的なインタフェースを持つ抽象クラスが、java.io.InputStreamです。一方出力ストリームを表す汎用的なインタフェースを持つ抽象クラスが、java.io.OutputStreamです。
InputStreamとOutputStreamには、入出力のソースに応じて、いくつかの具象クラスが用意されています。例えばBufferedInputStreamクラスは、バイナリファイルを効率的に読み込むためのInputStreamであり、BufferedOutputStreamクラスは、バイナリファイルに効率的に書き込むためのOutputStreamです。

5.1.3 テキストファイルの読み込みと書き込み

このレッスンでは、テキストファイルからの読み込みや書き込みを行う方法を説明します。

テキストファイルからの読み込み

テキストデータを何らかのリソースから読み込むための汎用的なインタフェースを持つクラスが、java.io.Readerです。Readerは抽象クラスであり、子の具象クラスにはいくつかの種類がありますが、ファイルの読み込みではほとんどの場合、java.io.BufferedReaderクラスが使われます。BufferedReaderクラスは、文字を一文字ずつ読み込むのではなく一行単位にバッファリングして読み込むため、効率的に処理することができます。
それでは、BufferedReaderクラスの使用方法を具体的に見てきましょう。

snippet (pro.kensait.java.advanced.lsn_5_1_3.Main_BufferedReader)
Path path = Paths.get("hoge/fuga/foo.txt"); //【1】
try (BufferedReader br = Files.newBufferedReader(path,
        StandardCharsets.UTF_8)) { //【2】
    String line;
    while ((line = br.readLine()) != null) { //【3】
        System.out.println(line);
    }
} catch (IOException ioe) {
    throw new RuntimeException(ioe);
}

このコードは、テキストファイル"hoge/fuga/foo.txt"を一行ごとに読み込み、その内容をコンソールに表示する、という処理を行うものです。
まずPathsクラスのget()メソッドによって、読み込み対象ファイルのPathオブジェクトを生成します【1】。
次にFilesクラスのnewBufferedReader()メソッドに、第一引数として対象のパス、第二引数として文字コードを引数に渡して、BufferedReaderオブジェクトを生成します【2】。文字コードには既出のCharsetクラスを指定しますが、ここではUTF-8として読み込むため、StandardCharsetsクラスの定数である"UTF_8"を指定しています。なお第二引数は省略可能で、その場合はOSデフォルトの文字コードが自動的に選択されます。BufferedReaderクラスによる読み込みでは、入出力がエラーがあるとIOException(チェック例外)が発生するため、例外ハンドリングが必要です。
またこのようにしてBufferedReaderを生成すると、ファイルというリソースがオープンされるため、最終的には必ずクローズする必要があります。BufferedReaderはjava.lang.AutoCloseableインタフェースをimplementsしているため、このコードのようにtry-with-resources文によって自動的にクローズすると良いでしょう。
このようにしてBufferedReaderオブジェクトを生成したら、それを使ってファイルを一行ごとに読み込みます。ファイルから一行毎にテキストを読み込むためには、readLine()メソッドを使用します【3】。readLine()メソッドで読み込んだデータは、指定された文字コードに従って文字列に変換され、String型として取り出されます(このコードでは変数line)。
またこのメソッドは、ファイルの最終行にたどり着くとnull値を返すため、while文の条件式に(line = br.readLine()) != nullと指定することで、行単位のループ処理を実現できます。このコードでは、このようにして読み込まれた各行(変数line)を、そのままコンソールに表示しています。

【図5-1-1】テキストファイルからの読み込み
image.png

テキストファイルからの簡易的な読み込み

BufferedReaderクラスではなく、FilesクラスのAPIによってテキストファイルを読み込むことも可能です。
以下のコードを見てください。

snippet (pro.kensait.java.advanced.lsn_5_1_3.Main_ReadAllLines)
Path path = Paths.get("hoge/fuga/foo.txt");
try {
    List<String> fileContents = Files.readAllLines(path, StandardCharsets.UTF_8);
    for (String line : fileContents) {
        System.out.println(line);
    }
} catch (IOException ioe) {
    throw new RuntimeException(ioe);
}

Filesクラスによってテキストファイルを読み込むためには、readAllLines()メソッドに、対象のパスと文字コードを引数に渡します。このようにすると、テキストファイルを、1行=1文字列として「String型のリスト」に読み込むことができます。なおreadAllLines()メソッドの第二引数は省略可能で、その場合はOSデフォルトの文字コードが選択されます。
この読み込み方法は、BufferedReaderクラスよりも幾分簡易的ではありますが、注意点があります。BufferedReaderクラスのreadLine()メソッドによるテキストファイル読み込みは「一行ごと」に行われますが、FilesクラスのreadAllLines()メソッドでは「一度にすべての行」が読み込まれメモリ展開されます。従ってこの方法で巨大なテキストファイルを読み込むのは、メモリリソースを大きく消費することになるため、控えた方が良いでしょう。

テキストファイルへの書き込み

テキストデータを何らかのリソースに書き込むための汎用的なインタフェースを持つクラスが、java.io.Writerです。Writerは抽象クラスであり、子の具象クラスにはいくつかの種類がありますが、ファイルの書き込みでは、ほとんどの場合、java.io.BufferedWriterクラスが使われます。BufferedWriterクラスは、文字を一文字ずつ書き込むのではなく一行単位にバッファリングして書き込むため、効率的に処理することができます。
それでは、BufferedWriterクラスの使用方法を具体的に見てきましょう。

snippet (pro.kensait.java.advanced.lsn_5_1_3.Main_BufferedWriter)
Path src = Paths.get("hoge/fuga/foo.txt"); //【1】
Path dest = Paths.get("hoge/fuga/foo2.txt"); //【2】
try (BufferedReader br = Files.newBufferedReader(src);
        BufferedWriter bw = Files.newBufferedWriter(dest)) { //【3】
    String line;
    while ((line = br.readLine()) != null) {
        bw.write(line + System.lineSeparator()); //【4】
    }
} catch (IOException ioe) {
    throw new RuntimeException(ioe);
}

このコードは、テキストファイル"hoge/fuga/foo.txt"を一行ごとに読み込み、その内容をそのままテキストファイル"hoge/fuga/foo2.txt"に書き込む、という処理を行うものです。
まずPathsクラスのget()メソッドによって、読み込み対象ファイルと書き込み対象ファイルのPathオブジェクトを生成します【1、2】。
次にFilesクラスのAPIによって、BufferedReaderとBufferedWriterのオブジェクトをそれぞれ生成します。BufferedWriterのオブジェクトは、FilesクラスのnewBufferedWriter()メソッドに、対象のパスと文字コードを引数に渡すことで生成しますが、ここでは文字コードは省略しています【3】。
どちらのクラスのAPIも入出力エラーがあるとIOException(チェック例外)が発生するため例外ハンドリングが必要ですが、どちらもAutoCloseableインタフェースをimplementsしているため、このコードのようにtry-with-resources文によって自動的にクローズすると良いでしょう。
次に読み込みと書き込みの処理です。while文によって、foo.txtファイルから一行単位に文字列を読み込むのは、前項の例と同様です。ループ処理の中では、BufferedWriterのwrite()メソッドに読み込んだ文字列を渡すことで、foo2.txtファイルへの書き込みを行います【4】。

ストリームからのテキストデータ読み込みと書き込み

前述したようにテキストデータがファイルとして保存されている、もしくはファイルとして保存する場合は、BufferedReaderクラスやBufferedWriterクラスといったクラスを使い、文字コードを意識しながら、読み込みや書き込みを行います。
ただしテキストデータは、常にファイルとして保存されているとは限らず、アプリケーションに対して、後述する入力ストリームとして読み込まれるケースがあります。例えばサーバーサイドのアプリケーション開発では、ネットワークを経由して送信されるテキストデータや、データベースに保存されたテキストファイルを読み込んだりする場合が、その典型です。このような場合は、テキストデータを読み込むためには、入力ストリームをもとに、文字コードを意識しながらReaderへの変換が必要です。
入力ストリームに文字コードを指定してReaderに変換するためには、InputStreamReaderクラスを利用します。具体的には、以下のようなコードになります。

snippet
InputStream is = .... // ネットワークやデータベースから読み込む
Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);

InputStreamReaderクラスには、入力ストリームと文字コードを引数に取るコンストラクタがあるため、それを使ってオブジェクトを生成します。生成したオブジェクトはReader型になるため、そこからテキストデータの読み込みを行うことができます。
なおここでは「入力ストリームからのテキストデータ読み込み」について説明しましたが、「出力ストリームに対するテキストデータ書き込み」も裏返しの処理となるため、割愛します。

5.1.4 バイナリファイルの読み込みと書き込み

このレッスンでは、入出力ストリームを使って、バイナリファイルからの読み込みや書き込みを行う方法を説明します。

バイナリファイルからの読み込み

InputStreamは抽象クラスであり、子の具象クラスには、java.io.BufferedInputStreamや、次のチャプターで取り上げるjava.io.ByteArrayInputStreamなどがあります。
ファイルの読み込みでは、ほとんどの場合、BufferedInputStreamクラスが使われます。BufferedInputStreamクラスは、データを一バイトずつ読み込むのではなく、一定サイズ分をバッファリングして読み込むため、効率的に処理することができます。
それではBufferedInputStreamクラスの使用方法を、コードで具体的に見てきましょう。

snippet (pro.kensait.java.advanced.lsn_5_1_4.Main_BufferedInputStream)
Path path = Paths.get("java_logo1.jpg"); //【1】
try (InputStream is = Files.newInputStream(path)) { //【2】
    BufferedInputStream bis = new BufferedInputStream(is); //【3】
    byte[] buf = new byte[10]; //【4】
    while (bis.read(buf) != -1) { //【5】
        for (byte b : buf) {
            System.out.println("読み込んだバイトデータ => " + b);
        }
    }
} catch (IOException ioe) {
    throw new RuntimeException(ioe);
}

このコードは、JPEGファイル"java_logo1.jpg"を読み込み、そのバイトデータをコンソールに表示する、という処理を行うものです。
まずPathsクラスのget()メソッドによって、読み込み対象ファイルのPathオブジェクトを生成します【1】。
次にFilesクラスのnewInputStream()メソッドに、対象のパスを引数に渡し、InputStreamを生成します【2】。
BufferedInputStreamクラスによるファイル読み込みでは、入出力がエラーがあるとIOException(チェック例外)が発生するため、例外ハンドリングが必要です。
またこのようにしてInputStreamを生成すると、ファイルというリソースがオープンされるため、最終的には必ずクローズする必要があります。InputStreamはjava.lang.AutoCloseableインタフェースをimplementsしているため、このコードのようにtry-with-resources文によって自動的にクローズします。
次にBufferedInputStreamのコンストラクタに生成済みのInputStreamを渡し、BufferedInputStreamオブジェクトを生成します【3】。
このようにしてBufferedInputStreamオブジェクトを生成したら、それを使ってファイルを一定サイズごとに読み込みます。ここではサイズ10のバイト配列bufを生成し【4】、それをバッファ領域として、10バイトごとに読み込むものとします。
ファイルからバッファ領域にバイトデータを読み込むためには、read()メソッドを使用します。read()メソッドを呼び出すと、入力ストリームからデータ読み込まれ、そのまま指定されたバッファ領域(変数buf)に格納されます【5】。
またこのメソッドはファイルの最後にたどり着くと-1を返すため、while文の条件式にbis.read(buf) != -1と指定することで、バッファ領域ごとのループ処理を実現できます。このコードでは、ファイルから読み込まれたデータが格納されたバッファ領域(変数buf)を、for文によって1バイトずつコンソールに表示しています。

バイナリファイルへの書き込み

OutputStreamは抽象クラスであり、子の具象クラスには、java.io.BufferedOutputStreamや、次のチャプターで取り上げるjava.io.ByteArrayOutputStreamなどがあります。
ファイルの書き込みでは、ほとんどの場合、BufferedOutputStreamクラスが使われます。BufferedOutputStreamクラスは、データを一バイトずつ書き込むのではなく、一定サイズ分をバッファリングして書き込むため、効率的に処理することができます。
それではBufferedOutputStreamクラスの使用方法を具体的に見てきましょう。

snippet (pro.kensait.java.advanced.lsn_5_1_4.Main_BufferedOutputStream)
Path src = Paths.get("java_logo1.jpg"); //【1】
Path dest = Paths.get("java_logo2.jpg"); //【2】
try (InputStream is = Files.newInputStream(src);
        OutputStream os = Files.newOutputStream(dest)) { //【3】
    BufferedInputStream bis = new BufferedInputStream(is); //【4】
    BufferedOutputStream bos = new BufferedOutputStream(os); //【5】
    byte[] buf = new byte[10]; //【6】
    while (bis.read(buf) != -1) { //【7】
        bos.write(buf); //【8】
    }
    bos.flush(); //【9】
} catch (IOException ioe) {
    throw new RuntimeException(ioe);
}

このコードは、バイナリファイル"java_logo1.jpg"を10バイトごとに読み込み、その内容をそのままバイナリファイル"java_logo2.jpg"に書き込む、という処理を行うものです。
まずPathsクラスのget()メソッドによって、読み込み対象ファイルと書き込み対象ファイルのPathオブジェクトを生成します【1、2】。
次にFilesクラスのAPIによって、InputStreamとOutputStreamのオブジェクトをそれぞれ生成します【3】。
どちらのクラスもAutoCloseableインタフェースをimplementsしているため、try-with-resources文によって自動的にクローズするようにします。
次にBufferedInputStreamオブジェクトをInputStreamから、BufferedOutputStreamオブジェクトをOutputStreamから、それぞれ生成します【4、5】。
また読み込みと書き込みを行うためのバッファ領域として、サイズ10のバイト配列bufを生成します【6】。
次に読み込みと書き込みの処理です。
while文によって、"java_logo1.jpg"ファイルからバッファ領域にデータを読み込むのは、前項の例と同様です【7】。
ループ処理の中では、BufferedOutputStreamのwrite()メソッドに、データが格納されたバッファ領域(変数buf)を渡すことで、出力ストリームへの書き込みを行います【8】。
そして最後に、BufferedOutputStreamのflush()メソッドを呼び出し、"java_logo2.jpg"ファイルへの書き込みを行います【9】。
このように入力ストリームと出力ストリームは、入力から出力へと接続して利用するケースがよくあります。

【図5-1-2】バイナリファイルの読み込みと書き込み
image.png

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. Java NIO.2によるファイル操作のためのクラス・インタフェースの全体像について。
  2. Pathsクラス、Pathインタフェース、Filesクラスの特徴やAPIについて。
  3. java.io.Readerやjava.io.Writerによるテキストデータの読み込みおよび書き込みの方法について。
  4. 入出力ストリームによるテキストデータの読み込みおよび書き込みの方法について。
  5. 入出力ストリームによるバイナリデータの読み込みおよび書き込みの方法について。

Discussion