2016年度 発展プログラミング演習 第9講 Findプログラム 3日目
本日の内容
7. コマンドライン引数でファイルの中身を検索する文字列を受け取れるようにする.
3-1. コマンドライン引数を解析するで実現している内容ですが, まだできていない場合は,前回のページを参考に実現しましょう.
8. ファイルの中身を指定された文字列で検索できるようにする.
8-1. ファイルを開く.
8-1-1. ストリーム
Javaで入出力を扱うには,ストリームという概念の理解が必要です. ストリームとは,データを流れとして扱う仕組みです. データがデータソース(Data Source; データの源,入力元)から流れてくる入力ストリームと, データをデータの出力先に流し込む出力ストリームの2つがあります.
右の画像が入力ストリームを表しています. データソースはファイルやネットワークから,もしくは,別のプログラムであるなど, 入力ストリームを扱う側は知らなくても良いようになっています. とにかく,ストリームを扱う側は,必要なデータが流れてくることさえ理解していれば良いわけです. このような性質のため,Javaでは,あらゆる入出力にストリームを利用します.
入力元がファイルであろうと,ネットワークの先であろうと,受け取る方法は同じです. 入力元がどこであろうと,同じように扱うために,ラッピング という作業が必要になります.
右の画像が出力ストリームを表しています.出力も入力ストリームと同じく, ラッピングを利用して,出力先を気にすることなく, データを書き込めます.
ファイル,ネットワーク,また,メモリに書き込むときにもストリームを利用します.
8-1-2. ラッピング
先ほど,ファイルやネットワーク,さては,メモリの入出力までストリームを利用できると述べました. しかし,異なる出力先を,同じように扱うには,どうすれば良いのでしょうか. このような,異なるものを同じように扱うために,ラッピング(wrapping)と言う概念を利用します. ラッピングは,アダプタパターン と言われる場合もあります.
ラッピングとは,包むや包装するという意味です.その意味のとおり,ラッピングを行うには, とある実体を別の型で覆います. 右図のように,別の型で覆うことで,内側の型(Inner)を外側の型(Outer) として扱えるようになります.
public class Inner{ void method1(){ // ... } }
public class Outer{ Inner inner; void method2(){ inner.method1(); } }
先ほどの図をJavaのプログラムで表すと,左のプログラムのようになります.
Inner
型が内側,Outer
型が外側です.
Outer
型がフィールドにInner
を持ち,
Outer
のmethod2
の呼び出しを
Inner
のmethod1
を呼び出すことで,
型が変わっても同じ機能が実現できています.
public class Main{ void run(){ Inner inner = new Inner(); Outer outer = this.wrap(inner); outer.method2(); } Outer wrap(Inner inner){ Outer outer = new Outer(); outer.inner = inner; return outer; } }
Inner
とOuter
の使用例は左のMain
を参照してください.
wrap
メソッドで型の変換を行っています.
wrap
メソッドは,Inner
型を受け取り,
Outer
型を返しています.
Inner
とOuter
と型は異なりますが,
Outer
のmethod2
を呼び出したとしても,最終的には,
Inner
のmethod1
が実行されます.
このように,とある型を異なる型に変換するために,ラッピングが使われます.
実際に,Javaの入出力には,ラッピングを使用して,異なる型を同じように扱います. では,次に,ストリームを扱う型を見てみましょう.
8-1-3. Reader
Javaのストリームを扱う型は,右の表のようにReader
,Writer
,
InputStream
,OutputStream
の4種類に大別できます.
文字を扱う型がReader
とWriter
,
バイナリデータを扱う型がInputStream
とOutputStream
です.
バイナリデータとは,画像ファイルや,音声ファイルなどです.テキストファイルは,
HTMLやプログラムのソースファイルなどを指します.
また,入力を扱う型が,Reader
,InputStream
,
出力を扱う型がWriter
とOutputStream
です.
この講義では,バイナリデータを扱う型であるInputStream
とOutputStream
は扱わず,テキストデータを扱う型のみを利用します.
ただし,テキストデータを扱う型も,バイナリデータを扱う型も使い方はほぼ同じですので,
必要になった時は,この内容を見返しましょう.
まずは,入力を扱うReader
を利用する方法を確認しましょう.
Reader
と呼ばれるものには,幾つかの型が存在します.
代表的な型は,FileReader
,BufferedReader
の2つです.
FileReader
はファイルからのデータを扱うための型です.
BufferedReader
は,バッファリングを行うための型であり,
1行の単位での読み取りが可能になっています.
では,ラッピングをどのように行うかを見ていきましょう.
今回は,先ほどから述べている2つの型,FileReader
とBufferedReader
を利用します.なお,これらの型を利用する場合は,java.io.*
のインポートが必要です.
8-1-4. FileReaderとBufferedReader
// File型の値を取得し,変数に代入する. File file = new File("file.txt"); FileReader freader = new FileReader(file);
ファイルからの入力を扱う型がFileReader
です.
File
型が指し示すパスのファイルを読むには,左のように
FileReader
を作成するときに,File
型の変数を渡します.
このプログラムでFileReader
型の実体が作成できました.
しかし,FileReader
型では,1文字ずつしか読み込めません.
テキストファイルですから,1行ずつ読み込んで処理を行いたいところです.
そのために,BufferedReader
型でFileReader
をラッピングします.
FileReader freader = // 取得する. BufferedReader in = new BufferedReader(freader);
左のように,BufferedReader
の実体を作成する時に,
FileReader
を渡せば,ラッピングが完了し,以降,
BufferedReader
型として扱えるようになります.
BufferedReader in = new BufferedReader( new FileReader("file.txt") ); String line; while((line = in.readLine()) != null){ // 1行ずつ処理を行う. } in.close();
なお,左の1〜3行目のように,FileReader
を別の変数に代入することなく書くことも可能です.
さて,BufferedReader
を利用して,ファイルの内容を1行ずつ読み込みましょう.
読み込むためのメソッドは,readLine
です.
ファイルの終端に達するとnull
が返されます.
ですから,左のようなwhile
文を書くことで,1行ごとに処理を行えるようになります.
さて,読み込みが終われば,最後に閉じなければいけません.閉じるには,ストリームに対して,
close
メソッドを呼び出してください.ストリームをラッピングしている場合は,
順番に閉じてくれますので,一番外側のストリームに対してclose
してください.
ここでは,BufferedReader
型のin
に対してclose
すれば良いです.
では,上のプログラムのfile.txt
をダウンロードし,file.txt
の内容を出力するプログラムを書いてみましょう.左のプログラムで,
1行ずつ読み込んだ文字列をそのままSystem.out.println
を使い出力してください.
クラス名は,FileViewer
とします.
8-2. 例外 (Exception)
さて,上記のプログラムをコンパイルしようとすると,次のエラーが発生します.
FileViewer.java:5: エラー: 例外FileNotFoundExceptionは報告されません。スローするには、捕捉または宣言する必要があります BufferedReader in = new BufferedReader(new FileReader("file.txt")); ^ prog/find/FileViewer.java:7: エラー: 例外IOExceptionは報告されません。スローするには、捕捉または宣言する必要があります while((line = in.readLine()) != null){ ^ エラー2個
このエラーはどのような意味を表しているのでしょうか. このエラーの内容を理解するには,例外(Exception)という概念の理解が必要です.
8-2-1. 例外とは何か.
例外は,近年のプログラミング言語で採用されている実行時エラーの通知機構です.
例えば,C言語の場合,fopen
で開くファイルが見つからなかった場合,
返り値がNULL
でエラーを通知していました.
この場合,右図のように,返り値の値を確認して正常か異常かを判断しなければいけません.
このとき,プログラマなどの人による判断が必須であり,また,
正常処理と異常処理が物理的に近い場所に書かなくてはならず,離すことは非常に困難です.
また,正常であろうと異常であろうと,同じ経路で判断しなければいけないため,
正常処理の内容が異常処理に埋もれてしまう原因になります.
これでは,プログラム全体の見通しが悪くなり,理解も困難になってしまいます.
一方,例外機構は,人しか判断できないものではなく,異常処理を行うための別の経路を作り出すものです. つまり,例外機構は,プログラムの実行途中で,何らかの異常が発生した時, それまで行なっていた処理を中断して別の処理を行うための機構です. このため,例外が投げられれば,何らかの異常が発生したと判断できます.例外が投げられない場合は, データが変であろうが,正常な処理であると判断できます (もちろん,開発途中で変な場合はバグの可能性はありますが).
また,プログラマ自ら投げる例外も存在しますが,多くの場合,システム側が例外を投げます. 例えば,ファイルが見つからない場合や,ファイルを開く権限がない場合は, システムが自動的に例外を投げます.そのため,プログラマは,ファイルを開く処理を書き, その例外を処理する場所を自由に決められます.そのため, 例外が起こった直後に異常処理を行う必要はありません.
8-2-2. 例外の種類.
例外には,どんな状況で起こったエラーかによって,様々な種類があります. 例えば,代表的なエラーの原因とそれに対応する例外の型を以下に列挙します.
ArrayIndexOutOfBoundsException
- 配列の要素数を超えてアクセスしようとした場合
FileNotFoundException
- ファイルが見つからなかった場合
IOException
- 入出力エラーが起こった場合
NullPointerException
null
が代入された変数にアクセスしようとした場合.
なお,例外の型は階層構造を持っており,上位の概念の例外は下位の概念の例外を包括できます.
例えば,FileNotFoundException
という例外の上位に
IOException
が存在しますので,FileNotFoundException
の代わりに
IOException
で代用できます.
FileFinderで扱う例外は,ファイル入出力関係のみですので,ここでは,
IOException
のみを扱うことにしましょう.
8-2-3. 例外の扱い方.
では,どのように,例外を処理する場所を決めるのでしょうか. この講義では,例外を処理する方法ではなく,例外を呼び出し元に例外はすべてスルーして, 最終的にJavaの実行環境に処理を任せるものとします.
例外をプログラム内で処理したい場合は,try-catch構文というものがありますので,調べてみてください. この講義内では,例外への対応は行いません.
public class FileViewer{ void run() throws IOException{ BufferedReader in = new BufferedReader( new FileReader("file.txt") ); // 以下のプログラムは省略. } public static void main(String[] args) throws IOException{ // 内容は省略. } }
さて,例外を投げるには,左のプログラムのように,メソッドの宣言の後ろに,
throws
節を宣言しなければいけません.
このthrows
節は,そのメソッドが投げる可能性のある例外を宣言するものです.
また,throws
宣言されたメソッドを呼び出す時,
呼び出すメソッドでその例外に対応するか,その呼び出し元のメソッドで対応するため,
throws
節を定義しなければいけません.
つまり,左のプログラムの場合,BufferedReader
の実体を作成するとき,
IOException
という例外が投げられる可能性があるため,
run
メソッド内でこの例外に対応するか,
run
メソッドの呼び出し元で対応してもらうため,
run
メソッドにthrows
節を追加しなければいけません.
また,run
メソッドの呼び出し元であるmain
メソッドも例外に対応しないため,throws
節の追加が必要です.
このように,メソッドにthrows
節を追加することで,
異常処理を書く場所の呼び出し元への委譲が可能になります.
では,上記の処理を踏まえて,FileViewer
を作成し,
実行結果を確認してみましょう.
8-3. ファイルの内容を検索する.
Boolean isTarget(Arguments args, File file) throws IOException{ if(args.grep != null){ // 検索にマッチしない場合のみ返す.検索にマッチする // 場合は,別の条件でも検索を行うことに注意. if(!this.isContained(args.grep, file)){ return false; } } // 残りの処理は省略. } Boolean isContained(String keyword, File file) throws IOException{ // fileから1行ずつlineを読み,keyword が // 含まれているかを判定する.含まれていればtrue, // 含まれていなければfalseを返す. }
では,FileFinder
にファイルの内容を検索するオプションである
grep
を実現しましょう.
今までと同じように,4-1を拡張し,
ファイルの内容で検索するisContained
メソッドを作成してください.
また,isContained
メソッドは,ファイルを開くため,
IOException
が発生します.ですから,メソッドにthrows
節の宣言が必要になります.
isContained
メソッドにthrows
を追加することにより,
isContained
の呼び出し元,その呼び出し元,そして,そのまた呼び出し元も同じように
throws
節が必要になりますので,注意してください.
では,検索を行いましょう.文字列の完全一致は,Objects.equals
で判定できましたが,
文字列Aが文字列Bに含まれているかを判定するにはどうすれば良いでしょうか.
つまり,ここでは,line
にkeyword
が含まれているかを判定したいわけです.
その確認には,String
型が持つcontains
メソッドが利用できます.
line.contains(keyword)
のように呼び出すことで,line
が表す文字列にkeyword
が含まれていればtrue
が返り,
含まれていなければ,false
が返されます.このメソッドを使い,判定してください.
また,FileReader
にディレクトリを表すFile
型を渡すと,
FileNotFoundException
が発生します.
isContained
メソッド内で,ディレクトリか否かを判定し,
ファイルのみを処理の対象とするようにしましょう.
練習問題
1. Catコマンドを作成する.
$ cat file1.txt abcdefg ABCDEFG $ cat file2.txt 1234567890 9876543210 $ java Cat file1.txt abcdefg ABCDEFG $ java Cat file2.txt file1.txt 1234567890 9876543210 abcdefg ABCDEFG
UNIXのコマンドであるcat
コマンドを作成してください.
引数に受け取ったファイルの内容を画面に出力します.
左に実行例を示します.
まずは,コマンドラインで受け取った1つのファイルを出力できるようにしてください.
それができれば,コマンドラインで複数のファイルを受け取ったとき, すべてのファイルを出力するようにしてください.
2. 検索結果をファイルに出力するオプションを追加する.
Findの出力結果をファイルに出力するオプションを追加してください.
-output
オプションが指定された場合,出力先がoutput
オプションで指定されたファイルになるようにしてください.
$ java FileFinder . -type f Arguments.class Arguments.java FileFinder.class FileFinder.java $ java FileFinder . -type f -output result.txt $ cat result.txt Arguments.class Arguments.java FileFinder.class FileFinder.java
ファイルにテキストを書き込むには,FileWriter
を利用してください.
また,print
やprintf
を利用するには,
FileReader
の時と同じように,FileWriter
をラップする必要があります.
PrintWriter
でラップすると,print
,printf
,
println
が使えるようになります.
また,System.out
をPrintWriter
でラップすることで,
標準出力にも出力できます.
つまり,以下のようなプログラムを書き,out.println
メソッドを使って出力することで,適切な場所に出力されるようになります.
PrintWriter out; if(args.output == null){ out = new PrintWriter(System.out); } else{ out = new PrintWriter(new FileWriter(args.output)); }
args
はArguments
型の変数です.
実際に出力する場所は,printResult
メソッド内でしょうから,
printResult
の中でArguments
型の変数にアクセスできるようにしておく必要があります.
また,PrintWriter
を使い終えたら,
close
メソッドを呼び出して閉じましょう.
ファイルを開きっぱなしにするのはプログラミングの作法的によろしくありません.
もし,画面に何も出力されなかった場合は,out
に対して,
flush
メソッドを呼び出してください.バッファリングされている
(溜められている) データをすべて吐き出します.
本日のまとめ
今日,学んだ内容は次の通りです.
- ファイルの内容を読み込む方法
- ファイルを開くための型:
FileReader
. - 一行ずつ読み込むための型:
BufferedReader
. - ファイルを一行ずつ読み込む方法.
BufferedReader in = new BufferedReader(new FileReader("file.txt")); String line; while((line = in.readLine()) != null){ // 一行ずつ読み込む. }
- ファイルを開くための型:
- ファイルの内容を読み込む方法
- ファイルに書き込むための型:
FileWriter
. - 文字列を出力するための型:
PrintWriter
.System.out
もラップできる.
- ファイルに書き込むための型:
- 例外(Exception)
- 例外の概念.
- 代表的な例外の型.
- 例外を投げることを宣言する.
-
文字列Aが文字列Bに含まれているかを確認する方法.
String
型のcontains
メソッド.