第12章 入出力処理(OCJP-Bronze範囲外)

はじめに

プログラムは処理するデータを外部から貰う必要があります。またプログラムで処理した結果はどこかに保存しなければなりません。これが入出力処理です。これまでは入力はキーボード、出力はコンソールだけでしたが、この章からはファイルへの入出力を学習します。

またオブジェクトをファイルに記録する方法も学習します。文字や数値などではなく、オブジェクトそのものを処理対象として入出力できると、効率の良いプログラムを書けるようになります。

目次

  • ファイル入出力クラス
  • Fileクラスの操作
  • ファイル出力
  • ファイル入力
  • シリアライズとデシリアライズ
  • transient修飾子
  • 継承と直列化
  • 章のまとめ

目標

ファイルからデータを読み書きする方法を理解すること。またオブジェクト単位で入出力処理できるようにすること。

ファイル入出力クラス

コンピュータに接続する機器が多岐にわたるため、プログラミングでは入出力処理があります。

  • キーボードをタイプして入力し、処理結果をコンソールに出力する。
  • 記憶装置のファイルを読み書きする。
  • マウスやスキャナなど周辺機器からデータを入力する
  • ウェブなどから送信されてくるデータを受信し、応答を送信する。
  • データベースのレコードを読み書きする。

Java言語ではこれらの処理を行うクラスがJDKに付属して配布されています。
それらはいくつかのパッケージにまとめられています。

  • Java.ioパッケージ
  • Javax.servlet、java.netパッケージ
  • Java.sqlパッケージ

この章で扱う入出力は、記憶装置のファイルを読み書きする処理です。

ファイル入出力にはデータを単なる0と1の並びとして扱うバイトストリームと文字データとして扱う文字ストリームがあります。

なお、ストリームという呼び方は入出力を行う装置や仕組みに対して、データを時間の流れ(stream)と捉える考え方に由来します。

文字ストリーム

文字ストリームに関連するクラスは以下の6つのクラスです。

Fileの名前を扱うクラス

  • Fileクラス

Fileクラスはファイル名を表すクラスです。ファイル入出力を行うとき、「どのファイルか」を指定するのに使われます。また、Fileクラスのメソッドを使うとファイルの新規作成、名前変更、削除などができます。

File出力クラス

  • FileWriterクラス

ファイルへの書き込みメソッドを持つクラスです。writeメソッドを使って文字列をファイルに書き込むことができます。効率よく書き込みを行うためにBufferedWriterと組み合わせて利用します。

  • BufferedWriterクラス

書き込まれるデータをそのまま記憶装置に出力せず、メモリーに蓄えておいて一定量が溜まった時に一度にまとめて書き込むようにすることをバッファリングといいます。
記憶装置は動作の低速な機器ですからバッファリングによりアクセス回数を減らすことで効率良い書き込みができます。このクラスをFileWriteと組み合わせると、出力をバッファリングして効率の良いファイル出力を行うことができます。

  • PrintWriterクラス

高機能なメソッドを持つクラスです。FileWriter、BufferedWriterとともに併用し、出力処理の多くはPrintWriterで実行します。また、Java5から機能が拡充されPrintWriter単体でも使えるようになりました。

File入力クラス

  • FileReaderクラス

ファイルからの読み込みメソッドを持つクラスです。readメソッドを使って文字列をファイルから読み出すことができます。1文字単位での読み出しが基本の動作です。効率よく読み込みを行うためにBufferedReaderと組み合わせて使用します。

  • BufferedReaderクラス

ファイルから一度に大量のデータを読み込んでバッファに貯めておき、実際の読み込み処理に対してバッファからデータを返すことにより効率の良い読み込み処理を実現します。このクラスをFileReaderと組み合わせると、入力をバッファリングして効率の良いファイル入力を行うことができます。

Fileクラスの操作

ファイルクラスにはたくさんのメソッドがありますが、そのうち特に重要なメソッドは以下です。

クラス名 主なメソッド メソッドの機能
File mkdirs() ディレクトリを作る
File exists() ファイル・ディレクトリの存在をチェックする
File list() ディレクトリ内のファイル名のリストを返す
File isFile() ファイルかどうか調べる
File isDirectory() ディレクトリかどうか調べる

mkdirs(ディレクトリを作る)

新たにMkdirsExecクラスを作成し、実際に以下を入力してください。

項目 名前 備考
プロジェクト alj_study
パッケージ chapter12
クラス名 MkdirsExec
package chapter12;

import java.io.File;

public class MkdirsExec {

public static void main(String[] args) {

String FS = File.separator;

File f = new File("mydata" + FS + "sys");

//ディレクトリを作成する
boolean result = f.mkdirs();

//結果を表示
System.out.println("結果:" + result);

}
}

コンパイル例:

javac chapter12¥MkdirsExec.java

実行結果:

java chapter12.MkdirsExec
結果:true

mkdirsメソッドは、ディレクトリが存在していない場合には、ディレクトリを作成します。作成が成功すると true を返します。失敗すると falseを返します。

※ mkdirsのメソッドの詳細については API 仕様 をご確認ください。

仕様書(mkdirs)

File.separatorは、Windowsの場合は¥、LinuxやUnixの場合は/が代入されています。separatorを使っておくと、OSが違っていてもプログラムを書き換える必要がありません。

exists(ファイル・ディレクトリの存在をチェックする)

existsメソッドを使うとファイルやディレクトリが存在するかとうか検査できます。

新たにExistsExecクラスを作成し、実際に以下を入力してください。

項目 名前 備考
プロジェクト alj_study
パッケージ chapter12
クラス名 ExistsExec
package chapter12;

import java.io.File;

public class ExistsExec {

public static void main(String[] args){

String fname = "mydata";
File f = new File(fname);

//パスが存在するかどうか調べる
if(f.exists()){
System.out.println(fname + "は存在する。");
}else{
System.out.println(fname + "は存在しない。");
}

}

}

コンパイル例:

javac chapter12¥ExistsExec.java

実行結果:

java chapter12.ExistsExec
mydataは存在する。

existsメソッドはファイルやディレクトリの存在を検査するのに使います。パスは絶対パスを指定します。

※ existsメソッドの詳細については API 仕様 をご確認ください。

仕様書(exists)

list、isFile、isDirectory(ディレクトリの内容を表示する)

listメソッドを使うと、ディレクトリの中にある全てのファイル名とディレクトリ名をStringの配列として取得することができます。ただし、ファイル名とディレクトリ名が混在している場合は、どれがファイル名でどれがディレクトリ名かを区別する必要があります。isFileメソッドを使うとファイルがどうか判定できます。またisDirectoryメソッドを使うとディレクトリかどうか判定できます

新たにFileListExecクラスを作成し、実際に以下を入力してください。

項目 名前 備考
プロジェクト alj_study
パッケージ chapter12
クラス名 FileListExec
package chapter12;

import java.io.File;

public class FileListExec {

public static void main(String[] args) {

String dir = args[0];
File f = new File(dir);

//名前のリストを取得する
String[] list = f.list();

//dirがディレクトリでなければlistはnullなのでチェックする
if(list != null){

//全ての要素をチェックする
for(String name : list){

File t = new File(dir + File.separator + name);
//ファイル名だけを表示する
if(t.isFile()){
System.out.println(name);
}
}
}else{
System.out.println( dir + "は存在しないかディレクトリではない");
}

}
}

コンパイル例:

javac chapter12¥FileListExec.java

実行結果:

java chapter12.FileListExec mydata

mydataを引数に指定すると、mydataディレクトリ配下にあるファイル一覧が表示されます。
mydataディレクトリ配下には、ファイルが存在しない為、何も表示はされません。

実行結果:

java chapter12.FileListExec chapter12
FileListExec.java
ExecMkdirs.java
ExistsExec.class
ExistsExec.java
FileListExec.class
ExecMkdirs.class

chapter12を引数に指定すると、chapter12ディレクトリの配下にあるファイルが一覧で表示されます。

※ listメソッド、isFileメソッド、isDirectoryメソッドの詳細については API 仕様 をご確認ください。

仕様書(list)

仕様書(isFile)

仕様書(isDirectory)

ファイル出力

ファイルへ文字列を出力するにはFileWriter、BufferedWriter、PrintWriterを使います。

ラップ

ファイルへ文字を出力するとき、直接的にはBufferedWriterを使います。それはバッファリング機能で効率よく出力でき、1行分ずつ出力できるメソッドがあるなど使いやすいからです。しかし、BufferedWriterにはファイルへ文字を書く能力はなく、その部分はFileWriterに処理を依頼します。バッファにデータが溜まったところを見計らい、FileWriterに送って一度出力します。

この関係を概念的に表したのが以下の図です。

オブジェクトのレベルで考えると、この関係は次のようになります。BufferedWriterはフィールドにFileWriterの参照を持ちます。これにより、必要な時、FileWriterの出力メソッドを使うことができます。

このように、他のオブジェクトの参照を内部に持つことをHAS-A関係といいます。

実際のプログラムでは以下のようにコーディングします。

FileWriter     fw = new FileWriter("test.txt");
BufferedWriter bw = new BufferedWriter(fw);

上記の書き方は、以下のように1行に短縮することが可能です。

BufferedWriter bw = ew BufferedWriter(new FileWriter("test.txt"));

上記の書き方はBufferedWriterがFileWriterを包み込むように見えます。そのためこれをBufferedWriterで FileWriterFイェWリテrをラップするといいます。

しかし、文字列出力の場合、最も使い勝手の良いのはPrintWriterです。PrintWriterは書式付出力やテキストの行単位の処理など、高機能なメソッドを持つクラスです。使い方としては以下のようにBufferedWriterをさらに以下のようにラップします。

PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter("test.txt")));

新たにPrintWriterExecクラスを作成し、実行結果を確認して下さい。

項目 名前 備考
プロジェクト alj_study
パッケージ chapter12
クラス名 PrintWriterExec
package chapter12;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class PrintWriterExec {

public static void main(String[] args) {
try {
//ファイルを開く
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter("test.txt")));
//行単位でデータを出力する
pw.println("ALJ Education Plus 株式会社");
pw.println("テスト");
//ファイルを閉じる
pw.close();
} catch (IOException e) {
System.err.println("ファイルを作成できない");
}
}

}

test.txtは以下の手順で作成してください。

プロジェクトで右クリックします。そして [新規]-[表題なしのテキストファイル] を選択します。

ファイルを保存します。[command] + [s] を押し、ファイル名を「test.txt」にします。

出力結果

入出力処理は一般にチェック例外を発生します。そのためtry-catchで囲まないとコンパイルエラーになります。多くの場合発生する例外はIOExceptionです。

「ファイルを開く」というのは、データを書き込むための初期化作業にあたります。PrintWriterオブジェクトを作成することが「ファイルを開く」ことです。この時、指定されたファイルが存在しなければ自動的に作成します。

出力にはprintlnやprintメソッドを使用します。上記の場合pw.println("ALJ Education Plus 株式会社");
pw.println("テスト");で2行、test.txtファイルへ出力しています。

最後にcloseメソッドを忘れないようにしてください。ファイルへの出力はバッファリングされているので、closeしないとメモリー上のバッファにデータが残ったままになります。また、closeはファイル出力のために確保したバッファメモリなどを解放します。システムの資源を解放するという点からも必ず実行してください。

ファイル入力

ファイルからデータを読み出すには、FileReaderBufferedReaderを使います。次のようにFileReaderをBufferedReaderでラップします。

BufferedReader br = new BufferedReader(new FileReader("test.txt"))

読み込みは以下のようにreadLineメソッドを使って一行単位で読み出します。データを最後まで読み出してしまうとreadLineメソッドはnullを返すので次のようなwhile文を使います。

String str;
//strがnullでなければ繰り返す
while((str = br.readLine()) != null){
//処理
}

新たにBufferedReaderExecクラスを作成し、実際に以下を入力してください。

※test.txtはPrintWriterExecクラスを実行した際に使用したものを使います。

package chapter12;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class BufferedReaderExec {
public static void main(String[] args) {
try {
//ファイルを開く
BufferedReader br = new BufferedReader(new FileReader("test.txt"));
//行単位でデータを読み出す
String str;
while( (str=br.readLine()) != null){
System.out.println(str);
}
//ファイルを閉じる
br.close();
} catch (FileNotFoundException e) {
System.err.println("ファイルを開けない");
} catch (IOException e){
System.err.println("データを読み出せない");
}
}
}

確認結果

上記の例は典型的な読み込みのパターンです。1行分ずつデータを読み取り、それをコンソールに表示する処理を繰り返します。readLineがnullを返すとそれがデータの最後を示します。

シリアライズとデシリアライズ

オブジェクトをファイルなどに出力して保存することを直列化(シリアライズ)といいます。また、シリアライズで記録したファイルの内容を読み取ってオブジェクトを元どおり復元することを直列化復元(デシリアライズ)といいます。

直列化可能なクラスはSerializableインターフェースを実装するルールになっています。インターフェースの内容は以下です。

public interface Serializable{}

Serializableインターフェースの中身は何も定義されていません。これは直列化可能であることを宣言するために使われています。このようなインターフェースをマーカーインターフェースといいます。

直列化と直列化復元を行うクラスはObjectInputStreamクラスObjectOutputStreamクラスです。FileInputStreamFileOutputStreamをこれらでラップして使用します。

クラス構成図は以下です。

シリアライズ

まずは実際にオブジェクトを作成し、ファイルを保存してみましょう。

新たにObjectBookクラスとObjectBookWriterExecクラスを作成してください。

ObjectBook.java

package chapter12;

import java.io.Serializable;

public class ObjectBook implements Serializable {
//シリアルバーション番号付与
private static final long serialVersionUID = 1L;
//フィールド変数
private String subject;
private String author;
private int price;
//コンストラクタ
ObjectBook(String subject,String author, int price){
this.subject = subject;
this.author = author;
this.price = price;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}

ObjectBookWriterExec.java

package chapter12;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class ObjectBookWriterExec {

public static void main(String[] args) {
//直列化するオブジェクトを作成
ObjectBook ob = new ObjectBook("Java","ALJ",1000);
try {
//ファイルを開く
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.txt"));
//オブジェクトを出力する
out.writeObject(ob);
//ファイルを閉じる
out.close();
} catch (FileNotFoundException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
} catch (IOException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
}
}
}

出力結果

直列化するクラス(ObjectBookクラス)には通常、シリアルバーション番号をつけます。常に以下のように宣言してください。

private static final long serialVersionUID = 1L;

シリアルバーション番号は保存時と読み出し時に比較され、違う番号になっていると互換性がないと判断され、例外が発生します。

※シリアルバーション番号が宣言されないとコンパイラが適当につけてくれますが、ロジックを変えてしまうだけでも新たに別の番号が付与されてしまうので、明示的に付与するしてください。

ObjectBookWriterExecクラスの以下の箇所は、典型的なオブジェクト出力処理の書き方です。

//ファイルを開く
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.txt"));
//オブジェクトを出力する
out.writeObject(ob);
//ファイルを閉じる
out.close();

上記のように、FileOutputStreamをObjectOutputStreamでラップします。引数に出力ファイルを指定します。
writeObjectメソッドでオブジェクトを出力します。引数にオブジェクトの参照を指定します。そしてcloseメソッドでファイルを閉じます。

デシリアライズ

次はファイルに保存したオブジェクトを読み出して復元する例です。

ObjectBookReadExecクラスを作成してください。

ObjectBookReadExec.java

package chapter12;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ObjectBookReadExec {

public static void main(String[] args) {
ObjectInputStream in;
try {
in = new ObjectInputStream(new FileInputStream("book.txt"));
ObjectBook ob = (ObjectBook)(in.readObject());
System.out.println("名前=" + ob.getSubject());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e){
e.printStackTrace();
}
}

}

以下が復元処理(直列化復元、デシリアライズ)です。

in = new ObjectInputStream(new FileInputStream("book.txt"));
ObjectBook ob = (ObjectBook)(in.readObject());

FileInputStreamObjectInputStreamでラップして使用しています。

次に、readObjectメソッドでオブジェクトを読み出します。このメソッドの戻り値型はObject型なのでObjectBook型の参照にキャストしています。

実行すると以下のようにコンソールへ出力されます。

出力結果

transient修飾子

全てのクラスが直列化できるわけではありません。何らかの理由で直列化できないクラスの参照をフィールドとして持つクラスは、そのままでは直列化できないことになります。

そこで、「このフィールドは直列化しなくて良い」と宣言するのがtransient修飾子です。

transient修飾子をつけた変数は、直列化から除外されます。これによりクラスの残りの部分を直列化することが可能です。直列化から除外されたフィールドは、かなのツヨゥレツ復元時に既定値で初期化されます。コンストラクタでの初期化は実行されないので注意してください。

先ほど作成したObjectBookクラスのフィールド変数「subject」にtransientを付与してください。

package chapter12;

import java.io.Serializable;

public class ObjectBook implements Serializable {
//バーション番号
private static final long serialVersionUID = 1L;
//フィールド変数
transient private String subject; //transient修飾子をつける
private String author;
private int price;
//コンストラクタ
ObjectBook(String subject,String author, int price){
this.subject = subject;
this.author = author;
this.price = price;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}

先ほど作成したObjectBookWriterExecクラスを実行してみてください。

package chapter12;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class ObjectBookWriterExec {

public static void main(String[] args) {
//直列化するオブジェクトを作成
ObjectBook ob = new ObjectBook("Java","ALJ",1000);
try {
//ファイルを開く
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.txt"));
//オブジェクトを出力する
out.writeObject(ob);
//ファイルを閉じる
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

そしてObjectBookReadExecクラスを実行してみてください。

package chapter12;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ObjectBookReadExec {

public static void main(String[] args) {
ObjectInputStream in;
try {
in = new ObjectInputStream(new FileInputStream("book.txt"));
ObjectBook ob = (ObjectBook)(in.readObject());
System.out.println("名前=" + ob.getSubject());
System.out.println("名前=" + ob.getAuthor());
System.out.println("名前=" + ob.getPrice());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e){
e.printStackTrace();
}
}

}

実行結果

継承と直列化

親クラスがSerializableインターフェースを実装していれば、サブクラスは特に実装しなくても暗黙のうちにSerializableインターフェースを実装していることになります。

一方、サブクラスだけがSerializableインターフェースを実装し、スーパークラスが実装していない場合でも、サブクラスを直列化することができます。ただ、直列化復元時に、スーパークラスから引き継いだフィールドは、全てコンストラクタによりデフォルトに初期化されます。

直列化復元では通常はコンストラクタは起動しません。しかし、Serializableを実装していない親クラスから引き継いだ部分についてはコンストラクタが起動し、初期化します。

Parent.java

package chapter12;
//Serializableを実装していないスーパークラス
public class Parent {
int i;
Parent(){
this.i = 100; //初期値
}
}

Child.java

package chapter12;

import java.io.Serializable;

public class Child extends Parent implements Serializable{

private static final long serialVersionUID = 1L;

String name ;
public Child(int i , String name){
this.i = i;
this.name = name;
}
}

package chapter12;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ReadExec {

public static void main(String[] args) {
try {

//直列化
Child ch = new Child(20,"山田");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("child.txt"));
out.writeObject(ch);
out.close();

//直列化復元
ObjectInputStream in = new ObjectInputStream(new FileInputStream("child.txt"));
Child c = (Child)in.readObject();
System.out.println("i=" + c.i);
System.out.println("name=" + c.name);
in.close();

} catch (Exception e) {
e.printStackTrace();
}
}
}

実行結果

実行結果を見るとわかるように、サブクラスのnameは復元されて山田となっていますが、スーパークラスから引き継いだiはコンストラクタで初期化された20ではなく、100に戻っていることがわかります。

なお、staticをつけて宣言したクラス変数は、オブジェクトの中に含まれていない為、直列化の対象とはなりません。

章のまとめ

以下の要点をしっかり理解してから次の章へ進んでください。

  • Fileクラスはファイル名を表すクラスである。
  • ファイル入出力にはバイトストリーム文字ストリームがある。
  • 入力処理に使うのはFileReaderBufferedReaderである。
  • 実際に入力処理を行う際は、FileReaderをBufferedReaderでラップする。
  • BufferedReaderクラスではreadLineメソッドで一行単位に読み出す。
  • 出力処理に使うのはFileWriterBufferedWriterPrintWriterである。
  • 実際に出力処理を行う際は、FileWriterをBufferedWriterでラップし、さらにPrintWriterラップする。
  • PrintWriterクラスはprintlnメソッドprintメソッドで出力することができる。
  • ディレクトリを作るときは、Fileクラスのメソッドmkdirs()を使う。
  • ファイル・ディレクトリの存在をチェックするときは、Fileクラスのメソッドexists()を使う。
  • ディレクトリ内のファイル名のリストを返すときは、Fileクラスのメソッドlist()を使う。
  • ファイルかどうか調べるときは、FileクラスのメソッドisFile()を使う。
  • ディレクトリかどうか調べるときは、FileクラスのメソッドisDirectory()を使う。
  • オブジェクトを復元可能な状態でファイルに出力することを直列化(シリアライズ)という。
  • 直列化したファイルからオブジェクトを復元することを直列化復元(デシリアライズ)という。
  • 直列化するクラスはSerializableインターフェースをimplementsしなければならない。
  • 直列化するためには、まずはFileOutputStreamObjectOutputStreamでラップする。そしてObjectOutputStreamクラスのwriteObjectメソッドを使用して直列化を行う。
  • 直列化復元するためには、まずはFileInputStreamObjectInputStreamでラップする。そしてObjectInputStreamクラスのreadObjectメソッドを使用して直列化復元を行う。
  • transient修飾子をつけることでメンバーを直列化の対象からはずすことができる。
  • スーパークラスがSerializableインターフェースを実装していればサブクラスSerializableインターフェースを実装していることになる。
  • Serializableインターフェースを実装しないスーパークラスから継承したフィールドは直列化されない
  • Serializableインターフェースを実装しないスーパークラスから継承したフィールドは復元時にコンストラクタで初期化される。
  • staticをつけて宣言したクラス変数は、直列化の対象とはならない。