2016年度 発展プログラミング演習 第7講 Findプログラム 1日目
本日の内容
1. コマンドライン引数で指定されたディレクトリを受け取る.
1-1. プログラムの大枠を作成する.
まず,プログラムを作成しましょう.Javaの基礎 で勉強したクラス定義の基本形 を元に,次の条件を満たすプログラムを作成しましょう.
- クラス名は,
FileFinderとします. runメソッドは,String型の配列 (String[]型)を受け取ります.mainメソッドから,FileFinderの実体を介してrunメソッドを呼ぶとき,mainメソッドの引数をそのままrunメソッドに渡してください.-
printArgsメソッドを作成し,String型の配列を引数として受け取ってください.返り値はvoidとします. -
runメソッドでは,printArgsを呼び出してください.runメソッドの引数をそのままprintArgsに渡してください. -
printArgsメソッドでは,引数に受け取ったString型の配列(args)の要素を順に出力してください. 下に示すコードで出力できます(どちらも同じ結果です.どちらかをメソッド内に書いてください).
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がコマンド,.,-name,
HelloWorld.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型のdirectory. 検索の開始ポイントとなるディレクトリを意味します.必須項目です.File型については,後述します. -
Boolean型のhelp. 指定されるとヘルプメッセージを表示します. -
String型のname. 指定されるとファイル名・ディレクトリ名を元に検索を行います. -
String型のtype. 指定されるとファイル・ディレクトリの種別を元に検索を行います. -
Integer型のmaximum. 指定されると,ファイルサイズの上限が定まり,この上限より小さなファイルサイズのファイルが検索対象となります. -
Integer型のminimum. 指定されると,ファイルサイズの下限が定まり,この下限より大きなファイルサイズのファイルが検索対象となります. -
String型のgrep. ファイルの内容を元に検索を行います.
なお,File型を利用するときは,import java.io.*;
の一文をプログラムの最初に追加してください.
2. 指定されたディレクトリから,ディレクトリを辿っていく.
2-1. Javaでファイル・ディレクトリを扱う.
Javaでファイルやディレクトリを扱う型は2種類存在します.
java.ioパッケージのFileと
java.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メソッドを定義しておきましょう.
-
FileFinderに次の3つのメソッドを定義しましょう.-
parseArgumentsメソッド: 引数はString型の配列, 返り値の型は,Argumentsです. -
findメソッド:引数は,Arguments,返り値の型は,List<File>とします. なお,コンパイルが通るように,List<File>の実体を作成し, 返すようにしてください. -
printResultメソッド: 引数はList<File>,返り値はなし(void)とします. 受け取ったList<File>の要素を一つずつ取り出し,System.out.printlnで出力してください.
-
-
parseArgumentsメソッドでは,次の処理を行ってください.Arguments型の変数を定義し,newしてください.-
引数で受け取った配列を順に調べて,
Arguments型の実体のdirectoryフィールドに代入してください. 代入の時には,File型の実体を作成する必要があります.new File(args[i])として作成してください (拡張for文の場合など,args[i]を適切に読み替えてください). - 本来はコマンドライン引数を解析して,適切なフィールドに代入しますが, ここでは検索対象のディレクトリを指定するだけにしておきます. 実際の分析は後日プログラミングを行います.
- 作成した
Arguments型の実体をreturnしてください.
2-2. ファイル・ディレクトリを区別する.
Javaでは,ファイルも,ディレクトリも,File型で表現します.
しかし,ディレクトリは0個以上のディレクトリ,もしくはファイルを含みます.
両者を区別する2つの方法が,File型には用意されています.
それが,isFileメソッド,もしくは,isDirectoryメソッドです.
File型の実体が指し示すパスがファイルの場合,isFileメソッドは
true,isDirectoryメソッドは,falseを返します.
一方,
File型の実体が指し示すパスがディレクトリの場合,isFileメソッドは
false,isDirectoryメソッドは,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メソッドを追加してください.引数はArguments,List<File>,Fileの3つ,返り値はvoidです. traverseメソッドの処理は次の通りとします.-
引数の
List<File>に引数のFileを追加してください. -
引数に受け取った
Fileがディレクトリの場合,listFilesメソッドでディレクトリ内の ファイル・ディレクトリそれぞれでtraverseメソッドを呼び出します. -
引数に受け取った
Fileがファイルの場合,何も行いません.
-
引数の
-
findメソッドで次の処理を追加してください.
さて,上記のプログラムが作成できれば,実行してみましょう.
実行の際,プログラムにコマンドラインから引数を渡すのを忘れずに.
また,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} \]
- クラス名は,
Fibonacci2とします. -
クラス定義の基本形を参考に,
mainメソッドからrunメソッドを呼び出します. -
printFibonacciSeriesメソッドを用意します. 返り値はvoid,引数はInteger型の変数maxとします. -
runメソッドからprintFibonacciSeriesメソッドを呼び出しましょう. 引数は,20を渡してください. -
getFibonacciValueメソッドを用意します. 返り値はInteger,引数はIntegerとします. -
printFibonacciSeriesメソッド内で0からmaxまで繰り返してください. この繰り返しで,Fibonacci数列のi番目の値を順に取得します. Fibonacci数列のi番目の値は,getFibonacciValueメソッドの呼び出しで取得してください. また,繰り返し中に取得した値を画面に出力してください. -
getFibonacciValueメソッドでは次の処理を行ってください.- 引数の値が
0の場合は,$F_0$の値である0を返します. - 引数の値が
1の場合は,$F_1$の値である1を返します. -
上記以外の場合は,再帰呼び出しで$F_i$の値を計算して返しましょう.
getFibonacciValueを適切な引数で呼び,返り値の値を足し合わせれば, $F_i$の値が取得できます.
- 引数の値が
本日のまとめ
今日,学んだ内容は次の通りです.
- コマンドラインオプションの扱い方.
mainメソッドの引数で与えられるString型の配列.- インデックスの0番目は最初のコマンドライン引数.
- Javaでのファイル・ディレクトリを扱う型,
File.File型が指し示すパスがディレクトリかファイルかを判定する方法.File型が指し示すパスがディレクトリの場合,そのディレクトリに含まれるファイル・ディレクトリの一覧を取得する方法.
- コマンドライン引数を解析する方法.
- 再帰呼び出し.