2016年度 発展プログラミング演習 第9講 Findプログラム 3日目

本日の内容

  1. コマンドライン引数でファイルの中身を検索する文字列を受け取れるようにする.
  2. ファイルの中身を指定された文字列で検索できるようにする.
  1. 練習問題
  2. 本日のまとめ

7. コマンドライン引数でファイルの中身を検索する文字列を受け取れるようにする.

3-1. コマンドライン引数を解析するで実現している内容ですが, まだできていない場合は,前回のページを参考に実現しましょう.

8. ファイルの中身を指定された文字列で検索できるようにする.

8-1. ファイルを開く.

8-1-1. ストリーム

Javaで入出力を扱うには,ストリームという概念の理解が必要です. ストリームとは,データを流れとして扱う仕組みです. データがデータソース(Data Source; データの源,入力元)から流れてくる入力ストリームと, データをデータの出力先に流し込む出力ストリームの2つがあります.

入力ストリーム 右の画像が入力ストリームを表しています. データソースはファイルやネットワークから,もしくは,別のプログラムであるなど, 入力ストリームを扱う側は知らなくても良いようになっています. とにかく,ストリームを扱う側は,必要なデータが流れてくることさえ理解していれば良いわけです. このような性質のため,Javaでは,あらゆる入出力にストリームを利用します.

入力元がファイルであろうと,ネットワークの先であろうと,受け取る方法は同じです. 入力元がどこであろうと,同じように扱うために,ラッピング という作業が必要になります.

出力ストリーム 右の画像が出力ストリームを表しています.出力も入力ストリームと同じく, ラッピングを利用して,出力先を気にすることなく, データを書き込めます.

ファイル,ネットワーク,また,メモリに書き込むときにもストリームを利用します.

8-1-2. ラッピング

Adapter 先ほど,ファイルやネットワーク,さては,メモリの入出力までストリームを利用できると述べました. しかし,異なる出力先を,同じように扱うには,どうすれば良いのでしょうか. このような,異なるものを同じように扱うために,ラッピング(wrapping)と言う概念を利用します. ラッピングは,アダプタパターン と言われる場合もあります.

ラッピングとは,包むや包装するという意味です.その意味のとおり,ラッピングを行うには, とある実体を別の型で覆います. 右図のように,別の型で覆うことで,内側の型(Inner)を外側の型(Outer) として扱えるようになります.

public class Inner{
    void method1(){
        // ...
    }
}
public class Outer{
    Inner inner;
    void method2(){
        inner.method1();
    }
}

先ほどの図をJavaのプログラムで表すと,左のプログラムのようになります. Inner型が内側,Outer型が外側です. Outer型がフィールドにInnerを持ち, Outermethod2の呼び出しを Innermethod1を呼び出すことで, 型が変わっても同じ機能が実現できています.

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;
    }
}

InnerOuterの使用例は左のMainを参照してください. wrapメソッドで型の変換を行っています. wrapメソッドは,Inner型を受け取り, Outer型を返しています. InnerOuterと型は異なりますが, Outermethod2を呼び出したとしても,最終的には, Innermethod1が実行されます. このように,とある型を異なる型に変換するために,ラッピングが使われます.

実際に,Javaの入出力には,ラッピングを使用して,異なる型を同じように扱います. では,次に,ストリームを扱う型を見てみましょう.

8-1-3. Reader

ストリームの分類 Javaのストリームを扱う型は,右の表のようにReaderWriterInputStreamOutputStreamの4種類に大別できます. 文字を扱う型がReaderWriter, バイナリデータを扱う型がInputStreamOutputStreamです. バイナリデータとは,画像ファイルや,音声ファイルなどです.テキストファイルは, HTMLやプログラムのソースファイルなどを指します.

また,入力を扱う型が,ReaderInputStream, 出力を扱う型がWriterOutputStreamです. この講義では,バイナリデータを扱う型であるInputStreamOutputStream は扱わず,テキストデータを扱う型のみを利用します. ただし,テキストデータを扱う型も,バイナリデータを扱う型も使い方はほぼ同じですので, 必要になった時は,この内容を見返しましょう.

まずは,入力を扱うReaderを利用する方法を確認しましょう. Readerと呼ばれるものには,幾つかの型が存在します. 代表的な型は,FileReaderBufferedReaderの2つです. FileReaderはファイルからのデータを扱うための型です. BufferedReaderは,バッファリングを行うための型であり, 1行の単位での読み取りが可能になっています.

ストリームのラッピング では,ラッピングをどのように行うかを見ていきましょう. 今回は,先ほどから述べている2つの型,FileReaderBufferedReader を利用します.なお,これらの型を利用する場合は,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言語のエラー処理機構 例外は,近年のプログラミング言語で採用されている実行時エラーの通知機構です. 例えば,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に含まれているかを判定するにはどうすれば良いでしょうか. つまり,ここでは,linekeywordが含まれているかを判定したいわけです. その確認には,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を利用してください. また,printprintfを利用するには, FileReaderの時と同じように,FileWriterをラップする必要があります. PrintWriterでラップすると,printprintfprintlnが使えるようになります.

また,System.outPrintWriterでラップすることで, 標準出力にも出力できます. つまり,以下のようなプログラムを書き,out.println メソッドを使って出力することで,適切な場所に出力されるようになります.

PrintWriter out;
if(args.output == null){
    out = new PrintWriter(System.out);
}
else{
    out = new PrintWriter(new FileWriter(args.output));
}

argsArguments型の変数です. 実際に出力する場所は,printResultメソッド内でしょうから, printResultの中でArguments 型の変数にアクセスできるようにしておく必要があります.

また,PrintWriterを使い終えたら, closeメソッドを呼び出して閉じましょう. ファイルを開きっぱなしにするのはプログラミングの作法的によろしくありません.

もし,画面に何も出力されなかった場合は,outに対して, flushメソッドを呼び出してください.バッファリングされている (溜められている) データをすべて吐き出します.

本日のまとめ

今日,学んだ内容は次の通りです.