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

本日の内容

  1. コマンドライン引数で指定されたディレクトリを受け取る.
  2. 指定されたディレクトリから,ディレクトリを辿っていく.
  1. 練習問題
  2. 本日のまとめ

1. コマンドライン引数で指定されたディレクトリを受け取る.

1-1. プログラムの大枠を作成する.

まず,プログラムを作成しましょう.Javaの基礎 で勉強したクラス定義の基本形 を元に,次の条件を満たすプログラムを作成しましょう.

void printArgs(String[] args){
    for(String arg: args){
        System.out.println(arg);
    }
}
void printArgs(String[] args){
    for(Integer i = 0; i < args.length; i++){
        System.out.println(args[i]);
    }
}

Listに格納されている要素の数は,Listの実体に対して, sizeメソッドを呼び出すことで得られました. 配列の場合は,配列を指す変数に対して,length 変数を参照することで要素数を得られます.この変数は参照のみで代入はできません. 代入しようとするとコンパイルエラーとなります.

完成すれば,実行してみましょう. 何か値を出力させるために,どのように実行すれば良いか考えてみましょう.

1-2. コマンドライン引数を理解する.

コマンドライン引数とは,実行する時にプログラムに与える引数のことです. 例えば,findプログラムの例として,以下のコマンド列を挙げました.

find . -name HelloWorld.java

この例の場合,findがコマンド,.-nameHelloWorld.javaの3つがコマンドライン引数と呼ばれます. こレラコマンドライン引数をプログラムで扱うには,main メソッドの引数を使います.

java FileFinder . -name HelloWorld.java

仮に,左に示したコマンドが実行された場合,mainメソッドの引数である String[]型の変数にはどのような値が入るでしょうか. 配列の要素それぞれに,コマンドライン引数が文字列として格納されます.

コマンドライン引数

右図のように,スペースで区切った文字列それぞれが,配列の各要素に割り当てられます. 配列の0番目の要素には,最初の引数,配列の1番目の要素は,2番目の引数, のような割り当てが行われます.

では,実際に,先ほど作成したプログラムを使って,コマンドライン引数を指定してみましょう.

1-3. オプションを扱う型を作成する.

このfindプログラムの仕様では, 様々な条件を元にファイルが検索できることが示されています. これを元に,コマンドとそのオプションの指定方法を整理しましょう.

$ java FileFinder <DIR> [OPTIONS]
DIR:
    検索を開始するディレクトリを指定する.必須項目.
OPTIONS:
    -help:            ヘルプメッセージを表示して終了する(検索しない).
    -name <NAME>:     ファイル名・ディレクトリ名で検索する.NAMEは必須.
    -type <d|f>:      ファイルの種別で検索する.dはディレクトリ,fはファイルを指す.
                      dもしくはfのどちらかを必ず指定する.
                      このオプション自体を指定しない場合,両方を検索する.
    -maximum <SIZE>:  ファイルサイズが指定のサイズ(SIZE)より小さいものを検索する.
    -minimum <SIZE>:  ファイルサイズが指定のサイズ(SIZE)より大きいものを検索する.
    -grep <STRING>:   内容に指定された文字列(STRING)が含まれているファイルを検索する.

上のようなコマンドを作成しましょう.オプションは,6つあり,どれもが指定してもしなくても構いません. しかし,検索の開始ポイントであるDIRは必須とします. 何もオプションが指定されない場合は, DIR以下の全てのファイルが検索結果として表示されるものとします.

さて,どのようなオプションが指定されたのかを表す型を作成しましょう. 名前は,Argumentsとして,以下のフィールドを持つものとします.

なお,File型を利用するときは,import java.io.*; の一文をプログラムの最初に追加してください.

2. 指定されたディレクトリから,ディレクトリを辿っていく.

2-1. Javaでファイル・ディレクトリを扱う.

Javaでファイルやディレクトリを扱う型は2種類存在します. java.ioパッケージのFilejava.nio.fileパッケージのPathです. Pathの方が新しく,機能的にも充実していますが, 本講義では,Fileを扱います. 理由はFAQの 「なぜ新しいPathではなく古いFileを使うのでしょうか」に記しています.

void run(String[] args){
    Arguments arguments = this.parseArguments(args);
    List<File> resultList = this.find(arguments);
    this.printResult(resultList);
}

File型は,ファイル,ディレクトリを同じように扱います. では,次の指示に従い,File型の実体を作成しましょう. なお,FileFinderには,左に示す,runメソッドを定義しておきましょう.

2-2. ファイル・ディレクトリを区別する.

Javaでは,ファイルも,ディレクトリも,File型で表現します. しかし,ディレクトリは0個以上のディレクトリ,もしくはファイルを含みます. 両者を区別する2つの方法が,File型には用意されています. それが,isFileメソッド,もしくは,isDirectoryメソッドです.

File型の実体が指し示すパスがファイルの場合,isFileメソッドは trueisDirectoryメソッドは,falseを返します. 一方, File型の実体が指し示すパスがディレクトリの場合,isFileメソッドは falseisDirectoryメソッドは,trueを返します. 次に示す例を元に解説します.

public class FileOrDir{
    void run(){
        File file = new File("FileOrDir.java");
        System.out.println("FileOrDir.java is file: " + file.isFile());
        System.out.println("FileOrDir.java is directory: " + file.isDirectory());
        File dir  = new File(".");
        System.out.printf(". is file: %s%n", dir.isFile());
        System.out.printf(". is directory: %s%n", dir.isDirectory());
    }
    // mainメソッドは省略.
}

上のプログラムをコンパイル,実行して結果を確認しましょう. まず,3行目で,Fileの実体をを作成し, File型の変数fileに代入しています. fileが指し示すパスは,FileOrDir.javaです. 4, 5行目でfileに対して,ファイルであるか,ディレクトリあるか, それぞれ判定した結果を出力しています.

一方,6行目で別のFile型の変数dirに,別の実体を作成,代入しています. dirが指し示すパスはカレントディレクトリです. 7, 8行目でdirに対して,ファイルであるか,ディレクトリであるか, それぞれ判定した結果を出力しています.

2-3. ディレクトリを掘り進む.

ディレクトリ内のファイル一覧を取得する.

ここでは,指定されたディレクトリ内のファイル・ディレクトリを確認し,ディレクトリを掘り進んでいきます. そのために,ディレクトリ内のファイル・ディレクトリ一覧を取得しましょう. 取得するには,File型の変数に対して,listFilesメソッドを呼び出します.

File[] files = dir.listFiles();

左の呼び出しでdirが指し示すディレクトリ以下のファイル一覧を取得できます. もし,dirが指し示すパスがディレクトリでなければ (dir.isDirectory()falseを返せば), nullが返されます.

if(dir.isDirectory()){
    for(File file: dir.listFiles()){
        // file に対する処理を行う.
    }
}

そこで,左のようなプログラムであれば,dir 以下の各ファイルに対して順番に処理が行えるようになります. なお,listFilesで返される配列は特定の順序にはなりません.

ディレクトリを横断するメソッドを作成する.

では,FileFinderに,次の処理を追加しましょう.

さて,上記のプログラムが作成できれば,実行してみましょう. 実行の際,プログラムにコマンドラインから引数を渡すのを忘れずに. また,traverseメソッド内から traverseメソッドを呼び出しています. これがどのような結果になるのかを確認しましょう.

このような,あるメソッドからそのメソッド自身を呼び出すことを再帰呼び出しと呼びます.

練習問題

再帰呼び出しの例(階乗を例に)

Javaの基礎3. 階乗 のプログラムを元に再帰呼び出しの例を示します. 階乗の漸化式は次の通りです. \[ n! = \begin{cases} 1 & n = 1\\ n \times (n-1)! & n > 1 \end{cases} \]

public class Factorial2{
    void run(){
        Integer number = 3;
        Integer factorialValue = 
            this.factorial(number);
        System.out.printf(
            "%d! = %d%n", number, factorialValue
        );
    }
    Integer factorial(Integer value){
        if(value == 1){
            return 1;
        }
        return value * this.factorial(value - 1);
    }
    // mainメソッドは省略.
}

まず,左のプログラムのfactorialメソッドに注目してください. 引数のvalueの値が1の場合は,返り値として1を返します. そして,valueの値が1以外の場合は, valueの値を変更して,factorialを再度,呼び出しています. あるメソッドから,同一メソッドを呼び出すことを再帰呼び出しと言います. 上に示した漸化式で考えると理解が容易でしょう.

このプログラムがよくわからない場合は,factorial メソッド内でvalueがどのような値になっているのかを調べてみると良いでしょう. また,runメソッドのnumberの値を変更して実行しなおしてみましょう.

再帰呼び出しを利用するときは,呼び出しが確実に終わるようにしておく必要があります. このプログラムの例であれば,1より小さい値で呼び出すと, プログラムが終了しません(階乗自体が自然数を対象としていますので,ここでは対応していません). このように,条件によっては,終了しない場合もあり, その可能性を排除する必要があることは念頭に置いておいてください.

1. Fibonacci数列再び

Javaの基礎4. Fibonacci数列で書いた Fibonacci数列のプログラムを再帰呼び出しを利用して描き直してみましょう. Fibonacci数列の漸化式は次の通りです. \[ F_i = \begin{cases} 0 & i=0\\ 1 & i=1\\ F_{i-2}+F_{i-1} & i\geq2 \end{cases} \]

本日のまとめ

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