GO BACK

初心者のためのJava教室 その10

「ファイルを読み書きする」

■テキストファイルを読み込む

 プログラミングで一番面倒なのが「ファイルの操作」でしょう。それでもVisual Basicみたいなものでは、まあファイルを開いて読み書きして閉じて…という具合に基本さえわかればなんとかなります。が、Javaの場合、基本的な概念からしっかり理解しないとなかなかうまく使えるようになりません。ちょっと今回はハードかも知れないので覚悟してくださいね。

 Javaでは、外部とデータをやり取りするときには「ストリーム」と呼ばれるものを使います。ストリームとは、Java仮想マシンと外部とをつなぐパイプのようなもの、と考えればいいでしょうか。ファイルだろうがネットワーク経由でデータをやり取りする場合だろうがプリンタに印刷する時だろうが、とにかく「他とやりとりする」という時は全てストリームを使います。

 問題は、やり取りする手段やデータの種類などによってストリームがたくさん用意されているってことです。これは、使いこなせるようになれば大変便利なのですが、最初の内は「一体どれをどう使えばいいのだ?」と混乱してしまうでしょう。ということで、今回はまず「テキストファイルを読み書きする」というのに必要なものだけ説明します。

 テキスト関係の読み書きは「リーダー(Reader)」と「ライター(Writer)」と呼ばれる特別なストリームを使います。この中でも、基本のリーダー/ライター、ストリーム付きのものなど何種類かがあります。で、ぶっちゃけた話、「どれが正解」ってのがないんですね。要するに「Aを使ってもできるし、Bを使ってもできる」という感じなのです。Aは原始的だが細かい処理ができる、Bは高度だが大雑把、みたいな感じなので、人によって「オレはAを使ったほうがやりやすい」なんて具合にけっこうやり方に違いがあったりします。

 まあ、ここでは一番基本となるものだけ説明しておくことにします。基本部分だけでも十分難しいので(笑)。

import java.awt.*;
import java.awt.event.*;
import java.io.*;


public class Test10 extends Frame implements ActionListener {
	TextArea ta;
	Button b1,b2;
	
	public Test10() {
		super();
		setTitle("Hello");
		setSize(300,250);
		setLayout(null);
		
		ta = new TextArea("",5,10,TextArea.SCROLLBARS_VERTICAL_ONLY);
		ta.setBounds(25,25,250,150);
		this.add(ta);
		
		b1 = new Button("Read");
		b1.setBounds(25,200,100,25);
		b1.addActionListener(this);
		this.add(b1);
		
		b2 = new Button("Write");
		b2.setBounds(175,200,100,25);
		b2.addActionListener(this);
		this.add(b2);
	}
	
	 public static void main (String args []) {
		new Test10().show();
	}
	
	public void actionPerformed(ActionEvent ev) {
		if (ev.getSource() == b1) {
			this.readTextFromFile();
		}
		if (ev.getSource() == b2) {
			this.writeTextToFile();
		}
	}
	
	void readTextFromFile() {
		try {
			FileDialog fd = new FileDialog(this,"Select Text File.",FileDialog.LOAD);
			fd.setVisible(true);
			String fname = fd.getDirectory() + fd.getFile();
			FileReader fr = new FileReader(fname);
			BufferedReader br = new BufferedReader(fr);
			String rdata;
			String alldata = "";
			while((rdata = br.readLine()) != null) {
				alldata += rdata;
			}
			fr.close();
			ta.setText(alldata);
		} catch(Exception e) {
			System.out.println(e);
		}
	}
	
	void writeTextToFile() {
		try {
			FileDialog fd = new FileDialog(this,"Select Text File.",FileDialog.SAVE);
			fd.setVisible(true);
			String fname = fd.getDirectory() + fd.getFile();
			FileWriter fw = new FileWriter(fname);
			fw.write(ta.getText());
			fw.close();
		} catch(Exception e) {
			System.out.println(e);
		}
	}
}

 …なにやら物騒なソースコードになってきましたねえ(笑)。これは、テキストファイルを読み書きするプログラムです。「Read」ボタンをクリックするとファイル読み込みのダイアログが現れ、ここでファイルを選択するるとそのファイルが読み込まれてTextAreaに表示されます。また「Write」ボタンを押すとファイル保存のダイアログが現れ、TextAreaのテキストをファイルに保存します。どちらも、テキストファイル読み書きのもっともシンプルな部分だけを実装したものと考えて下さい。


■ストリーム以外の新しい知識

 テキストファイルの読み書きには、いくつか覚えておかないといけない知識があります。まず、最初のimport文を見て下さい。「import java.io.*;」というのが追加されていますね? これは、I/O関係(要するにデータのやり取り)の機能がまとめられたライブラリです。ストリーム関係もここにあるので、ファイルを扱う時はこれをimportするのを忘れないようにしましょう。

 今回は、ストリーム以外にもいろいろと細かい新知識が登場します。それらについてまず説明しておきましょう。――まず、このTest10クラスの仕組みからです。今まで、ボタンをクリックして処理をする時は、ActionListenerのクラスを作って組み込んでいましたね。今回はちょっと面白いやり方をしてます。それは「Test10自身をActionListenerにしてしまう」というものです。

 リスナークラスは、imeplementsでインターフェイスを組み込めば作れました。で、implementsはextendsと一緒に使うことができました。だったら、わざわざ別にクラスを作らず、このTest10自身にimplements ActionListenerしたっていいじゃないですか? 要するに、implementsしておいて、actionPerformedメソッドが用意してあればいいわけですよね?

 全くその通りで、実は別にクラスを作らなくても、この例のように1つのクラスでリスナーを兼ねてしまうことも可能なんですね。「じゃあなんで最初からそうしなかったんだ!」と思うかも知れませんが、やっぱり2つを分けてそれぞれの役割を理解して、その上で「2つを1つにまとめることもできます」ということを覚えたほうがよいと思ったのでそうしました。

 このクラス自身をリスナーにしているってことは、自分自身をリスナーとして部品に組み込むことになります。コンストラクタ部分を見てみると、「b1.addActionListener(this);」なんて具合に書いてありますね? thisをリスナーとして組み込んでいることがわかるでしょう。

 さて、次に進みましょう。actionPerformedではどんなことをやってるでしょうか。――見てみると、getSourceを使ってどっちのボタンをクリックしたかチェックして、それから「this.readTextFromFile();」とか「this.writeTextToFile();」ってものを呼び出してます。これって、どういうものでしょう?

 実は、これは、このクラス自身に自作して組み込んだメソッドなんですね。今まで、メソッドといえば「クラスに用意されているもの」という感じがしてましたが、もちろん自分で新しいメソッドを組み込むことだってできるわけです。今回はその例として、ファイルの読み込みと書き出しをそれぞれ独立したメソッドにして、actionPerformedでは単にそれらを呼び出すだけにしておきました。

 メソッドを作成すると、機能を1まとめにして整理することができます。例えば、actionPerformedの中にファイルの読み込みと書き出しと全部がずら〜〜って書いてあるより、それぞれがメソッドとして用意してあったほうがわかりやすいでしょう? 後で「読み込みの処理を少し変えよう」と思ったらそのメソッドだけを書き換えればいいのですから。長いactionPerformedから「どこが読み込みの部分だったっけな?」などと調べて修正するよりはるかにわかりやすいですね。

 さて、ではストリームに…と進む前に、もう1つ覚えておくことがありました。それは「ファイルダイアログ」です。今回はファイルの読み書きをしますから、やっぱりファイル選択のダイアログを使って処理をしたいですね。

 ファイルダイアログは、AWTに「FileDialog」っていうクラスとして用意されています。これは、単純にパラメータも何もなしでnewすることもできるんですが、普通はこんな感じで使う方が多いでしょう。

new FileDialog(親フレーム, タイトルのテキスト, モード)

 「親フレーム」っていうのは、このダイアログがどのFrame(というかウィンドウ)から呼び出されたものかを指定します。こうしたダイアログというのは、あるウィンドウから一時的に呼び出されるという感じのものですから、「属するもの」を指定する必要があるのですね。また、モードというのは「オープンダイアログか、セーブダイアログか」を指定するものです。これはFileDialogにある「LOAD」「SAVE」という定数を使って指定します。FileDialog.LOADなんて感じで書けばいいわけですね。


■ストリームとReader/Writer

 では、いよいよストリームについて説明していきましょう。まずは、メソッドの並び順は逆になってしまいますが、書き込みを行なう「writeTextToFile」メソッドから見ていきましょう。

	void writeTextToFile() {
		try {
			FileDialog fd = new FileDialog(this,"Select Text File.",FileDialog.SAVE);
			fd.setVisible(true);
			String fname = fd.getDirectory() + fd.getFile();
			FileWriter fw = new FileWriter(fname);
			fw.write(ta.getText());
			fw.close();
		} catch(Exception e) {
			System.out.println(e);
		}
	}

 こんな感じになっています。最初にtryなんてものがありますね? これは「エラー処理のための構文」と考えて下さい。――ファイル処理では、実行した命令が必ずしも期待通りの結果を出すとは限りません。ファイルを開こうとしたらファイルがなかった、データを読み込んだらデータがなかった、ファイルが破損していてうまく開けなかった…なんてことはいかにもありそうなことです。そこで、ファイル関係を扱う場合は、「エラーが起きたらこうして」ということを構文で指定しておくのです。

try {


	……ここにエラーが起きるかも知れないという処理を書く……


} catch(Exception e) {


	……ここに、エラーが起きた時にどうするかを書く……


}

 これが、「try構文」と呼ばれるエラー処理用構文の基本形です。tryの後にある{}部分に実行する処理を書いておくわけですね。で、そこでエラーが起きたらcatch以降の{}にジャンプするわけです。catchのパラメータにある (Exception e) ってのは、まあ「そう書くのだ」と思ってしまって下さい。実をいえば、これはもっと細かにいろいろ書き方があったりするのだけど、とりあえず「こう書けばエラー処理はできる」と覚えてしまって、今のところは問題ありません。

 さて、try内で行なっていることに進みます。まず、FileDialogを作って、保存するファイル名を入力してもらってますね。FileDialogは、インスタンスを作った後、「setVisible(true)」というので画面に表示をします。setVisibleってのは、部品の表示/非表示を設定するものです。

 FileDialogは、一度画面に表示されると、ボタンを押してダイアログが消えるまで、ずっとプログラムの処理は停止したままになります。つまり、setVisible(true)とすると、その次にある行は、ダイアログが消えてから実行されるのです。

 で、ダイアログが消えると、このFileDialogインスタンスには入力したファイル名と選択されたディレクトリなどの情報がセットされます。これらをメソッドで取り出してやれば、どの場所になんというファイル名で保存しようとしたかがわかるってわけです。

String fname = fd.getDirectory() + fd.getFile();

 それを行なっているのがこの部分です。「getDirectory()」が選択された場所のパスを、「getFile()が入力されたファイル名を、それぞれStringで返します。ですから、この2つを1まとめにすれば、保存するファイルのフルパスが得られるというわけです。もしキャンセルをした場合には、これらの値はnullになります。ここではキャンセル時の処理は省略していますので、余力があればこれらがnullでないときだけ処理を実行するように改良してみるとよいでしょう。

 さて、いよいよ、今度こそ本当に、ストリームの出番です。テキストファイルの書き出しを行なうには、「FileWriter」というストリームを使います。これはライターというものの一種で、文字どおりファイルに保存する時の基本となるクラスです。このFileWriterは、パラメータに、保存するファイルを示すStringを指定します。単にファイル名だけならカレントディレクトリ(要するにクラスファイルがある場所)にあるその名前のファイルに保存しますし、他の場所に保存したければフルパスで指定します。

 ファイルへの書き込みは「write()」というメソッドを実行します。これは、書き込むテキストをパラメータに指定するだけです。意外と簡単ですね。

 そして書き込みが終わったら、「close()」というメソッドでストリームを閉じてやります。これを忘れると、ファイルが「使用中」の状態のままとなりますから注意して下さい。

 というわけで、整理するとファイルへの書き込みは、

1.new FileWriter(ファイル名) でFileWriterインスタンスを作る。
2.《FileWriter》 .write(テキスト) でテキストを書き込む。
3. 《FileWriter》 .close() でストリームを閉じる。

――以上のような手順で行える、というわけです。わかってしまえば、意外に簡単そうでしょう?

 では、引き続いて読み込みについて説明しましょう。こちらは「readTextFromFile」というメソッドで行なっています。基本的には書き出しと同じなんですが、実はちょっとややこしいんです。

	void readTextFromFile() {
		try {
			FileDialog fd = new FileDialog(this,"Select Text File.",FileDialog.LOAD);
			fd.setVisible(true);
			String fname = fd.getDirectory() + fd.getFile();
			FileReader fr = new FileReader(fname);
			BufferedReader br = new BufferedReader(fr);
			String rdata;
			String alldata = "";
			while((rdata = br.readLine()) != null) {
				alldata += rdata;
			}
			fr.close();
			ta.setText(alldata);
		} catch(Exception e) {
			System.out.println(e);
		}
	}

 これが読み込みです。try構文をまず用意して、最初にFileDialogを使ってファイルのパスと名前を取り出すところまではほぼ同じです。が、そこから先が違います。

 ファイルの読み込みは「Reader」というリーダーを使います。が、このReader、読み込むのがちょっと厄介なんです。読み込むための「read」というのは用意してありますが、これは1文字(charという値)しか読めません。まとめて読み込むものもあるんですが、それは「char配列」っていうものの値で取り出されるようになっていて「何文字分読め」って感じで読んでいかないといけないのです。

 考えてみれば、書き出す時は、書くデータは最初から全部わかってます。けれど読む時は、そのファイルに何文字データがあるか、何行あるかなんてことはわかりません。どれだけデータがあるかわからない状態で、出たとこ勝負で読んでいくわけです。

 そこでテキストを読み込むときは、もう少し賢いリーダーとして用意されている「BufferedReader」というものを使うのが一般的です。これはパラメータにFileReaderを指定して作ります。このBufferedReaderを使うと何が便利なのかというと、1行ずつStringでテキストを読むことができるのです。「readLine()」というメソッドがそれで、これを呼び出すことで1行のテキストを取り出すことができます。

 そして、もし最後まで読んで、更に次の行を読もうとした時には、readLineの値はnullになります。つまり、繰り返し構文などを使って次々とreadLineしていき、その値がnullになったら繰り返しを抜け出るようにすれば、全てのテキストが取りだせるというわけなのです。

			while((rdata = br.readLine()) != null) {
				alldata += rdata;
			}

 ここでは、繰り返し部分がこんな具合になってますね。whileというのは、その後の()部分がtrueである間、繰り返しを続けるというものです。では、()部分はどうなってるんでしょう? まず「rdata = br.readLine()」というのがありますね? で、それが != null かどうか?っていうのを調べてます。ちょっとわかりにくいですが、これで「rdata = br.readLine()を実行し、その結果がnullでないかどうかチェックする」ということをまとめて行なっているんですね。

 ファイルの読み込みではこんな書き方が多用されるということで使ってみました。まあ、これではちょっとわかりにくいので、「もうちょっと簡単にしたい」と思うなら以下のような感じにしてもよいでしょう。

			while(true) {
				rdata = br.readLine();
				if (rdata == null) {
					break;
				}
				alldata += rdata;
			}

 while()の部分にはtrueってのがありますね? こうしておくと「ずーっと永遠にくり返す」っていう繰り返しができます。で、読み込んだところで if (rdata == null) というのでチェックをし、もしnullだったら「break」を実行します。これは、構文を途中で終了し抜け出すものです。これで先の繰り返しと同じ処理ができます。

 ところで、このメソッド、実際に使ってみるとちょっと変なことに気がつくでしょう。それは「Readボタンで読み込むと、改行してあるテキストが全部つながって出てくる」ということです。

 ここで使っているreadLine()というのは、1行ずつテキストを読み込むんですが、最後についている(はず)の改行コードを取り除いてくれるんです。このため、「alldata += rdata;」とすると、読み込んだ行が全部つながっちゃうんですね。もし「そんなのはイヤだ」という人は、この行の後に、

alldata += "\n";

――この文を追加して下さい。"\n"ってのは、Characterクラスにある、改行の制御コードです。これを追加してやれば、ちゃんと改行された状態になります。


■イメージファイルを読み込む

 テキストがわかったら、次はグラフィックのイメージをファイルから読み込んでみましょう。Javaでは、jpegやGIFのグラフィックを読み込んで使うことができます。まあ、グラフィックファイルの保存となるとエンコードなどが絡んできてちょっとややこしいんですが、読み込むだけなら私たちにも使えそうですから、一緒に覚えておきましょう。

import java.awt.*;
import java.awt.event.*;
import java.io.*;


public class Test11 extends Frame implements ActionListener {
	MyCanvas c1;
	Button b1;
	Image img;
		
	public Test11() {
		super();
		setTitle("Hello");
		setSize(300,250);
		setLayout(null);
		
		c1 = new MyCanvas();
		c1.setBounds(25,25,250,150);
		this.add(c1);
		
		b1 = new Button("Read");
		b1.setBounds(25,200,100,25);
		b1.addActionListener(this);
		this.add(b1);
	}
	
	 public static void main (String args []) {
		new Test11().show();
	}
	
	public void actionPerformed(ActionEvent ev) {
		if (ev.getSource() == b1) {
			this.readImageFromFile();
		}
	}
	
	void readImageFromFile() {
		try {
			FileDialog fd = new FileDialog(this,"Select Image File.",FileDialog.LOAD);
			fd.setVisible(true);
			String fname = fd.getDirectory() + fd.getFile();
			Toolkit tk = Toolkit.getDefaultToolkit();
			img = tk.getImage(fname);
			c1.repaint();
		} catch(Exception e) {
			System.out.println(e);
		}
	}
	
	class MyCanvas extends Canvas {
	
		public void paint(Graphics g) {
			if (img != null) {
				g.drawImage(img,0,0,this);
			}
		}
	}
}

 これが、そのサンプルです。「Read」ボタンを押してグラフィックファイルを選ぶと、それを読み込んで表示します。なかなかいい感じでしょう?

 Javaでは、イメージ(グラフィックのデータ)は「Image」というクラスとして用意されています。ファイルからグラフィックを読み込むと、このImageのインスタンスとしてJavaの中では扱われるようになります。

 そしてGraphicsには、このImageを描画するためのdrawImageというメソッドが用意されています。そこで、基本的な考え方としてはこうなるわけです。

1.まず、Canvasを継承したクラスを作る。
2. その中のpaintメソッドで、Imageが存在したらそれをdrawImageで描画するようにしておく。
3.メインのクラスでは、ボタンを押したらファイルからImageを読み込み、Canvasを再描画するような処理を用意しておく。

 これで、おそらく問題ないように思えますね。Canvas継承クラスは、ここではMyCanvasとしてメインのクラスの内部クラスとして用意します。そして、Imageをクラス全体で使えるようにしておきます。 そうすれば、内部クラスのMyCanvasのpaintメソッドでも、メインクラス側にあるImageが使えるようになりますからね。

 今回は、イメージファイルの読み込みは「readImageFromFile」というメソッドで行なっています。では、tryの中で何をやっているか見てみましょう。

			FileDialog fd = new FileDialog(this,"Select Image File.",FileDialog.LOAD);
			fd.setVisible(true);
			String fname = fd.getDirectory() + fd.getFile();
			Toolkit tk = Toolkit.getDefaultToolkit();
			img = tk.getImage(fname);
			c1.repaint();

 最初の部分はわかりますね。FileDialogでファイルのパスと名前を取り出しています。問題はその後です。「Toolkit tk = Toolkit.getDefaultToolkit();」というのが実行されていますね。これは何でしょう?

 このToolkitは「ツールキット」と呼ばれるもので、ネイティブな環境(要するにJava仮想マシンの外側)とのやり取りをするユーティリティ的なクラスと考えて下さい。ここに、ネイティブ環境のためのメソッドがいくつか用意されているんですね。イメージファイルの読み込みメソッドも、この中にあります。

 このツールキットを使うには、まず「getDefaultToolkit()」というものでデフォルトのツールキットを取り出します。そして、その中の「getImage()」というので、イメージをImageに読み込みます。

 このツールキットというのはちょっと抽象的な概念でややわかりにくいので、とりあえずは「getDefaultToolkitしてからgetImageすればイメージが取りだせる」とだけ覚えちゃって下さい。

 そして、最後に「repaint」というものでMyCanvasを再描画しています。AWTのコンポーネントというのは、3段階で描画が行なわれます。それは、以下のような感じです。

repaint()
 ↓
update(Graphics g)
 ↓
paint(Graphics g)

 最初のrepaintは、「再描画開始の命令」用のものと考えて下さい。これが呼び出されると、Javaは可能な限りすみやかにupdateを呼び出します。

 updateは、コンポーネントの表示部分をきれいに背景色で塗りつぶして消し、それからコンポーネントに必要な表示を描画していきます。そして全て描画し終えたところでpaintメソッドを呼び出します。

 つまり、コンポーネントを再描画させたいと思ったら、単にpaintメソッドを呼び出すだけではダメで、repaintを呼び出したほうがよい、ということです。

 …というわけで、今回はテキストとイメージのファイルを扱う方法について説明しました。どちらも、ストリームだのツールキットだのややこしそうなのが登場するので、とりあえずあんまり深く考えず、何度もコードを書いて動かして基本的な書き方だけ覚えちゃうようにしましょう。 ややこしい概念とかはそれから少しずつ勉強すれば大丈夫ですから。



GO NEXT


GO HOME