第14講 最終課題

第14講 最終課題のサブセクション

概要

目標

あるデータが与えられますので,そのデータを分析し,結果を出力するプログラムを作成します. いくつかのステップが与えられるので,各ステップで指定された分析を行ってください.

参考資料

課題の進め方

この課題は大きく6つのステップに分けられます. それぞれのステップで ScoreAnalyzer1.javaScoreAnalyzer6.java を作成します. 以下のことを念頭に課題を進めてください.

  • ステップ1からステップ2ステップ3と順番に課題を進めてください. 途中のステップのスキップはできません.
  • ステップ1で作成した内容を元に, ステップ2を作成してください.
    • ステップ3以降も同様に,前のステップで作成した内容を全て含めて当該ステップに取り組んでください.
    • プログラムを作成するとき,一つ前のステップのプログラムをコピーして始めると良いでしょう.
      • ただし,mainメソッドの内容を修正することを忘れないようにしましょう.
      • mainメソッドで異なる型をnewするバグはなかなか気付きません.
  • ステップ3までが必須で,ステップ4ステップ5ステップ6がチャレンジ問題です.
    • ただし,必須問題しか完成させていない場合,最高でも60点にしかならないため,試験で失敗すると単位取得が厳しくなります.
    • ステップ4までを完成させていれば,たとえ試験で失敗したとしても単位取得が現実的になります.
      • もちろん,失敗の度合いもありますし,ステップ4まで完成させたつもりが完成できていないこともありますので,確実に単位取得を保証する訳ではありません.

課題の提出方法

この課題は6つのステップに分けられています. 各ステップでScoreAnalyzer1.javaScoreAnalyzer6.javaを作成します. 具体的に指示はしていませんが,独自の型を作成する必要もあるでしょう. それら全てのソースファイルを Moodle に提出してください.

  • 提出期限は 2024-08-05 (Mon) 9:00です.
  • 提出先は Moodle の【2024-08-05 (Mon) 9:00〆切】最終課題提出場所 です.
  • 次のチェックリストを提出前に確認してください.
    • 関係するソースファイルをすべて提出しているか.
    • 提出したソースファイルのみでコンパイルに成功するか.
    • 全てのソースファイルにコメントとして,自分の学生証番号,名前が記載されているか.

課題のデータ

課題には次のデータを利用してください. なお,評価には,このページからダウンロードできるデータとは異なるものを利用します. データの形式は同じですが,記載されているデータやデータ量は異なります. しっかりとデータを読み,適切な分析を行うようにして下さい. なお,多少の計算誤差は許容されます. また,与えられるデータはソートされているとは限りません.

  • この課題で利用するデータです
  • これらのデータはとある講義の小テストの結果を示したものです.
    • ランダムに並び替えた上でIDを付け直していますので,個人を特定できないようにしています.

評価項目

以下の点を満たしていれば,加点されます. また,それぞれのステップで確認事項があります.それぞれを満たすことで加点されていきます.

  • インデントがずれている部分がないこと.
    • 少しでもインデントがずれているとNG.
    • Visual Studio Code の場合,Option + Shift + F でインデントすること.
  • ループ制御変数以外で1文字の変数名を利用していないこと.
  • 1つのメソッドが20行以内であること.
    • メソッド開始の{と終了の}は含まない.
  • 3つ以上のネストが存在しないこと.
    • 2重ループ内の条件分岐はアウト.別のメソッドに切り出しましょう.
  • フィールド,ローカル変数の数がそれぞれ5つ以下であること.
    • ローカル変数の数は,ここでは,メソッド内に定義されている全ての変数の数とします.
    • ただし,メソッドの引数は含みません.
  • 配列を使っていないこと.ただし,以下の部分は除きます.
    • mainメソッドの引数,および,その変数を他のメソッドに渡したときの引数,
    • splitメソッドの返り値および,その変数を他のメソッドに渡したときの引数.
  • クラス定義の基本形に従ってプログラムを書いていること.

最終課題

ステップ0

以下のステップを実行する前に,概要課題のデータから必要なデータをダウンロードしておいてください.

reading.csvwriting.csvは同じフォーマットのデータで,左カラムから順に以下のデータが書かれています.

  • 小テストが行われた日付
  • reading/writingの区別
  • 課題番号
  • 学生ID
  • 点数
  • 開始時刻
  • 提出時刻

ステップ1

1-A. 問題説明

コマンドライン引数で問題番号とデータファイルが指定されます. 指定されたデータファイルを読み,指定された問題番号のスコアの頻度(%)を出力してください. コマンドライン引数で与えられるデータは必ず1つであり,正しいフォーマットのファイルが渡されると仮定して構いません.

1-B. 実行例

$ java ScoreAnalyzer1 1 reading.csv # 問題番号1のスコアの頻度を出力する.
  :  2.308 ( 3/130) # 問題番号1を時間内に提出できなかった学生が3名(2.308%)いる.
 6:  7.692 (10/130) # 問題番号1で6点をとった人が130名中10名(7.692%)いる.
 8: 20.000 (26/130)
10: 70.000 (91/130)
$ java ScoreAnalyzer1 2 reading.csv
 0:  4.615 ( 6/130)
  :  0.769 ( 1/130)
 2: 10.000 (13/130)
 4: 24.615 (32/130)
 6: 31.538 (41/130)
 8:  6.923 ( 9/130)
10: 21.538 (28/130)
$ java ScoreAnalyzer1 4 reading.csv
  :  0.800 ( 1/125)
 0:  2.400 ( 3/125)
 2:  4.800 ( 6/125)
 4:  5.600 ( 7/125)
 6: 24.800 (31/125)
 8: 42.400 (53/125)
10: 19.200 (24/125)
$ java ScoreAnalyzer1 5 writing.csv
 1: 77.419 (72/93)
 3: 11.828 (11/93)
 5: 10.753 (10/93)
  • ファイルが何も指定されなかった場合は考える必要はありません.
  • 違うフォーマットのファイルが指定されることは考慮する必要はありません.
  • 出力の順番は実行例の通りでなくても構いません.

1-C. ヒント

処理の流れ

次の流れで処理すれば良いでしょう.

  1. HashMap<String, Integer>型の変数mapを宣言し,初期化する.
  2. コマンドライン引数で渡された文字列をファイルとして順に処理する.
    1. 1行をコンマ(,)で区切る.
    2. 3番目の要素(インデックスが2)が問題番号であるため,指定された問題番号であるかを確認する.
    3. 指定された問題番号の場合,当該スコアの人数を調べる.つまり,スコア(5番目の要素; インデックスが4)をキーとして人数(バリュー)をmapからgetする.
    4. 当該スコアの人数が0であれば(nullが返されれば),0で初期化する.
    5. 人数を +1 し,再度 mapに登録し直す.
  3. 受験者数を調べる.
    1. mapのバリューの全てを足し合わせることで,受験者数(examineeCount)が数えられます.
  4. HashMap<String, Integer> 型の変数の要素(キーとバリューのペア)を順に繰り返す.
    1. キーがスコア,バリューがそのスコアの人数を表します.
    2. バリューの数値に100.0 / examineeCountを掛けるとそのスコアの割合になります.
      • Integer型のままだと小数点以下が計算されない点に注意してください.

1-D. 評価項目

  • 概要に示した評価項目
  • データを変更しても,例外なく実行結果を出力できるか
    • 人数やスコアの有効値が変更されても例外なく出力されるか.

ステップ2

2-A. 問題説明

問題番号ごとに,スコアの割合を算出してcsv形式で出力してください. 各桁(column)にスコアを並べ,各行(row)には問題番号を並べてください.

2-B. 実行例

次のような出力になっています.

,スコア1,スコア2,....,スコアn
問題1,割合1,割合2,....割合n

上記のようにヘッダ(1行目)には有効なスコアを並べてください. 続いて,問題ごとに,各スコアの割合を出力してください.

$ java ScoreAnalyzer2 writing.csv
,1,3,5
1,,,100.000
2,70.536,14.286,15.179
3,25.893,25.893,48.214
4,41.818,13.636,44.545
5,77.419,11.828,10.753

2-C. ヒント

表形式の値を管理するために,多次元配列を用いるのではなく, HashMap<String, HashMap<String, Integer>> を用いるようにしてください. 次のような構造にすると良いでしょう.

HashMap<String, HashMap<String, Integer>> map = new HashMap<>();
// ... map に値をputしていく.
HashMap<String, Integer> scores = map.get("1");   // 問題番号1のスコア一覧を取得する.
Integer numberOfAInAssignment1 = scores.get("A"); // 問題番号1でAのスコアを取得した人数を取得する

2-D. 評価項目

  • 概要に示した評価項目
  • データを変更しても,例外なく実行結果を出力できるか
    • 人数やスコアの有効値,問題番号の範囲が変更されても例外なく出力されるか.
  • 成績のデータの管理に配列を用いていないこと.

ステップ3

3-A. 問題説明

学生ごとにスコアをまとめて csv として出力してください. また,学生ごと,問題ごとにスコアの最大,最小,平均も出力してください.

3-B. 実行例

次のような出力になっています.

学生ID,課題1の点数,課題2の点数,...., 最大値,最小値,平均点
....
,課題1の最大値,課題2の最大値,...
,課題1の最小値,課題2の最小値,...
,課題1の平均点,課題2の平均点,...
$ java ScoreAnalyzer3 reading.csv # プログラム読解のスコア
88,10,2,4,6,8,10,2,6.000000
89,10,,,,,10,10,10.000000
110,10,4,10,8,10,10,4,8.400000
111,10,4,8,10,10,10,4,8.400000
112,10,8,8,10,10,10,8,9.200000
# 途中省略
83,10,8,0,8,10,10,0,7.200000
84,,6,,,,6,6,6.000000
85,10,6,10,8,0,10,0,6.800000
86,,,,0,,0,0,0.000000
87,10,10,8,8,8,10,8,8.800000
,10,10,10,10,10
,6,0,0,0,0
,9.275591,5.829457,6.047244,7.177419,8.474576
$ java ScoreAnalyzer3 writing.csv # プログラム作成のスコア
88,5,1,1,1,1,5,1,1.800000
89,5,1,1,,,5,1,2.333333
110,5,5,5,5,,5,5,5.000000
111,5,1,5,5,1,5,1,3.400000
112,5,1,5,5,3,5,1,3.800000
# 途中省略
83,5,1,1,1,1,5,1,1.800000
84,5,,,,,5,5,5.000000
85,5,3,1,1,1,5,1,2.200000
86,,,3,5,,5,3,4.000000
87,5,3,5,3,3,5,3,3.800000
,5,5,5,5,5
,5,1,1,1,1
,5.000000,1.892857,3.446429,3.054545,1.666667

3-C. ヒント

条件を満たすために

  • 平均値,最大値,最小値をまとめる型Statsを用意しましょう.
    • フィールドに最大値を表すmax,最小値を表すmin,値の合計値を表すsum,値の数を表すcountを用意しましょう.
    • メソッドに,最大値を返すmax,最小値を返すmin,平均値を返すaverageを定義しましょう.
    • そして,値を追加するputメソッドを用意してください.
      • putメソッドでは,追加された値が最小値,最大値に相当する場合は,それぞれに代入してください.
      • 加えて,sumに値を追加し,countをインクリメントしてください.これらの2つの変数はaverageメソッドで利用します.
  • 次に,1人の学生の成績をまとめる型 StudentScore を用意しましょう.
    • StudentScoreidと課題番号とその評価をキー,バリューとするHashMap型の変数をフィールドとして持つと良いでしょう.
    • また,上記で定義したStats型の変数をフィールドに定義しましょう.
    • そして,評価の最大値を返すmax,最小値を返すmin,平均値を返すaverageメソッドを用意しましょう.
      • それぞれ,Stats型の変数から値を取得します.
    • 最後に,課題番号とその評価を追加するput(String, Integer)を用意し,putで追加された値をStats型の変数にputしましょう.

3-D. 評価項目

ステップ4

4-A. 問題説明

ステップ3の結果に加えて,ステップ3の結果をヒートマップとして出力してください. ヒートマップとは,二次元データの値の大小を色や濃度で表したグラフの一つです. 点数の範囲を調べ,最小値が0,最大値が255となるように点数をスケールさせてください. その計算結果を RGB のどれかに当てはめると良いでしょう(画素の計算方法).

ヒートマップの作成には,グラデーション画像の生成を参照してください. 必要な大きさの BufferedImage を作成し,各画素に対して,setRGB メソッドで色を設定してください. で作成してください. 出力するファイル名は heatmap.pngとしてください.

4-B. 実行例

画像をクリックするごとに赤,緑,青,黄,マゼンタ,シアン,グレー,HSVでヒートマップを作成したものに切り替わります. 色が黒に近いほど点数が低く,白(実際は透明色)は未受験,色が濃くなるほど良い点数であることを表しています. なお,色の種類は自由に決めてもらって構いません(赤,緑,青,黄,マゼンタ,シアン,グレー,HSVのいずれか1つ).

横軸が学生,縦軸が課題を表しています. また,1つの課題の点数に3×3のピクセルを使っています. このように拡大しない場合は,以下の画像の1/3程度の大きさになります.

reading.csv

拡大しない場合の大きさ(赤画素)

writing.csv

拡大しない場合の大きさ(緑画素)

4-C. ヒント

画素の計算方法

Integer maxScore = 10;
Color calculatePixelColor(Integer score){
    if(score == null){
        return new Color(0xff, 0xff, 0xff, 0xff); // 白の透明色
    }
    Double color = Double.valueOf(255.0 * score / maxScore);
    return new Color(value.intValue(), 0, 0); // 赤の場合
}

Color型を利用するには,java.awt.Colorimportする必要があります.

HSV

HSVとは色相(Hue),彩度(Saturation),明度(Value)の3つで1つの色を表す色モデルです. HSB(Hue, Saturation, Brightness)と表現されることもあります. 以下の画像は色相を30度ずつ変更した場合の色の変化です. 色相が0と360は同じ色です. クリックで確認してください.

ヒートマップを作るときに,スコアの点数を色相の0(赤)〜240(青)に割り当てることで,より自然なヒートマップになります. 以下の処理を参考にすると良いでしょう(HSBtoRGBは0.0〜1.0の範囲の数値を受け取ります).

Color calculatePixelColor(Integer score){
    if(score == null){
        return new Color(0xff, 0xff, 0xff, 0xff); // 白の透明色
    }
    Float hue = Float.valueOf(
        (1.0f - 1.0f * score / maxScore) * (240.0f / 360.0f)
    );
    return new Color(Color.HSBtoRGB(hue, 1.0f, 1.0f)); // 赤の場合
}

画素を大きなサイズにする.

最初から画素を大きなものにするのはのではなく,まずは1ピクセルに1課題の点数を割り当てたヒートマップ画像(original_heatmap (BufferedImage型))を作成してください.そして,次の手順のようにして,画素を拡大しましょう. ここでは,1課題を $n \times n$ピクセルに拡大したい場合を考えます.

  1. original_heatmap の大きさの $n$倍のBufferedImageresult_image)を作成する.
  2. result_image から Graphics2D を取得する. Graphics2D g = result_image.createGraphics();
  3. result_imageoriginal_heatmapを拡大して描画する. g.drawImage(original_heatmap, 0, 0, original_heatmap.getWidth() * n, original_heatmap.getHeight() * n, null);

4-D. 評価項目

  • 概要に示した評価項目
  • データを変更しても,例外なく実行結果を出力できるか
    • 人数やスコアの有効値が変更されても例外なく出力されるか.

ステップ5

5-A. 問題説明

各学生が各課題の提出までに要した時間を計算してください. 入力データの6,7カラム目が開始時刻,提出時刻です. この2つから所要時間を計算し,ステップ4の出力に,その時間を追加してください. 各行を次のように出力してください.

学生ID,課題1の点数,課題1の所要時間,課題2の点数,課題2の所要時間,...,点数の最大値,点数の最小値,点数の平均値

上記に加えて,次の内容をオプションで指定できるようにしてください. ただし,提出時間のヒートマップの作成はステップ6で対応するため, ここではオプションで受け取るだけで構いません(ヒートマップの出力部分はステップ4と同じで構いません).

  • ヒートマップの出力先
    • ただし,拡張子と出力フォーマットは一致させてください.
  • ヒートマップの種類
    • 点数のヒートマップか(score),提出時間のヒートマップ(time
      • 提出時間のヒートマップの作成はステップ6で対応するため,オプションで指定可能なようにするだけで構いません.
  • 学生のソート方法
    • ID順(id),成績の平均点順(score),所要時間の平均順(time)のいずれかでソートする(昇順).
  • ヘルプメッセージの表示

なお,ステップ3で作成した独自型(StudentScore)を修正すると,ステップ3が動作しなくなるためStudentScore5に変更しておきましょう.

5-B. 実行例

$ java ScoreAnalyzer5 --help # <= ヘルプメッセージを表示する.
java ScoreAnalyzer5 [OPTIONS] <FILENAME.CSV>
OPTIONS
    --help           このメッセージを表示して終了する.
    --dest <DEST>    ヒートマップの出力先を指定する.
    --sort <ITEM>    指定された項目の昇順でソートする.
    --heatmap <TYPE> ヒートマップの種類を指定する.scoreもしくはtime.
$ java ScoreAnalyzer5 --sort id --dest heatmap_score.png reading.csv
# heatmap_score.png.ppm にヒートマップを出力する.
1,,,,,,,,,10,3,10,10,10.000000
2,10,8,0,9,8,8,6,5,8,5,10,0,6.400000
3,10,5,6,5,4,12,8,5,,,10,4,7.000000
4,6,2,10,7,8,9,8,5,10,8,10,6,8.400000
5,10,4,2,13,10,6,10,8,10,7,10,2,8.400000
# 途中省略
140,6,4,0,1,2,6,6,9,8,7,8,0,4.400000
141,10,2,10,5,6,3,8,3,10,3,10,6,8.800000
142,10,3,10,5,,,8,3,10,1,10,8,9.500000
143,10,6,10,8,8,9,10,11,10,10,10,8,9.600000
144,10,7,6,9,6,8,2,9,10,8,10,2,6.800000
,10,10,10,10,10
,6,0,0,0,0
,9.275591,5.829457,6.047244,7.177419,8.474576
$ java ScoreAnalyzer5 reading.csv --sort score
86,,,,,,,0,0,,,0,0,0.000000
139,8,6,4,11,0,4,2,1,,,8,0,3.500000
100,,,0,10,4,7,6,7,5,12,6,0,3.750000
113,,,2,4,2,6,8,3,,,8,2,4.000000
116,10,5,,,2,0,,,0,10,10,0,4.000000
16,10,15,4,8,0,10,6,3,0,12,10,0,4.000000
# 途中省略
143,10,6,10,8,8,9,10,11,10,10,10,8,9.600000
50,10,4,10,3,10,3,8,8,10,2,10,8,9.600000
58,10,3,10,4,10,6,8,7,10,9,10,8,9.600000
103,10,4,10,5,8,4,10,6,10,2,10,8,9.600000
89,10,6,,,,,,,,,10,10,10.000000
1,,,,,,,,,10,3,10,10,10.000000
136,10,7,10,10,,,,,,,10,10,10.000000
51,10,5,10,7,,,10,9,,,10,10,10.000000
,10,10,10,10,10
,6,0,0,0,0
,9.275591,5.829457,6.047244,7.177419,8.474576

5-C. ヒント

オプションの解析

オプションの解析では,オプションの指定方法が間違っているケースは考える必要はありません. オプションの内容を記録するためにArguments型を用意しましょう. Arguments型にはフィールドとして,オプションの内容を保持する変数を宣言してください. そして,Arguments型に次のようなparseを用意してください.

void parse(String[] args){
    for(Integer i = 0; i < args.length; i++){
        if(!args[i].startsWith("--")){
            arguments.add(args[i]);
        }
        else {
            i = parseOption(args, i);
        }
    }
}
Integer parseOption(String[] args, Integer i) {
    if(Objects.equals(args[i], "--dest")){
        i++;
        this.dest = args[i];
    }
    ...
    return i;
}

拡張子と出力フォーマットを合わせる.

拡張子が望むものかどうかを確認し,望むものでなければ拡張子を追加しましょう.

String updateExtension(String fileName, String wontExtention){
    if(fileName.endsWith(wontExtention)){
        return fileName;
    }
    return fileName + wontExtention;
    // 拡張子を置き換えたい場合は次の処理.
    // Integer index = fileName.lastIndexOf(".");
    // if(index < 0)
    //     return fileName + wontExtention;
    // return fileName.substring(0, index) + wontExtention;
}

上記のメソッドを定義しておくと,次のような結果が得られます.

String name1 = updateExtension("heatmap.png", ".png"); // => "heatmap.png"
String name2 = updateExtension("heatmap.png", ".ppm"); // => "heatmap.png.ppm"
String name3 = updateExtension("heatmap", ".ppm");     // => "heatmap.ppm"

指定の方法でソートする.

次の3つの比較器を用意し,それぞれStudentIdComparator.javaStudentScoreComparator.javaStudentTakenTimeComparator.javaに保存し,同じディレクトリに置いてください.

StudentIdComparator.java
import java.util.Comparator;
public class StudentIdComparator implements Comparator<StudentScore5> {
    public int compare(StudentScore5 ss1, StudentScore5 ss2){
        return ss1.id.compareTo(ss2.id);
    }
}
StudentScoreComparator.java
import java.util.Comparator;
public class StudentScoreComparator implements Comparator<StudentScore5> {
    public int compare(StudentScore5 ss1, StudentScore5 ss2){
        return Double.compare(ss1.average(), ss2.average());
    }
}
StudentTakenTimeComparator.java

averageOfTakenTimeメソッドをStudentScore5に定義し,所要時間の平均を返すようにしてください.

import java.util.Comparator;
public class StudentTakenTimeComparator implements Comparator<StudentScore5> {
    public int compare(StudentScore5 ss1, StudentScore5 ss2){
        return Double.compare(ss1.averageOfTakenTime(), ss2.averageOfTakenTime());
    }
}
ソートされた学生一覧を取得する.

学生一覧がHashMap<String, StudentScore5>に保存されている場合,次のようなプログラムを書くことで, StudentScore5の一覧がソートされた状態で取得できます.

ArrayList<StudentScore5> sortedStudentList(HashMap<String, StudentScore5> map, String sortKey){
    Arraylist<StudentScore5> list = new ArrayList<>(map.values());
    if(Objects.equals(sortKey, "id")){
        Collections.sort(list, new StudentIdComparator());
    }
    else if(Objects.equals(sortKey, "score")){
        Collections.sort(list, new StudentScoreComparator());
    }
    else if(Objects.equals(sortKey, "time")){
        Collections.sort(list, new StudentTakenTimeComparator());
    }
    return list;
}

5-D. 評価項目

  • 概要に示した評価項目
  • オプションの解析を適切に行えているか.
  • オプションが指定された場合,指定されなかった場合の両方で,適切に処理できているか.
  • データを変更しても,例外なく実行結果を出力できるか
    • 人数やスコアの有効値が変更されても例外なく出力されるか.

ステップ6

6-A. 問題説明

ステップ5で指定したヒートマップを出力するようプログラムを変更してください. つまり,--heatmap scoreが指定された場合(もしくは--heatmapオプションが指定されなかった場合)は点数のヒートマップ, --heatmap timeが指定された場合は提出までに要した時間のヒートマップを出力してください. 加えて,ヒートマップもステップ5で指定したソート項目でソートされるようにしてください.

6-B. 実行例

  • reading.csvのスコアのヒートマップ

    ソートなし

    IDでソート

    スコアでソート

    時間でソート

  • reading.csvの時間のヒートマップ

    ソートなし

    IDでソート

    スコアでソート

    時間でソート

6-C. ヒント

6-D. 評価項目

  • 概要に示した評価項目
  • データを変更しても,例外なく実行結果を出力できるか
    • 人数やスコアの有効値が変更されても例外なく出力されるか.

最終課題に向けて

ここに示した内容はこれまでに提出された課題を見て,説明が必要であろうと思われる部分を抜粋しました.

Visual Studio Code で表示されるラベルについて

Visual Studio Codeなどの昨今の IDE (Integrated Development Environment; 統合開発環境) では, EoD (Ease of Development) のため実引数に仮引数の名前を表示する機能があります. 以下に例を図示します.

Visual Studio Codeスクリーンショット

この図中の5, 7行目と12行目にそれぞれ,prefixformat という文字列が見えます. これは IDE の機能により表示されているラベルです. 実際にプログラム中にこのラベルを書くとコンパイルエラーになるため,注意してください.

インデントを揃える.

インデントをしっかりと揃える必要がありますが,手作業でインデントを揃えないようにしましょう. 手作業でインデントすると漏れや間違いが起こる可能性があるためです. 利用しているエディタの一括インデントを行ってください.

Visual Studio Code の場合,Option+Shift+F で一括インデントが行えます.

コンパイル&実行結果を確認する.

提出された練習問題を見てみるとごく単純なコンパイルエラーが残っているケースが見られます. どんなに面倒でも一度コンパイルしてください. そして,コンパイルできたら実行して,結果を確認してください. その際,どのような入力により,どのような結果が期待されるのかを確認してから実行すると良いでしょう.

以下のような場合は,速やかにTA,教員に相談してください. 授業中に挙手する他に,Teams でのチャットでも質問対応しております. 相談により成績が下がることはありませんが,相談せずに未完成のまま提出することは 結果的に成績の低下に繋がります.

  • コンパイル方法がわからない.
  • 実行方法がわからない.
  • コンパイルエラーが修正できない.
  • 期待する結果がわからない.
  • 期待する結果と実行結果の差がわからない.

スコープについて

変数には有効範囲があります.この有効範囲のことを スコープ(scope) と呼びます. プログラム中の {} で囲まれた範囲を ブロック(block) と呼びます. スコープは変数が宣言された後,宣言されたブロックの中でのみ有効です.

メソッドの呼び出しについて

メソッドを呼び出すには,何らかの実体に対して呼び出す必要があります. この実体のことを レシーバ(receiver) と呼びます.

例えば,以下のフィボナッチ数列の $n$番目の値を求めるプログラムFibonacciで考えてみましょう.

public class Fibonacci {
    void run(String[] args) {
        Integer index = 10;
        if(args.length != 0) 
            index = Integer.parseInt(args[0]);
        Integer result = fibonacci(index);
    }
    Integer fibonacci(Integer index) {
        if(index < 2)
            return 1;
        return fibonacci(index - 1) + fibonacci(index - 2);
    }
    public static void main(String[] args) {
        HelloWorld app = new HelloWorld();
        app.run(args);
    }
}

mainメソッド内で,app.run(args) というメソッド呼び出しを行なっています. この呼び出しのappがレシーバ,argsが実引数(arguments),runがメソッド名です.

メソッド呼び出し部分の各名称

runメソッド内やfibonacciメソッド内の fibonacciメソッドの呼び出しには レシーバがないように見えます. これは実はthisというレシーバが隠されており,自分自身を表しています.

次に,8行目のfibonacciメソッドの宣言に注目してください. この行の最初の Integer は返り値の型であり,このメソッドの最後にこの型の値を return する必要があります. fibonacciはメソッド名,括弧内のindexは仮引数(parameter)と呼びます. 波括弧で囲まれた部分はメソッドボディやメソッドの中身と呼ばれ,そのメソッドが行う処理が書かれています.

メソッド定義部分の各名称

メソッド分割について

メソッドは細かく分割しましょう. メソッドを分割するのは,処理に名前をつけるために行います. 処理に名前が適切に付けられていると,処理内容を理解しなくても何が行われるのかを理解できます. つまり,読みやすくなるのです.

適切な名前のためには,命名規則に従うことも重要でしょう. Javaの場合,メソッド名は動詞から始まり,キャメルケースで命名することが推奨されています. このことを意識して適切なメソッド名をつけてみましょう.

最初は適切な名前をつけるのは難しいかもしれません. そのような場合,日本語(ローマ字)で付けるのも良いでしょう.

一方でメソッドを分けるときに,次のようなメソッドの中で他のメソッドを呼び出しているだけのメソッドには分割の意味はありません.

public class SomeClass {
    void run(String[] args) {
        perform(args);
    }
    void perform(String[] args) {
        // 何らかの処理
    }
    // mainメソッドは省略.
}

エラーについて

コンパイルエラーや実行時エラーの場合,エラーメッセージと発生した場所をしっかりと確認しましょう.

コンパイルエラーについて

コンパイルエラーは次のようにエラーの内容とエラーの場所を示してくれます. この例の場合,Fibonacci.java の 18 行目で,「シンボルを見つけられない」というコンパイルエラーです. シンボルを見つけられないというエラーは,典型的には綴りを間違えています. その変数の名前をしっかりと確認しましょう.

Fibonacci.java:18: エラー: シンボルを見つけられません
                prev2 = reslut; // result を代入して,次の i に備える.
                        ^
  シンボル:   変数 reslut
  場所: クラス Fibonacci
エラー1個

また,典型的なコンパイルエラーとその原因は次の通りです. 指摘された箇所を失火路と確認して修正していきましょう.

  • シンボルが見つかりません.
    • 綴りは合っていますか?
    • import は忘れていませんか.
    • 変数の有効範囲(スコープ)は合っていますか?
  • クラスXxxxpublicであり,ファイルXxxx.javaで宣言しなければなりません
    • クラス名とファイル名が一致していますか? 大文字小文字も区別してください.
  • \12288は不正な文字です.
    • 全角スペースが入っていないか確認してください.全角スペースはC言語と同じく文字列以外での利用が許されていません.

以下の内容も併せてご覧ください.

実行時エラーについて

実行時に何らかのエラーが起こった場合,次のようなメッセージが出力されます. このようなメッセージのことをスタックトレース(stack trace)と呼びます.

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
	at FizzBuzz.run(FizzBuzz.java:5)
	at FizzBuzz.main(FizzBuzz.java:25)

1行目にどのようなエラーであるか,2行目以降にプログラムのどこで発生したかが書かれています. どのようなエラーなのかは,thread "main" の後ろを見てみましょう. ここには,エラーの原因となった例外のクラス名が書かれています. 上の例の場合,ArrayIndexOutOfBoundsException,すなわち,array index out of bounds exception です. これは,配列の範囲を超えて要素にアクセスしようとした場合に発生する例外です. 英語の意味がわからない場合,Google 翻訳や DeepL 翻訳にかけてみましょう.

2行目以降は,クラス名.メソッド名(ソースファイル名:行番号) のように出力されています. 表示された場所と例外クラスの名前を手掛かりに実行時エラーが起こらないよう修正しましょう.

ヒント

スタックトレースとは,実行時に例外が発生したときに表示されるエラーメッセージです. 上で示したように,どこでどのような例外が発生したのかを把握する手掛かりとなる重要なメッセージです.

そして,例外が発生してスタックトレースを出力するときになると, 例外が発生した箇所はどのような経緯で呼び出されたのかを辿ってmainメソッドまで到達しようとします. メソッドの呼び出しはスタックで管理されていますので,スタックを辿っていくわけです. このことからスタックトレースと呼ばれています.

スタックトレースは Java に限ったものではなく,例外機構を導入しているプログラム言語であれば 似たような出力が行われます.

例外の責任転嫁について

ファイルの入出力時に発生する可能性のある IOException などの検査例外 と呼ばれる例外は, 例外が発生したときの対応をプログラム中に明示しておかなければコンパイルできないようになっています. 対応方法は以下の2種類です.

  • 例外に対してその場で対応し,プログラム実行を復帰させる.
  • メソッドの呼び出し元に対応を任せる(責任を転嫁する).

この講義では,呼び出し元に責任を転嫁します. 責任を転嫁するには,例外が発生する可能性のあるメソッドのシグネチャに throws 節を追加します.

public class Cat {
    void run(String[] args) throws IOException {
        for(String arg: args) {
            performEach(arg);
        }
    }
    void performEach(String arg) throws IOException {
        BufferedReader in = new BufferedReader(new FileReader(arg));
        String line;
        while((line = in.readLine()) != null)
            System.out.println(line);
        in.close();
    }
    public static void main(String[] args) throws IOException {
        SomeClass app = new SomeClass();
        app.run(args);
    }
}

こうすることで,performEach 内で IOExceptionが発生した場合,呼び出し元である run メソッドに対応を任せます. しかし,run メソッドも同様に throws 節が宣言されており,呼び出し元である main メソッドに対応を任せています. 同様に main メソッドも throws 節があるため,呼び出し元に責任転嫁しています. mainメソッドの呼び出し元とは,javaコマンドです. もし,例外が発生した場合,javaコマンドまで例外が伝播し,そこでスタックトレースが出力されてプログラムが終了することになります.